From 394ab7a86aad009fe5deb996aa5f5375e70f3bb1 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 14 Oct 2026 16:39:37 +0100 Subject: [PATCH 1/4] feat: SDK update for the 2.5.x majors --- CHANGELOG.md | 36 +- README.md | 90 +- .../java/account/create-o-auth-2-session.md | 1 + .../java/account/create-o-auth-2-token.md | 1 + .../java/account/create-passkey-token.md | 22 + docs/examples/java/account/create-passkey.md | 27 + docs/examples/java/account/delete-passkey.md | 26 + docs/examples/java/account/get-passkey.md | 26 + docs/examples/java/account/list-passkeys.md | 27 + .../java/account/update-passkey-token.md | 27 + .../account/update-passkey-verification.md | 27 + docs/examples/java/account/update-passkey.md | 27 + docs/examples/java/analytics/create-event.md | 38 + docs/examples/java/oauth2/authorize-post.md | 2 +- docs/examples/java/oauth2/authorize.md | 2 +- .../oauth2/create-device-authorization.md | 2 +- docs/examples/java/oauth2/create-par.md | 2 +- docs/examples/java/oauth2/create-token.md | 2 +- docs/examples/java/teams/create-membership.md | 1 + .../kotlin/account/create-o-auth-2-session.md | 1 + .../kotlin/account/create-o-auth-2-token.md | 1 + .../kotlin/account/create-passkey-token.md | 13 + .../examples/kotlin/account/create-passkey.md | 16 + .../examples/kotlin/account/delete-passkey.md | 15 + docs/examples/kotlin/account/get-passkey.md | 15 + docs/examples/kotlin/account/list-passkeys.md | 16 + .../kotlin/account/update-passkey-token.md | 16 + .../account/update-passkey-verification.md | 16 + .../examples/kotlin/account/update-passkey.md | 16 + .../examples/kotlin/analytics/create-event.md | 27 + docs/examples/kotlin/oauth2/authorize-post.md | 2 +- docs/examples/kotlin/oauth2/authorize.md | 2 +- .../oauth2/create-device-authorization.md | 2 +- docs/examples/kotlin/oauth2/create-par.md | 2 +- docs/examples/kotlin/oauth2/create-token.md | 2 +- .../kotlin/teams/create-membership.md | 1 + library/build.gradle.kts | 5 +- library/consumer-rules.pro | 27 - library/src/main/AndroidManifest.xml | 41 - library/src/main/java/io/appwrite/Client.kt | 41 +- library/src/main/java/io/appwrite/Topic.kt | 68 - .../main/java/io/appwrite/models/Account.kt | 30 +- .../java/io/appwrite/models/AlgoArgon2.kt | 8 +- .../java/io/appwrite/models/AlgoBcrypt.kt | 2 +- .../main/java/io/appwrite/models/AlgoMd5.kt | 2 +- .../java/io/appwrite/models/AlgoPhpass.kt | 2 +- .../java/io/appwrite/models/AlgoScrypt.kt | 10 +- .../io/appwrite/models/AlgoScryptModified.kt | 8 +- .../main/java/io/appwrite/models/AlgoSha.kt | 2 +- .../src/main/java/io/appwrite/models/App.kt | 52 +- .../io/appwrite/models/AppInstallation.kt | 18 +- .../io/appwrite/models/AppInstallationList.kt | 4 +- .../main/java/io/appwrite/models/AppKey.kt | 16 +- .../java/io/appwrite/models/AppKeyList.kt | 4 +- .../main/java/io/appwrite/models/AppScope.kt | 10 +- .../java/io/appwrite/models/AppScopeList.kt | 4 +- .../main/java/io/appwrite/models/AppSecret.kt | 16 +- .../java/io/appwrite/models/AppSecretList.kt | 4 +- .../io/appwrite/models/AppSecretPlaintext.kt | 16 +- .../main/java/io/appwrite/models/AppsList.kt | 4 +- .../main/java/io/appwrite/models/Continent.kt | 4 +- .../java/io/appwrite/models/ContinentList.kt | 4 +- .../main/java/io/appwrite/models/Country.kt | 4 +- .../java/io/appwrite/models/CountryList.kt | 4 +- .../main/java/io/appwrite/models/Currency.kt | 14 +- .../java/io/appwrite/models/CurrencyList.kt | 4 +- .../main/java/io/appwrite/models/Document.kt | 14 +- .../java/io/appwrite/models/DocumentList.kt | 4 +- .../main/java/io/appwrite/models/Execution.kt | 32 +- .../java/io/appwrite/models/ExecutionList.kt | 4 +- .../src/main/java/io/appwrite/models/File.kt | 32 +- .../main/java/io/appwrite/models/FileList.kt | 4 +- .../main/java/io/appwrite/models/Headers.kt | 4 +- .../main/java/io/appwrite/models/Identity.kt | 22 +- .../java/io/appwrite/models/IdentityList.kt | 4 +- .../src/main/java/io/appwrite/models/Jwt.kt | 2 +- .../main/java/io/appwrite/models/Language.kt | 6 +- .../java/io/appwrite/models/LanguageList.kt | 4 +- .../main/java/io/appwrite/models/Locale.kt | 14 +- .../java/io/appwrite/models/LocaleCode.kt | 4 +- .../java/io/appwrite/models/LocaleCodeList.kt | 4 +- .../java/io/appwrite/models/Membership.kt | 30 +- .../java/io/appwrite/models/MembershipList.kt | 4 +- .../java/io/appwrite/models/MfaChallenge.kt | 8 +- .../java/io/appwrite/models/MfaFactors.kt | 10 +- .../io/appwrite/models/MfaRecoveryCodes.kt | 2 +- .../main/java/io/appwrite/models/MfaType.kt | 4 +- .../java/io/appwrite/models/Oauth2Approve.kt | 2 +- .../io/appwrite/models/Oauth2Authorize.kt | 4 +- .../java/io/appwrite/models/Oauth2Consent.kt | 20 +- .../io/appwrite/models/Oauth2ConsentList.kt | 4 +- .../io/appwrite/models/Oauth2ConsentToken.kt | 22 +- .../appwrite/models/Oauth2ConsentTokenList.kt | 4 +- .../models/Oauth2DeviceAuthorization.kt | 12 +- .../java/io/appwrite/models/Oauth2Grant.kt | 24 +- .../io/appwrite/models/Oauth2Organization.kt | 2 +- .../appwrite/models/Oauth2OrganizationList.kt | 4 +- .../main/java/io/appwrite/models/Oauth2PAR.kt | 4 +- .../java/io/appwrite/models/Oauth2Project.kt | 6 +- .../io/appwrite/models/Oauth2ProjectList.kt | 4 +- .../java/io/appwrite/models/Oauth2Reject.kt | 2 +- .../java/io/appwrite/models/Oauth2Token.kt | 10 +- .../main/java/io/appwrite/models/Passkey.kt | 61 + .../io/appwrite/models/PasskeyChallenge.kt | 61 + .../java/io/appwrite/models/PasskeyList.kt | 37 + .../src/main/java/io/appwrite/models/Phone.kt | 6 +- .../main/java/io/appwrite/models/PhoneList.kt | 4 +- .../main/java/io/appwrite/models/Presence.kt | 12 +- .../java/io/appwrite/models/PresenceList.kt | 4 +- .../src/main/java/io/appwrite/models/Row.kt | 14 +- .../main/java/io/appwrite/models/RowList.kt | 4 +- .../main/java/io/appwrite/models/Session.kt | 58 +- .../java/io/appwrite/models/SessionList.kt | 4 +- .../java/io/appwrite/models/Subscriber.kt | 16 +- .../main/java/io/appwrite/models/Target.kt | 16 +- .../src/main/java/io/appwrite/models/Team.kt | 10 +- .../main/java/io/appwrite/models/TeamList.kt | 4 +- .../src/main/java/io/appwrite/models/Token.kt | 12 +- .../java/io/appwrite/models/TokenSecret.kt | 69 + .../java/io/appwrite/models/Transaction.kt | 12 +- .../io/appwrite/models/TransactionList.kt | 4 +- .../src/main/java/io/appwrite/models/User.kt | 30 +- .../main/java/io/appwrite/services/Account.kt | 289 +++- .../java/io/appwrite/services/Analytics.kt | 83 + .../main/java/io/appwrite/services/Oauth2.kt | 10 +- .../main/java/io/appwrite/services/Push.kt | 1018 ------------ .../io/appwrite/services/PushBackground.kt | 1457 ----------------- .../java/io/appwrite/services/PushCore.kt | 454 ----- .../io/appwrite/services/PushOpenActivity.kt | 57 - .../java/io/appwrite/services/PushReceiver.kt | 33 - .../java/io/appwrite/services/PushService.kt | 67 - .../java/io/appwrite/services/PushWakeups.kt | 55 - .../main/java/io/appwrite/services/Teams.kt | 3 + .../src/test/java/io/appwrite/TopicTest.kt | 53 - 134 files changed, 1438 insertions(+), 3892 deletions(-) create mode 100644 docs/examples/java/account/create-passkey-token.md create mode 100644 docs/examples/java/account/create-passkey.md create mode 100644 docs/examples/java/account/delete-passkey.md create mode 100644 docs/examples/java/account/get-passkey.md create mode 100644 docs/examples/java/account/list-passkeys.md create mode 100644 docs/examples/java/account/update-passkey-token.md create mode 100644 docs/examples/java/account/update-passkey-verification.md create mode 100644 docs/examples/java/account/update-passkey.md create mode 100644 docs/examples/java/analytics/create-event.md create mode 100644 docs/examples/kotlin/account/create-passkey-token.md create mode 100644 docs/examples/kotlin/account/create-passkey.md create mode 100644 docs/examples/kotlin/account/delete-passkey.md create mode 100644 docs/examples/kotlin/account/get-passkey.md create mode 100644 docs/examples/kotlin/account/list-passkeys.md create mode 100644 docs/examples/kotlin/account/update-passkey-token.md create mode 100644 docs/examples/kotlin/account/update-passkey-verification.md create mode 100644 docs/examples/kotlin/account/update-passkey.md create mode 100644 docs/examples/kotlin/analytics/create-event.md delete mode 100644 library/consumer-rules.pro delete mode 100644 library/src/main/java/io/appwrite/Topic.kt create mode 100644 library/src/main/java/io/appwrite/models/Passkey.kt create mode 100644 library/src/main/java/io/appwrite/models/PasskeyChallenge.kt create mode 100644 library/src/main/java/io/appwrite/models/PasskeyList.kt create mode 100644 library/src/main/java/io/appwrite/models/TokenSecret.kt create mode 100644 library/src/main/java/io/appwrite/services/Analytics.kt delete mode 100644 library/src/main/java/io/appwrite/services/Push.kt delete mode 100644 library/src/main/java/io/appwrite/services/PushBackground.kt delete mode 100644 library/src/main/java/io/appwrite/services/PushCore.kt delete mode 100644 library/src/main/java/io/appwrite/services/PushOpenActivity.kt delete mode 100644 library/src/main/java/io/appwrite/services/PushReceiver.kt delete mode 100644 library/src/main/java/io/appwrite/services/PushService.kt delete mode 100644 library/src/main/java/io/appwrite/services/PushWakeups.kt delete mode 100644 library/src/test/java/io/appwrite/TopicTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 21f1b0c0..c9b24e41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,29 +1,27 @@ # Change Log -## 29.1.0-rc.3 +## 30.0.0 -* Added: first background subscription auto-requests `POST_NOTIFICATIONS` (Android 13+) from the visible activity -* Fixed: `onOpen`/`onClose` callbacks fire correctly when joining or leaving the background host -* Fixed: notification taps are recorded after launching the app, so tap-opened screens land on top +* Breaking: requests send `x-appwrite-response-format: 2.5.0`; responses follow the 2.5 shapes +* Breaking: missing response fields map to their documented defaults +* Breaking: `oauth2` `resource` params now take `List` on the authorize, PAR, device and token methods +* Breaking: removed the `Push` service, `PushReceiver`, `Topic`, `Client.setPushEndpoint` and `setPushClientId` +* Updated: `minSdk` lowered to 23, and the push permissions and services left the manifest +* Fixed: a file exactly `CHUNK_SIZE` bytes long uploads in a single request -## 29.1.0-rc.2 +## 29.2.0 -* Added: `getInitialNotification()`, `onNotificationOpened()`, and the `PushNotificationOpened` data class (topic + data) -* Added: `onOpen`/`onClose` callbacks now fire for background push connections -* Added: push auth falls back to the client's cookie-store session -* Fixed: a foreground message with no live callback now still posts a notification -* Updated: notification taps route through an internal `PushOpenActivity` +* Added: `analytics.createEvent` in a new `Analytics` service to send tracking events +* Added: optional `state` on `account.createOAuth2Session` and `account.createOAuth2Token`, returned unchanged on the success and failure URLs +* Added: optional `membershipId` on `teams.createMembership` +* Fixed: `avatars.updatePhoto` sends `PUT`, so uploading a profile photo no longer returns 404 +* Fixed: OAuth2 query parameters (`success`, `failure`, `scopes`, `state`) are URL-encoded, so values containing `&`, `+` or `#` survive the redirect -## 29.1.0-rc.1 +## 29.1.0 -* Updated: regenerated with sdk-generator 5.5.0; no SDK code changes - -## 29.1.0-rc.0 - -* Added: optional `method` param on `Client.chunkedUpload()` to set the upload HTTP method -* Added: background push notifications render the server `notification` title, body, and image -* Updated: `Avatars.updatePhoto()` uploads via `PUT` -* Fixed: an exactly-chunk-sized file uploads in a single request +* Added: `account` passkey methods `listPasskeys`, `createPasskey`, `getPasskey`, `updatePasskey`, `deletePasskey` and `updatePasskeyVerification` +* Added: `account.createPasskeyToken` and `account.updatePasskeyToken` for passkey sign-in +* Added: `Passkey`, `PasskeyList`, `PasskeyChallenge` and `TokenSecret` models ## 29.0.0 diff --git a/README.md b/README.md index afcc18e1..efdbdd36 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ ![Maven Central](https://img.shields.io/maven-central/v/io.appwrite/sdk-for-android.svg?color=green&style=flat-square) ![License](https://img.shields.io/github/license/appwrite/sdk-for-android.svg?style=flat-square) -![Version](https://img.shields.io/badge/api%20version-2.3.0-blue.svg?style=flat-square) +![Version](https://img.shields.io/badge/api%20version-2.5.0-blue.svg?style=flat-square) [![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator) [![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite) [![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord) -**This SDK targets Appwrite server version 2.3.x as shipped on Appwrite Cloud.** Self-hosted releases can lag behind Cloud — if you run an older self-hosted build, use a matching older SDK from [previous releases](https://github.com/appwrite/sdk-for-android/releases) when APIs differ. +**This SDK targets Appwrite server version 2.5.x as shipped on Appwrite Cloud.** Self-hosted releases can lag behind Cloud — if you run an older self-hosted build, use a matching older SDK from [previous releases](https://github.com/appwrite/sdk-for-android/releases) when APIs differ. Appwrite is an open-source backend as a service server that abstracts and simplifies complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the Android SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs) @@ -38,7 +38,7 @@ repositories { Next, add the dependency to your project's `build.gradle(.kts)` file: ```groovy -implementation("io.appwrite:sdk-for-android:29.1.0-rc.3") +implementation("io.appwrite:sdk-for-android:30.0.0") ``` ### Maven @@ -49,7 +49,7 @@ Add this to your project's `pom.xml` file: io.appwrite sdk-for-android - 29.1.0-rc.3 + 30.0.0 ``` @@ -66,88 +66,6 @@ with `@Keep` so R8 keeps their fields: data class Note(val title: String, val body: String) ``` -### Push notifications - -#### Background delivery - -A subscription with `background = true` keeps delivering after the app is backgrounded, its -process is killed, or the device restarts, until it is unsubscribed or `push.close()` is -called (do this on sign-out). No service is kept running: a scheduled job and alarm wake the -app every 15 to 60 seconds to reconnect, and the broker replays what was sent in between -(`retry = true`). It reconnects with the credential saved at subscribe time, so use a session -(`client.setSession(...)`), not a short-lived JWT. - -While the app is not on screen, each message is also posted as a notification that opens -your launch activity with `Push.EXTRA_TOPIC` and `Push.EXTRA_PAYLOAD` in its extras. It shows the -title, body and image sent with `createPush`, falling back to the subscription's `title` and the -raw payload for other messages. On Android 13 and later it needs the `POST_NOTIFICATIONS` -permission: the first background subscription asks for it from the visible activity (the one -`Push` is created with, else the next one to resume), or call -`Push.requestNotificationPermission(activity)` to ask yourself. Set its -icon with `` -in your ``. While the app is on screen your callback shows the message, so no -notification is posted unless the subscription passes `notifyInForeground = true`. To handle -messages that arrive while no callback is listening in code, subclass `PushReceiver` and declare -it with the `io.appwrite.push.MESSAGE` action (see its docs). - -#### Delivery while the app is closed - -Messages sent while the app is closed arrive at the next scheduled wake-up. While the device is -awake that is about every 15 seconds, or about every 60 seconds once exact alarms are allowed; -without exact alarms the wake-ups are inexact, so battery saver can defer them further. In Doze -(screen off and idle for a while) Android limits background alarms, exact ones included, to about -one every nine minutes, so a closed app can take several minutes to receive a message: allowing -exact alarms makes wake-ups punctual, it does not lift Doze. For immediate delivery, also in -Doze, use `push.setForeground(true)` (see below). - -The SDK uses exact alarms on its own whenever the app may schedule them. To allow it: - -1. Declare the permissions in your app's `AndroidManifest.xml`. Both are optional and subject to - Google Play policy: `SCHEDULE_EXACT_ALARM` needs a declaration in the Play Console, and - `USE_EXACT_ALARM` is reserved for alarm, clock and calendar apps (the SDK does not use it). - - ```xml - - - ``` - -2. On Android 13 and later the user has to allow exact alarms, under Settings > Apps > Special app - access > Alarms & reminders. Android 12 grants a declared `SCHEDULE_EXACT_ALARM` - automatically, and older versions need nothing. Check with `Push.backgroundStatus(context)`: when `bestEffort` is - true, explain why to the user, then from a user action open that screen with `Push.requestExactAlarms(context)`, or - ask for the battery-optimisation exemption with `Push.requestIgnoreBatteryOptimizations(context)`. Both return false when there is - nothing to ask, including when the permission is not declared. The SDK never opens these - screens on its own. - -If the user force-stops the app (Settings > Force stop, and on some devices swiping it away from -recents), Android cancels its alarms and jobs: nothing is delivered until the app is opened -again, and the broker then replays what was sent meanwhile. - -For immediate delivery even after a kill and during Doze, call `push.setForeground(true)` from -the foreground: it adds a foreground service -with a quiet ongoing notification. If you never enable it, you can remove the service from -your merged manifest, so the Play Console does not ask you to declare it: - -```xml - - -``` - -#### Opening a tapped notification - -Read the `data` sent with `createPush` when the user taps a background notification: - -```kotlin -// The tap that launched the app (reported once, so call it at startup). -push.getInitialNotification()?.let { openSale(it.data["saleId"]) } - -// Taps while the app is running, including in the background. -val stop = push.onNotificationOpened { opened -> openSale(opened.data["saleId"]) } -``` - -A tap made while nothing listens is kept for `getInitialNotification()`. The tap intent still -carries `Push.EXTRA_TOPIC` and `Push.EXTRA_PAYLOAD` for apps that read their activity's intent. - ## Getting Started diff --git a/docs/examples/java/account/create-o-auth-2-session.md b/docs/examples/java/account/create-o-auth-2-session.md index da547421..f2a07eff 100644 --- a/docs/examples/java/account/create-o-auth-2-session.md +++ b/docs/examples/java/account/create-o-auth-2-session.md @@ -17,6 +17,7 @@ account.createOAuth2Session( "https://example.com", // success (optional) "https://example.com", // failure (optional) List.of(), // scopes (optional) + "", // state (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/java/account/create-o-auth-2-token.md b/docs/examples/java/account/create-o-auth-2-token.md index ae685850..885c066f 100644 --- a/docs/examples/java/account/create-o-auth-2-token.md +++ b/docs/examples/java/account/create-o-auth-2-token.md @@ -17,6 +17,7 @@ account.createOAuth2Token( "https://example.com", // success (optional) "https://example.com", // failure (optional) List.of(), // scopes (optional) + "", // state (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/java/account/create-passkey-token.md b/docs/examples/java/account/create-passkey-token.md new file mode 100644 index 00000000..12db5fe5 --- /dev/null +++ b/docs/examples/java/account/create-passkey-token.md @@ -0,0 +1,22 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.createPasskeyToken(new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); +})); +``` diff --git a/docs/examples/java/account/create-passkey.md b/docs/examples/java/account/create-passkey.md new file mode 100644 index 00000000..39a9a29d --- /dev/null +++ b/docs/examples/java/account/create-passkey.md @@ -0,0 +1,27 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.createPasskey( + "", // passkeyId (optional) + "", // name (optional) + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/delete-passkey.md b/docs/examples/java/account/delete-passkey.md new file mode 100644 index 00000000..a59d3848 --- /dev/null +++ b/docs/examples/java/account/delete-passkey.md @@ -0,0 +1,26 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.deletePasskey( + "", // passkeyId + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/get-passkey.md b/docs/examples/java/account/get-passkey.md new file mode 100644 index 00000000..5d988e86 --- /dev/null +++ b/docs/examples/java/account/get-passkey.md @@ -0,0 +1,26 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.getPasskey( + "", // passkeyId + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/list-passkeys.md b/docs/examples/java/account/list-passkeys.md new file mode 100644 index 00000000..6b04b1fb --- /dev/null +++ b/docs/examples/java/account/list-passkeys.md @@ -0,0 +1,27 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.listPasskeys( + List.of(), // queries (optional) + false, // total (optional) + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/update-passkey-token.md b/docs/examples/java/account/update-passkey-token.md new file mode 100644 index 00000000..6370aaaf --- /dev/null +++ b/docs/examples/java/account/update-passkey-token.md @@ -0,0 +1,27 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.updatePasskeyToken( + "", // challengeId + Map.of("a", "b"), // credential + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/update-passkey-verification.md b/docs/examples/java/account/update-passkey-verification.md new file mode 100644 index 00000000..53eed3a1 --- /dev/null +++ b/docs/examples/java/account/update-passkey-verification.md @@ -0,0 +1,27 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.updatePasskeyVerification( + "", // passkeyId + Map.of("a", "b"), // credential + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/update-passkey.md b/docs/examples/java/account/update-passkey.md new file mode 100644 index 00000000..3cf1078a --- /dev/null +++ b/docs/examples/java/account/update-passkey.md @@ -0,0 +1,27 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.updatePasskey( + "", // passkeyId + "", // name + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/analytics/create-event.md b/docs/examples/java/analytics/create-event.md new file mode 100644 index 00000000..e2b8dbe0 --- /dev/null +++ b/docs/examples/java/analytics/create-event.md @@ -0,0 +1,38 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Analytics; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Analytics analytics = new Analytics(client); + +analytics.createEvent( + "", // propertyId + "", // name + "https://example.com", // url + "", // domain (optional) + "", // referrer (optional) + 0L, // screenWidth (optional) + "", // sessionHash (optional) + 0L, // scrollDepth (optional) + 0L, // engagementTime (optional) + List.of(), // props (optional) + "", // userId (optional) + "", // ip (optional) + "", // userAgent (optional) + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/oauth2/authorize-post.md b/docs/examples/java/oauth2/authorize-post.md index 99ed6b8e..077546fd 100644 --- a/docs/examples/java/oauth2/authorize-post.md +++ b/docs/examples/java/oauth2/authorize-post.md @@ -23,7 +23,7 @@ oauth2.authorizePost( "", // prompt (optional) 0L, // max_age (optional) "", // authorization_details (optional) - "", // resource (optional) + List.of(), // resource (optional) "", // audience (optional) "", // request_uri (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/java/oauth2/authorize.md b/docs/examples/java/oauth2/authorize.md index 62a452c8..d5b72cf1 100644 --- a/docs/examples/java/oauth2/authorize.md +++ b/docs/examples/java/oauth2/authorize.md @@ -23,7 +23,7 @@ oauth2.authorize( "", // prompt (optional) 0L, // max_age (optional) "", // authorization_details (optional) - "", // resource (optional) + List.of(), // resource (optional) "", // audience (optional) "", // request_uri (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/java/oauth2/create-device-authorization.md b/docs/examples/java/oauth2/create-device-authorization.md index 0b65a1ab..c2fc4adb 100644 --- a/docs/examples/java/oauth2/create-device-authorization.md +++ b/docs/examples/java/oauth2/create-device-authorization.md @@ -15,7 +15,7 @@ oauth2.createDeviceAuthorization( "", // client_id (optional) "", // scope (optional) "", // authorization_details (optional) - "", // resource (optional) + List.of(), // resource (optional) "", // audience (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/java/oauth2/create-par.md b/docs/examples/java/oauth2/create-par.md index e65bdb54..e1f14de9 100644 --- a/docs/examples/java/oauth2/create-par.md +++ b/docs/examples/java/oauth2/create-par.md @@ -23,7 +23,7 @@ oauth2.createPAR( "", // prompt (optional) 0L, // max_age (optional) "", // authorization_details (optional) - "", // resource (optional) + List.of(), // resource (optional) "", // audience (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/java/oauth2/create-token.md b/docs/examples/java/oauth2/create-token.md index 39b6fa25..5c808df3 100644 --- a/docs/examples/java/oauth2/create-token.md +++ b/docs/examples/java/oauth2/create-token.md @@ -20,7 +20,7 @@ oauth2.createToken( "", // client_secret (optional) "", // code_verifier (optional) "https://example.com", // redirect_uri (optional) - "", // resource (optional) + List.of(), // resource (optional) "", // audience (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/java/teams/create-membership.md b/docs/examples/java/teams/create-membership.md index d7fba7c1..b6d23a6a 100644 --- a/docs/examples/java/teams/create-membership.md +++ b/docs/examples/java/teams/create-membership.md @@ -19,6 +19,7 @@ teams.createMembership( "+12065550100", // phone (optional) "https://example.com", // url (optional) "", // name (optional) + "", // membershipId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/kotlin/account/create-o-auth-2-session.md b/docs/examples/kotlin/account/create-o-auth-2-session.md index cde941c7..c9b3a92a 100644 --- a/docs/examples/kotlin/account/create-o-auth-2-session.md +++ b/docs/examples/kotlin/account/create-o-auth-2-session.md @@ -15,5 +15,6 @@ account.createOAuth2Session( success = "https://example.com", // (optional) failure = "https://example.com", // (optional) scopes = listOf(), // (optional) + state = "", // (optional) ) ``` diff --git a/docs/examples/kotlin/account/create-o-auth-2-token.md b/docs/examples/kotlin/account/create-o-auth-2-token.md index 5e905649..3d5b0d77 100644 --- a/docs/examples/kotlin/account/create-o-auth-2-token.md +++ b/docs/examples/kotlin/account/create-o-auth-2-token.md @@ -15,5 +15,6 @@ account.createOAuth2Token( success = "https://example.com", // (optional) failure = "https://example.com", // (optional) scopes = listOf(), // (optional) + state = "", // (optional) ) ``` diff --git a/docs/examples/kotlin/account/create-passkey-token.md b/docs/examples/kotlin/account/create-passkey-token.md new file mode 100644 index 00000000..52b6bf3d --- /dev/null +++ b/docs/examples/kotlin/account/create-passkey-token.md @@ -0,0 +1,13 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.createPasskeyToken() +``` diff --git a/docs/examples/kotlin/account/create-passkey.md b/docs/examples/kotlin/account/create-passkey.md new file mode 100644 index 00000000..69a06515 --- /dev/null +++ b/docs/examples/kotlin/account/create-passkey.md @@ -0,0 +1,16 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.createPasskey( + passkeyId = "", // (optional) + name = "", // (optional) +) +``` diff --git a/docs/examples/kotlin/account/delete-passkey.md b/docs/examples/kotlin/account/delete-passkey.md new file mode 100644 index 00000000..08bf8fd6 --- /dev/null +++ b/docs/examples/kotlin/account/delete-passkey.md @@ -0,0 +1,15 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.deletePasskey( + passkeyId = "", +) +``` diff --git a/docs/examples/kotlin/account/get-passkey.md b/docs/examples/kotlin/account/get-passkey.md new file mode 100644 index 00000000..ddf9e833 --- /dev/null +++ b/docs/examples/kotlin/account/get-passkey.md @@ -0,0 +1,15 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.getPasskey( + passkeyId = "", +) +``` diff --git a/docs/examples/kotlin/account/list-passkeys.md b/docs/examples/kotlin/account/list-passkeys.md new file mode 100644 index 00000000..74c8add8 --- /dev/null +++ b/docs/examples/kotlin/account/list-passkeys.md @@ -0,0 +1,16 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.listPasskeys( + queries = listOf(), // (optional) + total = false, // (optional) +) +``` diff --git a/docs/examples/kotlin/account/update-passkey-token.md b/docs/examples/kotlin/account/update-passkey-token.md new file mode 100644 index 00000000..f48e29f9 --- /dev/null +++ b/docs/examples/kotlin/account/update-passkey-token.md @@ -0,0 +1,16 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.updatePasskeyToken( + challengeId = "", + credential = mapOf( "a" to "b" ), +) +``` diff --git a/docs/examples/kotlin/account/update-passkey-verification.md b/docs/examples/kotlin/account/update-passkey-verification.md new file mode 100644 index 00000000..41c016a8 --- /dev/null +++ b/docs/examples/kotlin/account/update-passkey-verification.md @@ -0,0 +1,16 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.updatePasskeyVerification( + passkeyId = "", + credential = mapOf( "a" to "b" ), +) +``` diff --git a/docs/examples/kotlin/account/update-passkey.md b/docs/examples/kotlin/account/update-passkey.md new file mode 100644 index 00000000..c8c2bc34 --- /dev/null +++ b/docs/examples/kotlin/account/update-passkey.md @@ -0,0 +1,16 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.updatePasskey( + passkeyId = "", + name = "", +) +``` diff --git a/docs/examples/kotlin/analytics/create-event.md b/docs/examples/kotlin/analytics/create-event.md new file mode 100644 index 00000000..820e0bac --- /dev/null +++ b/docs/examples/kotlin/analytics/create-event.md @@ -0,0 +1,27 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Analytics + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val analytics = Analytics(client) + +val result = analytics.createEvent( + propertyId = "", + name = "", + url = "https://example.com", + domain = "", // (optional) + referrer = "", // (optional) + screenWidth = 0, // (optional) + sessionHash = "", // (optional) + scrollDepth = 0, // (optional) + engagementTime = 0, // (optional) + props = listOf(), // (optional) + userId = "", // (optional) + ip = "", // (optional) + userAgent = "", // (optional) +) +``` diff --git a/docs/examples/kotlin/oauth2/authorize-post.md b/docs/examples/kotlin/oauth2/authorize-post.md index 88bc4ee0..ea490994 100644 --- a/docs/examples/kotlin/oauth2/authorize-post.md +++ b/docs/examples/kotlin/oauth2/authorize-post.md @@ -21,7 +21,7 @@ val result = oauth2.authorizePost( prompt = "", // (optional) maxAge = 0, // (optional) authorizationDetails = "", // (optional) - resource = "", // (optional) + resource = listOf(), // (optional) audience = "", // (optional) requestUri = "", // (optional) ) diff --git a/docs/examples/kotlin/oauth2/authorize.md b/docs/examples/kotlin/oauth2/authorize.md index 4d307e8d..6c598343 100644 --- a/docs/examples/kotlin/oauth2/authorize.md +++ b/docs/examples/kotlin/oauth2/authorize.md @@ -21,7 +21,7 @@ val result = oauth2.authorize( prompt = "", // (optional) maxAge = 0, // (optional) authorizationDetails = "", // (optional) - resource = "", // (optional) + resource = listOf(), // (optional) audience = "", // (optional) requestUri = "", // (optional) ) diff --git a/docs/examples/kotlin/oauth2/create-device-authorization.md b/docs/examples/kotlin/oauth2/create-device-authorization.md index ab307c37..d4fb9b02 100644 --- a/docs/examples/kotlin/oauth2/create-device-authorization.md +++ b/docs/examples/kotlin/oauth2/create-device-authorization.md @@ -13,7 +13,7 @@ val result = oauth2.createDeviceAuthorization( clientId = "", // (optional) scope = "", // (optional) authorizationDetails = "", // (optional) - resource = "", // (optional) + resource = listOf(), // (optional) audience = "", // (optional) ) ``` diff --git a/docs/examples/kotlin/oauth2/create-par.md b/docs/examples/kotlin/oauth2/create-par.md index 752c64eb..213096c1 100644 --- a/docs/examples/kotlin/oauth2/create-par.md +++ b/docs/examples/kotlin/oauth2/create-par.md @@ -21,7 +21,7 @@ val result = oauth2.createPAR( prompt = "", // (optional) maxAge = 0, // (optional) authorizationDetails = "", // (optional) - resource = "", // (optional) + resource = listOf(), // (optional) audience = "", // (optional) ) ``` diff --git a/docs/examples/kotlin/oauth2/create-token.md b/docs/examples/kotlin/oauth2/create-token.md index 5e23344f..a0e4f20e 100644 --- a/docs/examples/kotlin/oauth2/create-token.md +++ b/docs/examples/kotlin/oauth2/create-token.md @@ -18,7 +18,7 @@ val result = oauth2.createToken( clientSecret = "", // (optional) codeVerifier = "", // (optional) redirectUri = "https://example.com", // (optional) - resource = "", // (optional) + resource = listOf(), // (optional) audience = "", // (optional) ) ``` diff --git a/docs/examples/kotlin/teams/create-membership.md b/docs/examples/kotlin/teams/create-membership.md index 219119af..2f73c039 100644 --- a/docs/examples/kotlin/teams/create-membership.md +++ b/docs/examples/kotlin/teams/create-membership.md @@ -17,5 +17,6 @@ val result = teams.createMembership( phone = "+12065550100", // (optional) url = "https://example.com", // (optional) name = "", // (optional) + membershipId = "", // (optional) ) ``` diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 3a60a6b3..45641217 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -46,11 +46,9 @@ android { } defaultConfig { - minSdk = 24 + minSdk = 23 buildConfigField("String", "SDK_VERSION", "\"$publishVersion\"") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - // Push: R8 rules for the bundled MQTT client, applied to consuming apps. - consumerProguardFiles("consumer-rules.pro") } buildTypes { @@ -84,7 +82,6 @@ dependencies { api("com.squareup.okhttp3:okhttp:5.5.0") implementation("com.google.code.gson:gson:2.14.0") - implementation("com.hivemq:hivemq-mqtt-client-shaded:1.3.6") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.11.0") implementation("androidx.lifecycle:lifecycle-common-java8:2.11.0") diff --git a/library/consumer-rules.pro b/library/consumer-rules.pro deleted file mode 100644 index 04d7db2c..00000000 --- a/library/consumer-rules.pro +++ /dev/null @@ -1,27 +0,0 @@ -# Push: rules applied to apps that shrink with R8. -# -# The shaded HiveMQ MQTT client relocates Netty and JCTools under -# com.hivemq.client.internal.shaded. Netty references optional classes that are never loaded -# on Android: native transports, the HTTP/WebSocket codecs, proxy handlers, alternative TLS -# providers (tcnative, BouncyCastle, Conscrypt, Jetty ALPN) and logging backends. --dontwarn com.hivemq.client.internal.shaded.io.netty.channel.epoll.** --dontwarn com.hivemq.client.internal.shaded.io.netty.channel.kqueue.** --dontwarn com.hivemq.client.internal.shaded.io.netty.handler.codec.http.** --dontwarn com.hivemq.client.internal.shaded.io.netty.handler.proxy.** --dontwarn com.hivemq.client.internal.shaded.io.netty.internal.tcnative.** --dontwarn org.bouncycastle.** --dontwarn org.conscrypt.** --dontwarn org.eclipse.jetty.** --dontwarn org.apache.log4j.** --dontwarn org.apache.logging.log4j.** --dontwarn org.slf4j.** --dontwarn reactor.blockhound.** - -# Netty and JCTools reach their own fields by name, through atomic field updaters and Unsafe offsets. --keepclassmembernames class com.hivemq.client.internal.shaded.io.netty.** { ; } --keepclassmembers class com.hivemq.client.internal.shaded.org.jctools.** { ; } - -# Netty's leak detector looks these methods up by name when its classes load. --keepclassmembers class com.hivemq.client.internal.shaded.io.netty.buffer.AbstractByteBufAllocator { *** toLeakAwareBuffer(...); } --keepclassmembers class com.hivemq.client.internal.shaded.io.netty.buffer.AdvancedLeakAwareByteBuf { *** recordLeakNonRefCountingOperation(...); } --keepclassmembers class com.hivemq.client.internal.shaded.io.netty.util.ReferenceCountUtil { *** touch(...); } diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml index d2b7d500..1dcf875e 100644 --- a/library/src/main/AndroidManifest.xml +++ b/library/src/main/AndroidManifest.xml @@ -3,46 +3,5 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/library/src/main/java/io/appwrite/Client.kt b/library/src/main/java/io/appwrite/Client.kt index 3df991a3..2c5d2df2 100644 --- a/library/src/main/java/io/appwrite/Client.kt +++ b/library/src/main/java/io/appwrite/Client.kt @@ -50,11 +50,7 @@ class Client @JvmOverloads constructor( var endpoint: String = "https://cloud.appwrite.io/v1", @set:JvmSynthetic var endpointRealtime: String? = null, - private var selfSigned: Boolean = false, - @set:JvmSynthetic - var endpointPush: String? = null, - @set:JvmSynthetic - var pushClientId: String? = null + private var selfSigned: Boolean = false ) : CoroutineScope { companion object { @@ -101,8 +97,8 @@ class Client @JvmOverloads constructor( "x-sdk-name" to "Android", "x-sdk-platform" to "client", "x-sdk-language" to "android", - "x-sdk-version" to "29.1.0-rc.3", - "x-appwrite-response-format" to "2.3.0" + "x-sdk-version" to "30.0.0", + "x-appwrite-response-format" to "2.5.0" ) config = mutableMapOf() @@ -329,37 +325,6 @@ class Client @JvmOverloads constructor( return this } - /** - * Set push endpoint - * - * The MQTT broker URL the [io.appwrite.services.Push] service connects to, e.g. - * `mqtt://host:1883` or `mqtts://host:8883`. - * - * @param endpoint - * - * @return this - */ - fun setPushEndpoint(endpoint: String): Client { - this.endpointPush = endpoint - return this - } - - /** - * Set push client id - * - * A stable client id for the [io.appwrite.services.Push] service. The broker keys its - * offline-replay cursor on this id, so pass a stable value to resume replay across - * restarts. Defaults to the userId decoded from the JWT (else the credential) when unset. - * - * @param pushClientId - * - * @return this - */ - fun setPushClientId(pushClientId: String): Client { - this.pushClientId = pushClientId - return this - } - /** * Add Header * diff --git a/library/src/main/java/io/appwrite/Topic.kt b/library/src/main/java/io/appwrite/Topic.kt deleted file mode 100644 index 8945da0e..00000000 --- a/library/src/main/java/io/appwrite/Topic.kt +++ /dev/null @@ -1,68 +0,0 @@ -package io.appwrite - -/** - * Validate the levels passed to `path()`: a non-empty list of non-empty strings, none holding - * the level separator `/` or the wildcards `+` and `#` (use `any()` and `all()` for those). - */ -private fun levelsOf(levels: List): List { - require(levels.isNotEmpty()) { "path() needs at least one level" } - levels.forEachIndexed { index, level -> - require(level.isNotEmpty()) { "empty level at index $index" } - require(!level.contains("/")) { - val parts = level.split("/").joinToString(", ") { "'$it'" } - "\"$level\" contains \"/\", split it: path([$parts])" - } - require(level != "+") { "\"+\" is reserved, use any()" } - require(!level.contains("+")) { "\"$level\" contains \"+\", use any() for wildcards" } - require(level != "#") { "\"#\" is reserved, use all()" } - require(!level.contains("#")) { "\"$level\" contains \"#\", use all() for wildcards" } - } - return levels.toList() -} - -/** - * Helper to build MQTT topics for [services.Push], level by level. - * - * ``` - * Topic.path(listOf("user", userId, "notification")) // "user/123/notification" - * Topic.path(listOf("user")).any().path(listOf("notification")) // "user/+/notification" - * Topic.path(listOf("org")).any().path(listOf("logs")).all() // "org/+/logs/#" - * ``` - * - * `any()` matches exactly one level and `all()` everything below, at any depth (including the - * parent itself). `all()` ends the topic, so it returns a [ResolvedTopic]. - * - * @throws IllegalArgumentException from `path()` when a level is empty or holds `/`, `+` or `#`. - */ -class Topic private constructor( - private val levels: List, -) { - /** Append the given levels. */ - fun path(levels: List): Topic = Topic(this.levels + levelsOf(levels)) - - /** Append a single-level wildcard (`+`): matches exactly one level. */ - fun any(): Topic = Topic(levels + "+") - - /** Append the multi-level wildcard (`#`) and end the topic. */ - fun all(): ResolvedTopic = ResolvedTopic(levels + "#") - - override fun toString(): String = levels.joinToString("/") - - companion object { - /** Start a topic with the given levels, e.g. `Topic.path(listOf("user", userId))`. */ - fun path(levels: List): Topic = Topic(levelsOf(levels)) - - /** Start a topic with a single-level wildcard (`+`). */ - fun any(): Topic = Topic(listOf("+")) - - /** The multi-level wildcard alone (`#`): matches every topic. */ - fun all(): ResolvedTopic = ResolvedTopic(listOf("#")) - } -} - -/** A topic ended by `all()`: nothing can be appended after the `#` wildcard. */ -class ResolvedTopic internal constructor( - private val levels: List, -) { - override fun toString(): String = levels.joinToString("/") -} diff --git a/library/src/main/java/io/appwrite/models/Account.kt b/library/src/main/java/io/appwrite/models/Account.kt index 38865ff8..2d461144 100644 --- a/library/src/main/java/io/appwrite/models/Account.kt +++ b/library/src/main/java/io/appwrite/models/Account.kt @@ -237,28 +237,28 @@ data class Account( map: Map, nestedType: Class ) = Account( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - name = map["name"] as String, - registration = map["registration"] as String, - status = map["status"] as Boolean, - labels = map["labels"] as List, - passwordUpdate = map["passwordUpdate"] as String, - email = map["email"] as String, - phone = map["phone"] as String, - emailVerification = map["emailVerification"] as Boolean, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + registration = (if (map.containsKey("registration")) map["registration"] else "") as String, + status = (if (map.containsKey("status")) map["status"] else true) as Boolean, + labels = (if (map.containsKey("labels")) map["labels"] else listOf()) as List, + passwordUpdate = (if (map.containsKey("passwordUpdate")) map["passwordUpdate"] else "") as String, + email = (if (map.containsKey("email")) map["email"] else "") as String, + phone = (if (map.containsKey("phone")) map["phone"] else "") as String, + emailVerification = (if (map.containsKey("emailVerification")) map["emailVerification"] else false) as Boolean, emailCanonical = map["emailCanonical"] as? String, emailIsFree = map["emailIsFree"] as? Boolean, emailIsDisposable = map["emailIsDisposable"] as? Boolean, emailIsCorporate = map["emailIsCorporate"] as? Boolean, emailIsCanonical = map["emailIsCanonical"] as? Boolean, passwordPwned = map["passwordPwned"] as? Boolean, - phoneVerification = map["phoneVerification"] as Boolean, - mfa = map["mfa"] as Boolean, + phoneVerification = (if (map.containsKey("phoneVerification")) map["phoneVerification"] else false) as Boolean, + mfa = (if (map.containsKey("mfa")) map["mfa"] else false) as Boolean, prefs = Preferences.from(map = map["prefs"] as Map, nestedType), - targets = (map["targets"] as List>).map { Target.from(map = it) }, - accessedAt = map["accessedAt"] as String, + targets = ((if (map.containsKey("targets")) map["targets"] else listOf()) as List>).map { Target.from(map = it) }, + accessedAt = (if (map.containsKey("accessedAt")) map["accessedAt"] else "") as String, impersonator = map["impersonator"] as? Boolean, impersonatorUserId = map["impersonatorUserId"] as? String, ) diff --git a/library/src/main/java/io/appwrite/models/AlgoArgon2.kt b/library/src/main/java/io/appwrite/models/AlgoArgon2.kt index 39773328..1d26dfd3 100644 --- a/library/src/main/java/io/appwrite/models/AlgoArgon2.kt +++ b/library/src/main/java/io/appwrite/models/AlgoArgon2.kt @@ -44,10 +44,10 @@ data class AlgoArgon2( fun from( map: Map, ) = AlgoArgon2( - type = map["type"] as String, - memoryCost = (map["memoryCost"] as Number).toLong(), - timeCost = (map["timeCost"] as Number).toLong(), - threads = (map["threads"] as Number).toLong(), + type = (if (map.containsKey("type")) map["type"] else "argon2") as String, + memoryCost = ((if (map.containsKey("memoryCost")) map["memoryCost"] else 0) as Number).toLong(), + timeCost = ((if (map.containsKey("timeCost")) map["timeCost"] else 0) as Number).toLong(), + threads = ((if (map.containsKey("threads")) map["threads"] else 0) as Number).toLong(), ) } } diff --git a/library/src/main/java/io/appwrite/models/AlgoBcrypt.kt b/library/src/main/java/io/appwrite/models/AlgoBcrypt.kt index 38317807..2a99dad7 100644 --- a/library/src/main/java/io/appwrite/models/AlgoBcrypt.kt +++ b/library/src/main/java/io/appwrite/models/AlgoBcrypt.kt @@ -23,7 +23,7 @@ data class AlgoBcrypt( fun from( map: Map, ) = AlgoBcrypt( - type = map["type"] as String, + type = (if (map.containsKey("type")) map["type"] else "bcrypt") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/AlgoMd5.kt b/library/src/main/java/io/appwrite/models/AlgoMd5.kt index e46eb99b..d683ea70 100644 --- a/library/src/main/java/io/appwrite/models/AlgoMd5.kt +++ b/library/src/main/java/io/appwrite/models/AlgoMd5.kt @@ -23,7 +23,7 @@ data class AlgoMd5( fun from( map: Map, ) = AlgoMd5( - type = map["type"] as String, + type = (if (map.containsKey("type")) map["type"] else "md5") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/AlgoPhpass.kt b/library/src/main/java/io/appwrite/models/AlgoPhpass.kt index d457a81d..3eba6cad 100644 --- a/library/src/main/java/io/appwrite/models/AlgoPhpass.kt +++ b/library/src/main/java/io/appwrite/models/AlgoPhpass.kt @@ -23,7 +23,7 @@ data class AlgoPhpass( fun from( map: Map, ) = AlgoPhpass( - type = map["type"] as String, + type = (if (map.containsKey("type")) map["type"] else "phpass") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/AlgoScrypt.kt b/library/src/main/java/io/appwrite/models/AlgoScrypt.kt index 92147046..789061fe 100644 --- a/library/src/main/java/io/appwrite/models/AlgoScrypt.kt +++ b/library/src/main/java/io/appwrite/models/AlgoScrypt.kt @@ -51,11 +51,11 @@ data class AlgoScrypt( fun from( map: Map, ) = AlgoScrypt( - type = map["type"] as String, - costCpu = (map["costCpu"] as Number).toLong(), - costMemory = (map["costMemory"] as Number).toLong(), - costParallel = (map["costParallel"] as Number).toLong(), - length = (map["length"] as Number).toLong(), + type = (if (map.containsKey("type")) map["type"] else "scrypt") as String, + costCpu = ((if (map.containsKey("costCpu")) map["costCpu"] else 8) as Number).toLong(), + costMemory = ((if (map.containsKey("costMemory")) map["costMemory"] else 14) as Number).toLong(), + costParallel = ((if (map.containsKey("costParallel")) map["costParallel"] else 1) as Number).toLong(), + length = ((if (map.containsKey("length")) map["length"] else 64) as Number).toLong(), ) } } diff --git a/library/src/main/java/io/appwrite/models/AlgoScryptModified.kt b/library/src/main/java/io/appwrite/models/AlgoScryptModified.kt index 539828ac..ff306fa4 100644 --- a/library/src/main/java/io/appwrite/models/AlgoScryptModified.kt +++ b/library/src/main/java/io/appwrite/models/AlgoScryptModified.kt @@ -44,10 +44,10 @@ data class AlgoScryptModified( fun from( map: Map, ) = AlgoScryptModified( - type = map["type"] as String, - salt = map["salt"] as String, - saltSeparator = map["saltSeparator"] as String, - signerKey = map["signerKey"] as String, + type = (if (map.containsKey("type")) map["type"] else "scryptMod") as String, + salt = (if (map.containsKey("salt")) map["salt"] else "") as String, + saltSeparator = (if (map.containsKey("saltSeparator")) map["saltSeparator"] else "") as String, + signerKey = (if (map.containsKey("signerKey")) map["signerKey"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/AlgoSha.kt b/library/src/main/java/io/appwrite/models/AlgoSha.kt index 45fb0104..5c52f087 100644 --- a/library/src/main/java/io/appwrite/models/AlgoSha.kt +++ b/library/src/main/java/io/appwrite/models/AlgoSha.kt @@ -23,7 +23,7 @@ data class AlgoSha( fun from( map: Map, ) = AlgoSha( - type = map["type"] as String, + type = (if (map.containsKey("type")) map["type"] else "sha") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/App.kt b/library/src/main/java/io/appwrite/models/App.kt index f4a715c8..9125cdcf 100644 --- a/library/src/main/java/io/appwrite/models/App.kt +++ b/library/src/main/java/io/appwrite/models/App.kt @@ -198,32 +198,32 @@ data class App( fun from( map: Map, ) = App( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - name = map["name"] as String, - description = map["description"] as String, - clientUri = map["clientUri"] as String, - logoUri = map["logoUri"] as String, - privacyPolicyUrl = map["privacyPolicyUrl"] as String, - termsUrl = map["termsUrl"] as String, - contacts = map["contacts"] as List, - tagline = map["tagline"] as String, - tags = map["tags"] as List, - labels = map["labels"] as List, - images = map["images"] as List, - supportUrl = map["supportUrl"] as String, - dataDeletionUrl = map["dataDeletionUrl"] as String, - redirectUris = map["redirectUris"] as List, - postLogoutRedirectUris = map["postLogoutRedirectUris"] as List, - enabled = map["enabled"] as Boolean, - type = map["type"] as String, - deviceFlow = map["deviceFlow"] as Boolean, - teamId = map["teamId"] as String, - userId = map["userId"] as String, - installationScopes = map["installationScopes"] as List, - installationRedirectUrl = map["installationRedirectUrl"] as String, - secrets = (map["secrets"] as List>).map { AppSecret.from(map = it) }, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + description = (if (map.containsKey("description")) map["description"] else "") as String, + clientUri = (if (map.containsKey("clientUri")) map["clientUri"] else "") as String, + logoUri = (if (map.containsKey("logoUri")) map["logoUri"] else "") as String, + privacyPolicyUrl = (if (map.containsKey("privacyPolicyUrl")) map["privacyPolicyUrl"] else "") as String, + termsUrl = (if (map.containsKey("termsUrl")) map["termsUrl"] else "") as String, + contacts = (if (map.containsKey("contacts")) map["contacts"] else listOf()) as List, + tagline = (if (map.containsKey("tagline")) map["tagline"] else "") as String, + tags = (if (map.containsKey("tags")) map["tags"] else listOf()) as List, + labels = (if (map.containsKey("labels")) map["labels"] else listOf()) as List, + images = (if (map.containsKey("images")) map["images"] else listOf()) as List, + supportUrl = (if (map.containsKey("supportUrl")) map["supportUrl"] else "") as String, + dataDeletionUrl = (if (map.containsKey("dataDeletionUrl")) map["dataDeletionUrl"] else "") as String, + redirectUris = (if (map.containsKey("redirectUris")) map["redirectUris"] else listOf()) as List, + postLogoutRedirectUris = (if (map.containsKey("postLogoutRedirectUris")) map["postLogoutRedirectUris"] else listOf()) as List, + enabled = (if (map.containsKey("enabled")) map["enabled"] else true) as Boolean, + type = (if (map.containsKey("type")) map["type"] else "confidential") as String, + deviceFlow = (if (map.containsKey("deviceFlow")) map["deviceFlow"] else false) as Boolean, + teamId = (if (map.containsKey("teamId")) map["teamId"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + installationScopes = (if (map.containsKey("installationScopes")) map["installationScopes"] else listOf()) as List, + installationRedirectUrl = (if (map.containsKey("installationRedirectUrl")) map["installationRedirectUrl"] else "") as String, + secrets = ((if (map.containsKey("secrets")) map["secrets"] else listOf()) as List>).map { AppSecret.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/AppInstallation.kt b/library/src/main/java/io/appwrite/models/AppInstallation.kt index 46bf9075..3c6d1f68 100644 --- a/library/src/main/java/io/appwrite/models/AppInstallation.kt +++ b/library/src/main/java/io/appwrite/models/AppInstallation.kt @@ -86,15 +86,15 @@ data class AppInstallation( fun from( map: Map, ) = AppInstallation( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - appId = map["appId"] as String, - teamId = map["teamId"] as String, - scopes = map["scopes"] as List, - authorizationDetails = map["authorizationDetails"] as List>, - createdById = map["createdById"] as String, - createdByName = map["createdByName"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + teamId = (if (map.containsKey("teamId")) map["teamId"] else "") as String, + scopes = (if (map.containsKey("scopes")) map["scopes"] else listOf()) as List, + authorizationDetails = (if (map.containsKey("authorizationDetails")) map["authorizationDetails"] else listOf()) as List>, + createdById = (if (map.containsKey("createdById")) map["createdById"] else "") as String, + createdByName = (if (map.containsKey("createdByName")) map["createdByName"] else "") as String, lastAccessedAt = map["lastAccessedAt"] as? String, ) } diff --git a/library/src/main/java/io/appwrite/models/AppInstallationList.kt b/library/src/main/java/io/appwrite/models/AppInstallationList.kt index f7434cf2..8006d98c 100644 --- a/library/src/main/java/io/appwrite/models/AppInstallationList.kt +++ b/library/src/main/java/io/appwrite/models/AppInstallationList.kt @@ -30,8 +30,8 @@ data class AppInstallationList( fun from( map: Map, ) = AppInstallationList( - total = (map["total"] as Number).toLong(), - installations = (map["installations"] as List>).map { AppInstallation.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + installations = ((if (map.containsKey("installations")) map["installations"] else listOf()) as List>).map { AppInstallation.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/AppKey.kt b/library/src/main/java/io/appwrite/models/AppKey.kt index f59e8d50..7348cd3f 100644 --- a/library/src/main/java/io/appwrite/models/AppKey.kt +++ b/library/src/main/java/io/appwrite/models/AppKey.kt @@ -79,14 +79,14 @@ data class AppKey( fun from( map: Map, ) = AppKey( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - appId = map["appId"] as String, - secret = map["secret"] as String, - hint = map["hint"] as String, - createdById = map["createdById"] as String, - createdByName = map["createdByName"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + hint = (if (map.containsKey("hint")) map["hint"] else "") as String, + createdById = (if (map.containsKey("createdById")) map["createdById"] else "") as String, + createdByName = (if (map.containsKey("createdByName")) map["createdByName"] else "") as String, lastAccessedAt = map["lastAccessedAt"] as? String, ) } diff --git a/library/src/main/java/io/appwrite/models/AppKeyList.kt b/library/src/main/java/io/appwrite/models/AppKeyList.kt index ea4b1cca..854af040 100644 --- a/library/src/main/java/io/appwrite/models/AppKeyList.kt +++ b/library/src/main/java/io/appwrite/models/AppKeyList.kt @@ -30,8 +30,8 @@ data class AppKeyList( fun from( map: Map, ) = AppKeyList( - total = (map["total"] as Number).toLong(), - keys = (map["keys"] as List>).map { AppKey.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + keys = ((if (map.containsKey("keys")) map["keys"] else listOf()) as List>).map { AppKey.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/AppScope.kt b/library/src/main/java/io/appwrite/models/AppScope.kt index 4cc7fe95..dbacafc7 100644 --- a/library/src/main/java/io/appwrite/models/AppScope.kt +++ b/library/src/main/java/io/appwrite/models/AppScope.kt @@ -51,11 +51,11 @@ data class AppScope( fun from( map: Map, ) = AppScope( - value = map["value"] as String, - description = map["description"] as String, - type = map["type"] as String, - category = map["category"] as String, - deprecated = map["deprecated"] as Boolean, + value = (if (map.containsKey("value")) map["value"] else "") as String, + description = (if (map.containsKey("description")) map["description"] else "") as String, + type = (if (map.containsKey("type")) map["type"] else "") as String, + category = (if (map.containsKey("category")) map["category"] else "") as String, + deprecated = (if (map.containsKey("deprecated")) map["deprecated"] else false) as Boolean, ) } } diff --git a/library/src/main/java/io/appwrite/models/AppScopeList.kt b/library/src/main/java/io/appwrite/models/AppScopeList.kt index cbcf91fb..76bc7369 100644 --- a/library/src/main/java/io/appwrite/models/AppScopeList.kt +++ b/library/src/main/java/io/appwrite/models/AppScopeList.kt @@ -30,8 +30,8 @@ data class AppScopeList( fun from( map: Map, ) = AppScopeList( - total = (map["total"] as Number).toLong(), - scopes = (map["scopes"] as List>).map { AppScope.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + scopes = ((if (map.containsKey("scopes")) map["scopes"] else listOf()) as List>).map { AppScope.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/AppSecret.kt b/library/src/main/java/io/appwrite/models/AppSecret.kt index 0349ffbd..b7d70922 100644 --- a/library/src/main/java/io/appwrite/models/AppSecret.kt +++ b/library/src/main/java/io/appwrite/models/AppSecret.kt @@ -79,14 +79,14 @@ data class AppSecret( fun from( map: Map, ) = AppSecret( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - appId = map["appId"] as String, - secret = map["secret"] as String, - hint = map["hint"] as String, - createdById = map["createdById"] as String, - createdByName = map["createdByName"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + hint = (if (map.containsKey("hint")) map["hint"] else "") as String, + createdById = (if (map.containsKey("createdById")) map["createdById"] else "") as String, + createdByName = (if (map.containsKey("createdByName")) map["createdByName"] else "") as String, lastAccessedAt = map["lastAccessedAt"] as? String, ) } diff --git a/library/src/main/java/io/appwrite/models/AppSecretList.kt b/library/src/main/java/io/appwrite/models/AppSecretList.kt index 04d2ebb7..1f2946c4 100644 --- a/library/src/main/java/io/appwrite/models/AppSecretList.kt +++ b/library/src/main/java/io/appwrite/models/AppSecretList.kt @@ -30,8 +30,8 @@ data class AppSecretList( fun from( map: Map, ) = AppSecretList( - total = (map["total"] as Number).toLong(), - secrets = (map["secrets"] as List>).map { AppSecret.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + secrets = ((if (map.containsKey("secrets")) map["secrets"] else listOf()) as List>).map { AppSecret.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/AppSecretPlaintext.kt b/library/src/main/java/io/appwrite/models/AppSecretPlaintext.kt index 098b7f19..7464598c 100644 --- a/library/src/main/java/io/appwrite/models/AppSecretPlaintext.kt +++ b/library/src/main/java/io/appwrite/models/AppSecretPlaintext.kt @@ -79,14 +79,14 @@ data class AppSecretPlaintext( fun from( map: Map, ) = AppSecretPlaintext( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - appId = map["appId"] as String, - secret = map["secret"] as String, - hint = map["hint"] as String, - createdById = map["createdById"] as String, - createdByName = map["createdByName"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + hint = (if (map.containsKey("hint")) map["hint"] else "") as String, + createdById = (if (map.containsKey("createdById")) map["createdById"] else "") as String, + createdByName = (if (map.containsKey("createdByName")) map["createdByName"] else "") as String, lastAccessedAt = map["lastAccessedAt"] as? String, ) } diff --git a/library/src/main/java/io/appwrite/models/AppsList.kt b/library/src/main/java/io/appwrite/models/AppsList.kt index 5dcff821..0ad546f1 100644 --- a/library/src/main/java/io/appwrite/models/AppsList.kt +++ b/library/src/main/java/io/appwrite/models/AppsList.kt @@ -30,8 +30,8 @@ data class AppsList( fun from( map: Map, ) = AppsList( - total = (map["total"] as Number).toLong(), - apps = (map["apps"] as List>).map { App.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + apps = ((if (map.containsKey("apps")) map["apps"] else listOf()) as List>).map { App.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Continent.kt b/library/src/main/java/io/appwrite/models/Continent.kt index 0adbb5fa..e8599a03 100644 --- a/library/src/main/java/io/appwrite/models/Continent.kt +++ b/library/src/main/java/io/appwrite/models/Continent.kt @@ -30,8 +30,8 @@ data class Continent( fun from( map: Map, ) = Continent( - name = map["name"] as String, - code = map["code"] as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + code = (if (map.containsKey("code")) map["code"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/ContinentList.kt b/library/src/main/java/io/appwrite/models/ContinentList.kt index 53f95f2a..0a1f4f34 100644 --- a/library/src/main/java/io/appwrite/models/ContinentList.kt +++ b/library/src/main/java/io/appwrite/models/ContinentList.kt @@ -30,8 +30,8 @@ data class ContinentList( fun from( map: Map, ) = ContinentList( - total = (map["total"] as Number).toLong(), - continents = (map["continents"] as List>).map { Continent.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + continents = ((if (map.containsKey("continents")) map["continents"] else listOf()) as List>).map { Continent.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Country.kt b/library/src/main/java/io/appwrite/models/Country.kt index 7b104f0b..1c3065ea 100644 --- a/library/src/main/java/io/appwrite/models/Country.kt +++ b/library/src/main/java/io/appwrite/models/Country.kt @@ -30,8 +30,8 @@ data class Country( fun from( map: Map, ) = Country( - name = map["name"] as String, - code = map["code"] as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + code = (if (map.containsKey("code")) map["code"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/CountryList.kt b/library/src/main/java/io/appwrite/models/CountryList.kt index 322cdaf8..629bae23 100644 --- a/library/src/main/java/io/appwrite/models/CountryList.kt +++ b/library/src/main/java/io/appwrite/models/CountryList.kt @@ -30,8 +30,8 @@ data class CountryList( fun from( map: Map, ) = CountryList( - total = (map["total"] as Number).toLong(), - countries = (map["countries"] as List>).map { Country.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + countries = ((if (map.containsKey("countries")) map["countries"] else listOf()) as List>).map { Country.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Currency.kt b/library/src/main/java/io/appwrite/models/Currency.kt index 95ff13ac..d78a90ca 100644 --- a/library/src/main/java/io/appwrite/models/Currency.kt +++ b/library/src/main/java/io/appwrite/models/Currency.kt @@ -65,13 +65,13 @@ data class Currency( fun from( map: Map, ) = Currency( - symbol = map["symbol"] as String, - name = map["name"] as String, - symbolNative = map["symbolNative"] as String, - decimalDigits = (map["decimalDigits"] as Number).toLong(), - rounding = (map["rounding"] as Number).toDouble(), - code = map["code"] as String, - namePlural = map["namePlural"] as String, + symbol = (if (map.containsKey("symbol")) map["symbol"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + symbolNative = (if (map.containsKey("symbolNative")) map["symbolNative"] else "") as String, + decimalDigits = ((if (map.containsKey("decimalDigits")) map["decimalDigits"] else 0) as Number).toLong(), + rounding = ((if (map.containsKey("rounding")) map["rounding"] else 0.0) as Number).toDouble(), + code = (if (map.containsKey("code")) map["code"] else "") as String, + namePlural = (if (map.containsKey("namePlural")) map["namePlural"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/CurrencyList.kt b/library/src/main/java/io/appwrite/models/CurrencyList.kt index 8082a2f1..5928c9a1 100644 --- a/library/src/main/java/io/appwrite/models/CurrencyList.kt +++ b/library/src/main/java/io/appwrite/models/CurrencyList.kt @@ -30,8 +30,8 @@ data class CurrencyList( fun from( map: Map, ) = CurrencyList( - total = (map["total"] as Number).toLong(), - currencies = (map["currencies"] as List>).map { Currency.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + currencies = ((if (map.containsKey("currencies")) map["currencies"] else listOf()) as List>).map { Currency.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Document.kt b/library/src/main/java/io/appwrite/models/Document.kt index 20e803e1..a19dd7fa 100644 --- a/library/src/main/java/io/appwrite/models/Document.kt +++ b/library/src/main/java/io/appwrite/models/Document.kt @@ -92,13 +92,13 @@ data class Document( map: Map, nestedType: Class ) = Document( - id = map["\$id"] as String, - sequence = map["\$sequence"] as String, - collectionId = map["\$collectionId"] as String, - databaseId = map["\$databaseId"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - permissions = map["\$permissions"] as List, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + sequence = (if (map.containsKey("\$sequence")) map["\$sequence"] else "") as String, + collectionId = (if (map.containsKey("\$collectionId")) map["\$collectionId"] else "") as String, + databaseId = (if (map.containsKey("\$databaseId")) map["\$databaseId"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + permissions = (if (map.containsKey("\$permissions")) map["\$permissions"] else listOf()) as List, data = map["data"]?.jsonCast(to = nestedType) ?: map.jsonCast(to = nestedType) ) } diff --git a/library/src/main/java/io/appwrite/models/DocumentList.kt b/library/src/main/java/io/appwrite/models/DocumentList.kt index d8741eab..59203167 100644 --- a/library/src/main/java/io/appwrite/models/DocumentList.kt +++ b/library/src/main/java/io/appwrite/models/DocumentList.kt @@ -39,8 +39,8 @@ data class DocumentList( map: Map, nestedType: Class ) = DocumentList( - total = (map["total"] as Number).toLong(), - documents = (map["documents"] as List>).map { Document.from(map = it, nestedType) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + documents = ((if (map.containsKey("documents")) map["documents"] else listOf()) as List>).map { Document.from(map = it, nestedType) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Execution.kt b/library/src/main/java/io/appwrite/models/Execution.kt index c698d9c6..c7cada63 100644 --- a/library/src/main/java/io/appwrite/models/Execution.kt +++ b/library/src/main/java/io/appwrite/models/Execution.kt @@ -152,24 +152,24 @@ data class Execution( fun from( map: Map, ) = Execution( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - permissions = map["\$permissions"] as List, - resourceId = map["resourceId"] as String, - resourceType = ExecutionResourceType.values().find { it.value == map["resourceType"] as String }!!, - deploymentId = map["deploymentId"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + permissions = (if (map.containsKey("\$permissions")) map["\$permissions"] else listOf()) as List, + resourceId = (if (map.containsKey("resourceId")) map["resourceId"] else "") as String, + resourceType = ExecutionResourceType.values().find { it.value == (if (map.containsKey("resourceType")) map["resourceType"] else "functions") as String }!!, + deploymentId = (if (map.containsKey("deploymentId")) map["deploymentId"] else "") as String, trigger = ExecutionTrigger.values().find { it.value == map["trigger"] as String }!!, status = ExecutionStatus.values().find { it.value == map["status"] as String }!!, - requestMethod = map["requestMethod"] as String, - requestPath = map["requestPath"] as String, - requestHeaders = (map["requestHeaders"] as List>).map { Headers.from(map = it) }, - responseStatusCode = (map["responseStatusCode"] as Number).toLong(), - responseBody = map["responseBody"] as String, - responseHeaders = (map["responseHeaders"] as List>).map { Headers.from(map = it) }, - logs = map["logs"] as String, - errors = map["errors"] as String, - duration = (map["duration"] as Number).toDouble(), + requestMethod = (if (map.containsKey("requestMethod")) map["requestMethod"] else "") as String, + requestPath = (if (map.containsKey("requestPath")) map["requestPath"] else "") as String, + requestHeaders = ((if (map.containsKey("requestHeaders")) map["requestHeaders"] else listOf()) as List>).map { Headers.from(map = it) }, + responseStatusCode = ((if (map.containsKey("responseStatusCode")) map["responseStatusCode"] else 0) as Number).toLong(), + responseBody = (if (map.containsKey("responseBody")) map["responseBody"] else "") as String, + responseHeaders = ((if (map.containsKey("responseHeaders")) map["responseHeaders"] else listOf()) as List>).map { Headers.from(map = it) }, + logs = (if (map.containsKey("logs")) map["logs"] else "") as String, + errors = (if (map.containsKey("errors")) map["errors"] else "") as String, + duration = ((if (map.containsKey("duration")) map["duration"] else 0.0) as Number).toDouble(), scheduledAt = map["scheduledAt"] as? String, ) } diff --git a/library/src/main/java/io/appwrite/models/ExecutionList.kt b/library/src/main/java/io/appwrite/models/ExecutionList.kt index 4eac1d89..0eba2816 100644 --- a/library/src/main/java/io/appwrite/models/ExecutionList.kt +++ b/library/src/main/java/io/appwrite/models/ExecutionList.kt @@ -30,8 +30,8 @@ data class ExecutionList( fun from( map: Map, ) = ExecutionList( - total = (map["total"] as Number).toLong(), - executions = (map["executions"] as List>).map { Execution.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + executions = ((if (map.containsKey("executions")) map["executions"] else listOf()) as List>).map { Execution.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/File.kt b/library/src/main/java/io/appwrite/models/File.kt index d15a55b1..049bbae7 100644 --- a/library/src/main/java/io/appwrite/models/File.kt +++ b/library/src/main/java/io/appwrite/models/File.kt @@ -128,22 +128,22 @@ data class File( fun from( map: Map, ) = File( - id = map["\$id"] as String, - bucketId = map["bucketId"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - permissions = map["\$permissions"] as List, - name = map["name"] as String, - folder = map["folder"] as String, - key = map["key"] as String, - signature = map["signature"] as String, - mimeType = map["mimeType"] as String, - sizeOriginal = (map["sizeOriginal"] as Number).toLong(), - sizeActual = (map["sizeActual"] as Number).toLong(), - chunksTotal = (map["chunksTotal"] as Number).toLong(), - chunksUploaded = (map["chunksUploaded"] as Number).toLong(), - encryption = map["encryption"] as Boolean, - compression = map["compression"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + bucketId = (if (map.containsKey("bucketId")) map["bucketId"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + permissions = (if (map.containsKey("\$permissions")) map["\$permissions"] else listOf()) as List, + name = (if (map.containsKey("name")) map["name"] else "") as String, + folder = (if (map.containsKey("folder")) map["folder"] else "") as String, + key = (if (map.containsKey("key")) map["key"] else "") as String, + signature = (if (map.containsKey("signature")) map["signature"] else "") as String, + mimeType = (if (map.containsKey("mimeType")) map["mimeType"] else "") as String, + sizeOriginal = ((if (map.containsKey("sizeOriginal")) map["sizeOriginal"] else 0) as Number).toLong(), + sizeActual = ((if (map.containsKey("sizeActual")) map["sizeActual"] else 0) as Number).toLong(), + chunksTotal = ((if (map.containsKey("chunksTotal")) map["chunksTotal"] else 0) as Number).toLong(), + chunksUploaded = ((if (map.containsKey("chunksUploaded")) map["chunksUploaded"] else 0) as Number).toLong(), + encryption = (if (map.containsKey("encryption")) map["encryption"] else false) as Boolean, + compression = (if (map.containsKey("compression")) map["compression"] else "none") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/FileList.kt b/library/src/main/java/io/appwrite/models/FileList.kt index 7a462dc7..1a84a460 100644 --- a/library/src/main/java/io/appwrite/models/FileList.kt +++ b/library/src/main/java/io/appwrite/models/FileList.kt @@ -30,8 +30,8 @@ data class FileList( fun from( map: Map, ) = FileList( - total = (map["total"] as Number).toLong(), - files = (map["files"] as List>).map { File.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + files = ((if (map.containsKey("files")) map["files"] else listOf()) as List>).map { File.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Headers.kt b/library/src/main/java/io/appwrite/models/Headers.kt index 4fece1e4..d2c27770 100644 --- a/library/src/main/java/io/appwrite/models/Headers.kt +++ b/library/src/main/java/io/appwrite/models/Headers.kt @@ -30,8 +30,8 @@ data class Headers( fun from( map: Map, ) = Headers( - name = map["name"] as String, - value = map["value"] as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + value = (if (map.containsKey("value")) map["value"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Identity.kt b/library/src/main/java/io/appwrite/models/Identity.kt index 011c32e4..0269400f 100644 --- a/library/src/main/java/io/appwrite/models/Identity.kt +++ b/library/src/main/java/io/appwrite/models/Identity.kt @@ -93,17 +93,17 @@ data class Identity( fun from( map: Map, ) = Identity( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - userId = map["userId"] as String, - provider = map["provider"] as String, - providerUid = map["providerUid"] as String, - providerEmail = map["providerEmail"] as String, - providerAccessToken = map["providerAccessToken"] as String, - providerAccessTokenExpiry = map["providerAccessTokenExpiry"] as String, - providerRefreshToken = map["providerRefreshToken"] as String, - providerIdToken = map["providerIdToken"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + provider = (if (map.containsKey("provider")) map["provider"] else "") as String, + providerUid = (if (map.containsKey("providerUid")) map["providerUid"] else "") as String, + providerEmail = (if (map.containsKey("providerEmail")) map["providerEmail"] else "") as String, + providerAccessToken = (if (map.containsKey("providerAccessToken")) map["providerAccessToken"] else "") as String, + providerAccessTokenExpiry = (if (map.containsKey("providerAccessTokenExpiry")) map["providerAccessTokenExpiry"] else "") as String, + providerRefreshToken = (if (map.containsKey("providerRefreshToken")) map["providerRefreshToken"] else "") as String, + providerIdToken = (if (map.containsKey("providerIdToken")) map["providerIdToken"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/IdentityList.kt b/library/src/main/java/io/appwrite/models/IdentityList.kt index b92f4e6a..31e5b0ea 100644 --- a/library/src/main/java/io/appwrite/models/IdentityList.kt +++ b/library/src/main/java/io/appwrite/models/IdentityList.kt @@ -30,8 +30,8 @@ data class IdentityList( fun from( map: Map, ) = IdentityList( - total = (map["total"] as Number).toLong(), - identities = (map["identities"] as List>).map { Identity.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + identities = ((if (map.containsKey("identities")) map["identities"] else listOf()) as List>).map { Identity.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Jwt.kt b/library/src/main/java/io/appwrite/models/Jwt.kt index c188a853..7b27865e 100644 --- a/library/src/main/java/io/appwrite/models/Jwt.kt +++ b/library/src/main/java/io/appwrite/models/Jwt.kt @@ -23,7 +23,7 @@ data class Jwt( fun from( map: Map, ) = Jwt( - jwt = map["jwt"] as String, + jwt = (if (map.containsKey("jwt")) map["jwt"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Language.kt b/library/src/main/java/io/appwrite/models/Language.kt index be1f30e8..01eb45bd 100644 --- a/library/src/main/java/io/appwrite/models/Language.kt +++ b/library/src/main/java/io/appwrite/models/Language.kt @@ -37,9 +37,9 @@ data class Language( fun from( map: Map, ) = Language( - name = map["name"] as String, - code = map["code"] as String, - nativeName = map["nativeName"] as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + code = (if (map.containsKey("code")) map["code"] else "") as String, + nativeName = (if (map.containsKey("nativeName")) map["nativeName"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/LanguageList.kt b/library/src/main/java/io/appwrite/models/LanguageList.kt index e1b0e6e7..86f6aed4 100644 --- a/library/src/main/java/io/appwrite/models/LanguageList.kt +++ b/library/src/main/java/io/appwrite/models/LanguageList.kt @@ -30,8 +30,8 @@ data class LanguageList( fun from( map: Map, ) = LanguageList( - total = (map["total"] as Number).toLong(), - languages = (map["languages"] as List>).map { Language.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + languages = ((if (map.containsKey("languages")) map["languages"] else listOf()) as List>).map { Language.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Locale.kt b/library/src/main/java/io/appwrite/models/Locale.kt index 689de89c..dc6a1ad8 100644 --- a/library/src/main/java/io/appwrite/models/Locale.kt +++ b/library/src/main/java/io/appwrite/models/Locale.kt @@ -142,13 +142,13 @@ data class Locale( fun from( map: Map, ) = Locale( - ip = map["ip"] as String, - countryCode = map["countryCode"] as String, - country = map["country"] as String, - continentCode = map["continentCode"] as String, - continent = map["continent"] as String, - eu = map["eu"] as Boolean, - currency = map["currency"] as String, + ip = (if (map.containsKey("ip")) map["ip"] else "") as String, + countryCode = (if (map.containsKey("countryCode")) map["countryCode"] else "") as String, + country = (if (map.containsKey("country")) map["country"] else "") as String, + continentCode = (if (map.containsKey("continentCode")) map["continentCode"] else "") as String, + continent = (if (map.containsKey("continent")) map["continent"] else "") as String, + eu = (if (map.containsKey("eu")) map["eu"] else false) as Boolean, + currency = (if (map.containsKey("currency")) map["currency"] else "") as String, city = map["city"] as? String, timeZone = map["timeZone"] as? String, postalCode = map["postalCode"] as? String, diff --git a/library/src/main/java/io/appwrite/models/LocaleCode.kt b/library/src/main/java/io/appwrite/models/LocaleCode.kt index 602d9ed9..3bee5288 100644 --- a/library/src/main/java/io/appwrite/models/LocaleCode.kt +++ b/library/src/main/java/io/appwrite/models/LocaleCode.kt @@ -30,8 +30,8 @@ data class LocaleCode( fun from( map: Map, ) = LocaleCode( - code = map["code"] as String, - name = map["name"] as String, + code = (if (map.containsKey("code")) map["code"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/LocaleCodeList.kt b/library/src/main/java/io/appwrite/models/LocaleCodeList.kt index e0c568a7..86cb808f 100644 --- a/library/src/main/java/io/appwrite/models/LocaleCodeList.kt +++ b/library/src/main/java/io/appwrite/models/LocaleCodeList.kt @@ -30,8 +30,8 @@ data class LocaleCodeList( fun from( map: Map, ) = LocaleCodeList( - total = (map["total"] as Number).toLong(), - localeCodes = (map["localeCodes"] as List>).map { LocaleCode.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + localeCodes = ((if (map.containsKey("localeCodes")) map["localeCodes"] else listOf()) as List>).map { LocaleCode.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Membership.kt b/library/src/main/java/io/appwrite/models/Membership.kt index 7d5b963b..3d0c37b9 100644 --- a/library/src/main/java/io/appwrite/models/Membership.kt +++ b/library/src/main/java/io/appwrite/models/Membership.kt @@ -121,21 +121,21 @@ data class Membership( fun from( map: Map, ) = Membership( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - userId = map["userId"] as String, - userName = map["userName"] as String, - userEmail = map["userEmail"] as String, - userPhone = map["userPhone"] as String, - teamId = map["teamId"] as String, - teamName = map["teamName"] as String, - invited = map["invited"] as String, - joined = map["joined"] as String, - confirm = map["confirm"] as Boolean, - mfa = map["mfa"] as Boolean, - userAccessedAt = map["userAccessedAt"] as String, - roles = map["roles"] as List, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + userName = (if (map.containsKey("userName")) map["userName"] else "") as String, + userEmail = (if (map.containsKey("userEmail")) map["userEmail"] else "") as String, + userPhone = (if (map.containsKey("userPhone")) map["userPhone"] else "") as String, + teamId = (if (map.containsKey("teamId")) map["teamId"] else "") as String, + teamName = (if (map.containsKey("teamName")) map["teamName"] else "") as String, + invited = (if (map.containsKey("invited")) map["invited"] else "") as String, + joined = (if (map.containsKey("joined")) map["joined"] else "") as String, + confirm = (if (map.containsKey("confirm")) map["confirm"] else false) as Boolean, + mfa = (if (map.containsKey("mfa")) map["mfa"] else false) as Boolean, + userAccessedAt = (if (map.containsKey("userAccessedAt")) map["userAccessedAt"] else "") as String, + roles = (if (map.containsKey("roles")) map["roles"] else listOf()) as List, ) } } diff --git a/library/src/main/java/io/appwrite/models/MembershipList.kt b/library/src/main/java/io/appwrite/models/MembershipList.kt index 8376bc96..f6f4afe7 100644 --- a/library/src/main/java/io/appwrite/models/MembershipList.kt +++ b/library/src/main/java/io/appwrite/models/MembershipList.kt @@ -30,8 +30,8 @@ data class MembershipList( fun from( map: Map, ) = MembershipList( - total = (map["total"] as Number).toLong(), - memberships = (map["memberships"] as List>).map { Membership.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + memberships = ((if (map.containsKey("memberships")) map["memberships"] else listOf()) as List>).map { Membership.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/MfaChallenge.kt b/library/src/main/java/io/appwrite/models/MfaChallenge.kt index b3e9f6bd..c17e792d 100644 --- a/library/src/main/java/io/appwrite/models/MfaChallenge.kt +++ b/library/src/main/java/io/appwrite/models/MfaChallenge.kt @@ -44,10 +44,10 @@ data class MfaChallenge( fun from( map: Map, ) = MfaChallenge( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - userId = map["userId"] as String, - expire = map["expire"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/MfaFactors.kt b/library/src/main/java/io/appwrite/models/MfaFactors.kt index 20f7bb36..42872fa2 100644 --- a/library/src/main/java/io/appwrite/models/MfaFactors.kt +++ b/library/src/main/java/io/appwrite/models/MfaFactors.kt @@ -51,11 +51,11 @@ data class MfaFactors( fun from( map: Map, ) = MfaFactors( - totp = map["totp"] as Boolean, - phone = map["phone"] as Boolean, - email = map["email"] as Boolean, - recoveryCode = map["recoveryCode"] as Boolean, - custom = map["custom"] as Boolean, + totp = (if (map.containsKey("totp")) map["totp"] else false) as Boolean, + phone = (if (map.containsKey("phone")) map["phone"] else false) as Boolean, + email = (if (map.containsKey("email")) map["email"] else false) as Boolean, + recoveryCode = (if (map.containsKey("recoveryCode")) map["recoveryCode"] else false) as Boolean, + custom = (if (map.containsKey("custom")) map["custom"] else false) as Boolean, ) } } diff --git a/library/src/main/java/io/appwrite/models/MfaRecoveryCodes.kt b/library/src/main/java/io/appwrite/models/MfaRecoveryCodes.kt index a8967496..25e783cd 100644 --- a/library/src/main/java/io/appwrite/models/MfaRecoveryCodes.kt +++ b/library/src/main/java/io/appwrite/models/MfaRecoveryCodes.kt @@ -23,7 +23,7 @@ data class MfaRecoveryCodes( fun from( map: Map, ) = MfaRecoveryCodes( - recoveryCodes = map["recoveryCodes"] as List, + recoveryCodes = (if (map.containsKey("recoveryCodes")) map["recoveryCodes"] else listOf()) as List, ) } } diff --git a/library/src/main/java/io/appwrite/models/MfaType.kt b/library/src/main/java/io/appwrite/models/MfaType.kt index e20b7f66..5b7b9710 100644 --- a/library/src/main/java/io/appwrite/models/MfaType.kt +++ b/library/src/main/java/io/appwrite/models/MfaType.kt @@ -30,8 +30,8 @@ data class MfaType( fun from( map: Map, ) = MfaType( - secret = map["secret"] as String, - uri = map["uri"] as String, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + uri = (if (map.containsKey("uri")) map["uri"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Approve.kt b/library/src/main/java/io/appwrite/models/Oauth2Approve.kt index e09ab9ea..0aabf4de 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Approve.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Approve.kt @@ -23,7 +23,7 @@ data class Oauth2Approve( fun from( map: Map, ) = Oauth2Approve( - redirectUrl = map["redirectUrl"] as String, + redirectUrl = (if (map.containsKey("redirectUrl")) map["redirectUrl"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Authorize.kt b/library/src/main/java/io/appwrite/models/Oauth2Authorize.kt index e0812f7e..fe878e3a 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Authorize.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Authorize.kt @@ -30,8 +30,8 @@ data class Oauth2Authorize( fun from( map: Map, ) = Oauth2Authorize( - grantId = map["grantId"] as String, - redirectUrl = map["redirectUrl"] as String, + grantId = (if (map.containsKey("grantId")) map["grantId"] else "") as String, + redirectUrl = (if (map.containsKey("redirectUrl")) map["redirectUrl"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Consent.kt b/library/src/main/java/io/appwrite/models/Oauth2Consent.kt index 7769e9b1..28cde915 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Consent.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Consent.kt @@ -86,16 +86,16 @@ data class Oauth2Consent( fun from( map: Map, ) = Oauth2Consent( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - userId = map["userId"] as String, - appId = map["appId"] as String, - cimdUrl = map["cimdUrl"] as String, - scopes = map["scopes"] as List, - resources = map["resources"] as List, - authorizationDetails = map["authorizationDetails"] as String, - expire = map["expire"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + cimdUrl = (if (map.containsKey("cimdUrl")) map["cimdUrl"] else "") as String, + scopes = (if (map.containsKey("scopes")) map["scopes"] else listOf()) as List, + resources = (if (map.containsKey("resources")) map["resources"] else listOf()) as List, + authorizationDetails = (if (map.containsKey("authorizationDetails")) map["authorizationDetails"] else "[]") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2ConsentList.kt b/library/src/main/java/io/appwrite/models/Oauth2ConsentList.kt index da6acd38..2a6337bd 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2ConsentList.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2ConsentList.kt @@ -30,8 +30,8 @@ data class Oauth2ConsentList( fun from( map: Map, ) = Oauth2ConsentList( - total = (map["total"] as Number).toLong(), - consents = (map["consents"] as List>).map { Oauth2Consent.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + consents = ((if (map.containsKey("consents")) map["consents"] else listOf()) as List>).map { Oauth2Consent.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2ConsentToken.kt b/library/src/main/java/io/appwrite/models/Oauth2ConsentToken.kt index 31f6249b..1f5c1dfc 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2ConsentToken.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2ConsentToken.kt @@ -93,17 +93,17 @@ data class Oauth2ConsentToken( fun from( map: Map, ) = Oauth2ConsentToken( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - consentId = map["consentId"] as String, - userId = map["userId"] as String, - appId = map["appId"] as String, - cimdUrl = map["cimdUrl"] as String, - scopes = map["scopes"] as List, - resources = map["resources"] as List, - authorizationDetails = map["authorizationDetails"] as String, - expire = map["expire"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + consentId = (if (map.containsKey("consentId")) map["consentId"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + cimdUrl = (if (map.containsKey("cimdUrl")) map["cimdUrl"] else "") as String, + scopes = (if (map.containsKey("scopes")) map["scopes"] else listOf()) as List, + resources = (if (map.containsKey("resources")) map["resources"] else listOf()) as List, + authorizationDetails = (if (map.containsKey("authorizationDetails")) map["authorizationDetails"] else "[]") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2ConsentTokenList.kt b/library/src/main/java/io/appwrite/models/Oauth2ConsentTokenList.kt index 09278f45..b5d4c524 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2ConsentTokenList.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2ConsentTokenList.kt @@ -30,8 +30,8 @@ data class Oauth2ConsentTokenList( fun from( map: Map, ) = Oauth2ConsentTokenList( - total = (map["total"] as Number).toLong(), - tokens = (map["tokens"] as List>).map { Oauth2ConsentToken.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + tokens = ((if (map.containsKey("tokens")) map["tokens"] else listOf()) as List>).map { Oauth2ConsentToken.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2DeviceAuthorization.kt b/library/src/main/java/io/appwrite/models/Oauth2DeviceAuthorization.kt index 58c7e5ee..f4ca6e64 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2DeviceAuthorization.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2DeviceAuthorization.kt @@ -58,12 +58,12 @@ data class Oauth2DeviceAuthorization( fun from( map: Map, ) = Oauth2DeviceAuthorization( - device_code = map["device_code"] as String, - user_code = map["user_code"] as String, - verification_uri = map["verification_uri"] as String, - verification_uri_complete = map["verification_uri_complete"] as String, - expires_in = (map["expires_in"] as Number).toLong(), - interval = (map["interval"] as Number).toLong(), + device_code = (if (map.containsKey("device_code")) map["device_code"] else "") as String, + user_code = (if (map.containsKey("user_code")) map["user_code"] else "") as String, + verification_uri = (if (map.containsKey("verification_uri")) map["verification_uri"] else "") as String, + verification_uri_complete = (if (map.containsKey("verification_uri_complete")) map["verification_uri_complete"] else "") as String, + expires_in = ((if (map.containsKey("expires_in")) map["expires_in"] else 0) as Number).toLong(), + interval = ((if (map.containsKey("interval")) map["interval"] else 0) as Number).toLong(), ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Grant.kt b/library/src/main/java/io/appwrite/models/Oauth2Grant.kt index ef1a7c39..914b6f33 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Grant.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Grant.kt @@ -100,18 +100,18 @@ data class Oauth2Grant( fun from( map: Map, ) = Oauth2Grant( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - userId = map["userId"] as String, - appId = map["appId"] as String, - scopes = map["scopes"] as List, - resources = map["resources"] as List, - authorizationDetails = map["authorizationDetails"] as String, - prompt = map["prompt"] as String, - redirectUri = map["redirectUri"] as String, - authTime = (map["authTime"] as Number).toLong(), - expire = map["expire"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + scopes = (if (map.containsKey("scopes")) map["scopes"] else listOf()) as List, + resources = (if (map.containsKey("resources")) map["resources"] else listOf()) as List, + authorizationDetails = (if (map.containsKey("authorizationDetails")) map["authorizationDetails"] else "[]") as String, + prompt = (if (map.containsKey("prompt")) map["prompt"] else "") as String, + redirectUri = (if (map.containsKey("redirectUri")) map["redirectUri"] else "") as String, + authTime = ((if (map.containsKey("authTime")) map["authTime"] else 0) as Number).toLong(), + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Organization.kt b/library/src/main/java/io/appwrite/models/Oauth2Organization.kt index 605916a0..47b6a8ff 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Organization.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Organization.kt @@ -23,7 +23,7 @@ data class Oauth2Organization( fun from( map: Map, ) = Oauth2Organization( - id = map["\$id"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2OrganizationList.kt b/library/src/main/java/io/appwrite/models/Oauth2OrganizationList.kt index b90ee636..48f45063 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2OrganizationList.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2OrganizationList.kt @@ -30,8 +30,8 @@ data class Oauth2OrganizationList( fun from( map: Map, ) = Oauth2OrganizationList( - total = (map["total"] as Number).toLong(), - organizations = (map["organizations"] as List>).map { Oauth2Organization.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + organizations = ((if (map.containsKey("organizations")) map["organizations"] else listOf()) as List>).map { Oauth2Organization.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2PAR.kt b/library/src/main/java/io/appwrite/models/Oauth2PAR.kt index 94d175ee..1091bdd5 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2PAR.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2PAR.kt @@ -30,8 +30,8 @@ data class Oauth2PAR( fun from( map: Map, ) = Oauth2PAR( - request_uri = map["request_uri"] as String, - expires_in = (map["expires_in"] as Number).toLong(), + request_uri = (if (map.containsKey("request_uri")) map["request_uri"] else "") as String, + expires_in = ((if (map.containsKey("expires_in")) map["expires_in"] else 0) as Number).toLong(), ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Project.kt b/library/src/main/java/io/appwrite/models/Oauth2Project.kt index eb085dce..95c79e0b 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Project.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Project.kt @@ -37,9 +37,9 @@ data class Oauth2Project( fun from( map: Map, ) = Oauth2Project( - id = map["\$id"] as String, - region = map["region"] as String, - endpoint = map["endpoint"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + region = (if (map.containsKey("region")) map["region"] else "") as String, + endpoint = (if (map.containsKey("endpoint")) map["endpoint"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2ProjectList.kt b/library/src/main/java/io/appwrite/models/Oauth2ProjectList.kt index 551b9ee4..5eb46ed5 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2ProjectList.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2ProjectList.kt @@ -30,8 +30,8 @@ data class Oauth2ProjectList( fun from( map: Map, ) = Oauth2ProjectList( - total = (map["total"] as Number).toLong(), - projects = (map["projects"] as List>).map { Oauth2Project.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + projects = ((if (map.containsKey("projects")) map["projects"] else listOf()) as List>).map { Oauth2Project.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Reject.kt b/library/src/main/java/io/appwrite/models/Oauth2Reject.kt index b23f308c..42b3a3a3 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Reject.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Reject.kt @@ -23,7 +23,7 @@ data class Oauth2Reject( fun from( map: Map, ) = Oauth2Reject( - redirectUrl = map["redirectUrl"] as String, + redirectUrl = (if (map.containsKey("redirectUrl")) map["redirectUrl"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Token.kt b/library/src/main/java/io/appwrite/models/Oauth2Token.kt index f36f7768..53547c23 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Token.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Token.kt @@ -65,11 +65,11 @@ data class Oauth2Token( fun from( map: Map, ) = Oauth2Token( - access_token = map["access_token"] as String, - token_type = map["token_type"] as String, - expires_in = (map["expires_in"] as Number).toLong(), - refresh_token = map["refresh_token"] as String, - scope = map["scope"] as String, + access_token = (if (map.containsKey("access_token")) map["access_token"] else "") as String, + token_type = (if (map.containsKey("token_type")) map["token_type"] else "Bearer") as String, + expires_in = ((if (map.containsKey("expires_in")) map["expires_in"] else 0) as Number).toLong(), + refresh_token = (if (map.containsKey("refresh_token")) map["refresh_token"] else "") as String, + scope = (if (map.containsKey("scope")) map["scope"] else "") as String, authorization_details = map["authorization_details"] as? String, id_token = map["id_token"] as? String, ) diff --git a/library/src/main/java/io/appwrite/models/Passkey.kt b/library/src/main/java/io/appwrite/models/Passkey.kt new file mode 100644 index 00000000..90f0e497 --- /dev/null +++ b/library/src/main/java/io/appwrite/models/Passkey.kt @@ -0,0 +1,61 @@ +package io.appwrite.models + +import com.google.gson.annotations.SerializedName +import io.appwrite.extensions.jsonCast + +/** + * Passkey + */ +data class Passkey( + /** + * Passkey ID. + */ + @SerializedName("\$id") + val id: String, + + /** + * Passkey creation date in ISO 8601 format. + */ + @SerializedName("\$createdAt") + val createdAt: String, + + /** + * Passkey update date in ISO 8601 format. + */ + @SerializedName("\$updatedAt") + val updatedAt: String, + + /** + * Passkey name. + */ + @SerializedName("name") + val name: String, + + /** + * Most recent sign-in with this passkey in ISO 8601 format. Empty until the passkey is used to sign in. + */ + @SerializedName("accessedAt") + val accessedAt: String, + +) { + fun toMap(): Map = mapOf( + "\$id" to id as Any, + "\$createdAt" to createdAt as Any, + "\$updatedAt" to updatedAt as Any, + "name" to name as Any, + "accessedAt" to accessedAt as Any, + ) + + companion object { + @Suppress("UNCHECKED_CAST") + fun from( + map: Map, + ) = Passkey( + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + accessedAt = (if (map.containsKey("accessedAt")) map["accessedAt"] else "") as String, + ) + } +} diff --git a/library/src/main/java/io/appwrite/models/PasskeyChallenge.kt b/library/src/main/java/io/appwrite/models/PasskeyChallenge.kt new file mode 100644 index 00000000..dcdcb771 --- /dev/null +++ b/library/src/main/java/io/appwrite/models/PasskeyChallenge.kt @@ -0,0 +1,61 @@ +package io.appwrite.models + +import com.google.gson.annotations.SerializedName +import io.appwrite.extensions.jsonCast + +/** + * Passkey Challenge + */ +data class PasskeyChallenge( + /** + * Challenge ID. When signing in, pass it back with the credential to receive a token. + */ + @SerializedName("\$id") + val id: String, + + /** + * Challenge creation date in ISO 8601 format. + */ + @SerializedName("\$createdAt") + val createdAt: String, + + /** + * ID of the passkey being registered. Empty for sign-in challenges. + */ + @SerializedName("passkeyId") + val passkeyId: String, + + /** + * Challenge expiration date in ISO 8601 format. + */ + @SerializedName("expire") + val expire: String, + + /** + * WebAuthn options in JSON form. Pass them to `PublicKeyCredential.parseCreationOptionsFromJSON()` when registering, or `PublicKeyCredential.parseRequestOptionsFromJSON()` when signing in. + */ + @SerializedName("publicKey") + val publicKey: Map, + +) { + fun toMap(): Map = mapOf( + "\$id" to id as Any, + "\$createdAt" to createdAt as Any, + "passkeyId" to passkeyId as Any, + "expire" to expire as Any, + "publicKey" to publicKey as Any, + ) + + companion object { + @Suppress("UNCHECKED_CAST") + fun from( + map: Map, + ) = PasskeyChallenge( + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + passkeyId = (if (map.containsKey("passkeyId")) map["passkeyId"] else "") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, + publicKey = (if (map.containsKey("publicKey")) map["publicKey"] else mapOf()) as Map, + ) + } +} diff --git a/library/src/main/java/io/appwrite/models/PasskeyList.kt b/library/src/main/java/io/appwrite/models/PasskeyList.kt new file mode 100644 index 00000000..05692293 --- /dev/null +++ b/library/src/main/java/io/appwrite/models/PasskeyList.kt @@ -0,0 +1,37 @@ +package io.appwrite.models + +import com.google.gson.annotations.SerializedName +import io.appwrite.extensions.jsonCast + +/** + * Passkeys List + */ +data class PasskeyList( + /** + * Total number of passkeys that matched your query. + */ + @SerializedName("total") + val total: Long, + + /** + * List of passkeys. + */ + @SerializedName("passkeys") + val passkeys: List, + +) { + fun toMap(): Map = mapOf( + "total" to total as Any, + "passkeys" to passkeys.map { it.toMap() } as Any, + ) + + companion object { + @Suppress("UNCHECKED_CAST") + fun from( + map: Map, + ) = PasskeyList( + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + passkeys = ((if (map.containsKey("passkeys")) map["passkeys"] else listOf()) as List>).map { Passkey.from(map = it) }, + ) + } +} diff --git a/library/src/main/java/io/appwrite/models/Phone.kt b/library/src/main/java/io/appwrite/models/Phone.kt index 74bfa518..12c8eb63 100644 --- a/library/src/main/java/io/appwrite/models/Phone.kt +++ b/library/src/main/java/io/appwrite/models/Phone.kt @@ -37,9 +37,9 @@ data class Phone( fun from( map: Map, ) = Phone( - code = map["code"] as String, - countryCode = map["countryCode"] as String, - countryName = map["countryName"] as String, + code = (if (map.containsKey("code")) map["code"] else "") as String, + countryCode = (if (map.containsKey("countryCode")) map["countryCode"] else "") as String, + countryName = (if (map.containsKey("countryName")) map["countryName"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/PhoneList.kt b/library/src/main/java/io/appwrite/models/PhoneList.kt index fc0ffa3c..2c3f9cd1 100644 --- a/library/src/main/java/io/appwrite/models/PhoneList.kt +++ b/library/src/main/java/io/appwrite/models/PhoneList.kt @@ -30,8 +30,8 @@ data class PhoneList( fun from( map: Map, ) = PhoneList( - total = (map["total"] as Number).toLong(), - phones = (map["phones"] as List>).map { Phone.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + phones = ((if (map.containsKey("phones")) map["phones"] else listOf()) as List>).map { Phone.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Presence.kt b/library/src/main/java/io/appwrite/models/Presence.kt index c7f60731..b8e27fc0 100644 --- a/library/src/main/java/io/appwrite/models/Presence.kt +++ b/library/src/main/java/io/appwrite/models/Presence.kt @@ -79,13 +79,13 @@ data class Presence( fun from( map: Map, ) = Presence( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - permissions = map["\$permissions"] as List, - userId = map["userId"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + permissions = (if (map.containsKey("\$permissions")) map["\$permissions"] else listOf()) as List, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, status = map["status"] as? String, - source = map["source"] as String, + source = (if (map.containsKey("source")) map["source"] else "") as String, expiresAt = map["expiresAt"] as? String, metadata = map["metadata"] as? Map, ) diff --git a/library/src/main/java/io/appwrite/models/PresenceList.kt b/library/src/main/java/io/appwrite/models/PresenceList.kt index b69d3e13..01a20264 100644 --- a/library/src/main/java/io/appwrite/models/PresenceList.kt +++ b/library/src/main/java/io/appwrite/models/PresenceList.kt @@ -30,8 +30,8 @@ data class PresenceList( fun from( map: Map, ) = PresenceList( - total = (map["total"] as Number).toLong(), - presences = (map["presences"] as List>).map { Presence.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + presences = ((if (map.containsKey("presences")) map["presences"] else listOf()) as List>).map { Presence.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Row.kt b/library/src/main/java/io/appwrite/models/Row.kt index 1f190104..d0968469 100644 --- a/library/src/main/java/io/appwrite/models/Row.kt +++ b/library/src/main/java/io/appwrite/models/Row.kt @@ -92,13 +92,13 @@ data class Row( map: Map, nestedType: Class ) = Row( - id = map["\$id"] as String, - sequence = map["\$sequence"] as String, - tableId = map["\$tableId"] as String, - databaseId = map["\$databaseId"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - permissions = map["\$permissions"] as List, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + sequence = (if (map.containsKey("\$sequence")) map["\$sequence"] else "") as String, + tableId = (if (map.containsKey("\$tableId")) map["\$tableId"] else "") as String, + databaseId = (if (map.containsKey("\$databaseId")) map["\$databaseId"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + permissions = (if (map.containsKey("\$permissions")) map["\$permissions"] else listOf()) as List, data = map["data"]?.jsonCast(to = nestedType) ?: map.jsonCast(to = nestedType) ) } diff --git a/library/src/main/java/io/appwrite/models/RowList.kt b/library/src/main/java/io/appwrite/models/RowList.kt index f9c8f61e..0d52602c 100644 --- a/library/src/main/java/io/appwrite/models/RowList.kt +++ b/library/src/main/java/io/appwrite/models/RowList.kt @@ -39,8 +39,8 @@ data class RowList( map: Map, nestedType: Class ) = RowList( - total = (map["total"] as Number).toLong(), - rows = (map["rows"] as List>).map { Row.from(map = it, nestedType) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + rows = ((if (map.containsKey("rows")) map["rows"] else listOf()) as List>).map { Row.from(map = it, nestedType) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Session.kt b/library/src/main/java/io/appwrite/models/Session.kt index c9c39509..884d5814 100644 --- a/library/src/main/java/io/appwrite/models/Session.kt +++ b/library/src/main/java/io/appwrite/models/Session.kt @@ -219,35 +219,35 @@ data class Session( fun from( map: Map, ) = Session( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - userId = map["userId"] as String, - expire = map["expire"] as String, - provider = map["provider"] as String, - providerUid = map["providerUid"] as String, - providerAccessToken = map["providerAccessToken"] as String, - providerAccessTokenExpiry = map["providerAccessTokenExpiry"] as String, - providerRefreshToken = map["providerRefreshToken"] as String, - ip = map["ip"] as String, - osCode = map["osCode"] as String, - osName = map["osName"] as String, - osVersion = map["osVersion"] as String, - clientType = map["clientType"] as String, - clientCode = map["clientCode"] as String, - clientName = map["clientName"] as String, - clientVersion = map["clientVersion"] as String, - clientEngine = map["clientEngine"] as String, - clientEngineVersion = map["clientEngineVersion"] as String, - deviceName = map["deviceName"] as String, - deviceBrand = map["deviceBrand"] as String, - deviceModel = map["deviceModel"] as String, - countryCode = map["countryCode"] as String, - countryName = map["countryName"] as String, - current = map["current"] as Boolean, - factors = map["factors"] as List, - secret = map["secret"] as String, - mfaUpdatedAt = map["mfaUpdatedAt"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, + provider = (if (map.containsKey("provider")) map["provider"] else "") as String, + providerUid = (if (map.containsKey("providerUid")) map["providerUid"] else "") as String, + providerAccessToken = (if (map.containsKey("providerAccessToken")) map["providerAccessToken"] else "") as String, + providerAccessTokenExpiry = (if (map.containsKey("providerAccessTokenExpiry")) map["providerAccessTokenExpiry"] else "") as String, + providerRefreshToken = (if (map.containsKey("providerRefreshToken")) map["providerRefreshToken"] else "") as String, + ip = (if (map.containsKey("ip")) map["ip"] else "") as String, + osCode = (if (map.containsKey("osCode")) map["osCode"] else "") as String, + osName = (if (map.containsKey("osName")) map["osName"] else "") as String, + osVersion = (if (map.containsKey("osVersion")) map["osVersion"] else "") as String, + clientType = (if (map.containsKey("clientType")) map["clientType"] else "") as String, + clientCode = (if (map.containsKey("clientCode")) map["clientCode"] else "") as String, + clientName = (if (map.containsKey("clientName")) map["clientName"] else "") as String, + clientVersion = (if (map.containsKey("clientVersion")) map["clientVersion"] else "") as String, + clientEngine = (if (map.containsKey("clientEngine")) map["clientEngine"] else "") as String, + clientEngineVersion = (if (map.containsKey("clientEngineVersion")) map["clientEngineVersion"] else "") as String, + deviceName = (if (map.containsKey("deviceName")) map["deviceName"] else "") as String, + deviceBrand = (if (map.containsKey("deviceBrand")) map["deviceBrand"] else "") as String, + deviceModel = (if (map.containsKey("deviceModel")) map["deviceModel"] else "") as String, + countryCode = (if (map.containsKey("countryCode")) map["countryCode"] else "") as String, + countryName = (if (map.containsKey("countryName")) map["countryName"] else "") as String, + current = (if (map.containsKey("current")) map["current"] else false) as Boolean, + factors = (if (map.containsKey("factors")) map["factors"] else listOf()) as List, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + mfaUpdatedAt = (if (map.containsKey("mfaUpdatedAt")) map["mfaUpdatedAt"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/SessionList.kt b/library/src/main/java/io/appwrite/models/SessionList.kt index bda39fca..9be215ba 100644 --- a/library/src/main/java/io/appwrite/models/SessionList.kt +++ b/library/src/main/java/io/appwrite/models/SessionList.kt @@ -30,8 +30,8 @@ data class SessionList( fun from( map: Map, ) = SessionList( - total = (map["total"] as Number).toLong(), - sessions = (map["sessions"] as List>).map { Session.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + sessions = ((if (map.containsKey("sessions")) map["sessions"] else listOf()) as List>).map { Session.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Subscriber.kt b/library/src/main/java/io/appwrite/models/Subscriber.kt index 931b65db..adcd8c53 100644 --- a/library/src/main/java/io/appwrite/models/Subscriber.kt +++ b/library/src/main/java/io/appwrite/models/Subscriber.kt @@ -79,15 +79,15 @@ data class Subscriber( fun from( map: Map, ) = Subscriber( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - targetId = map["targetId"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + targetId = (if (map.containsKey("targetId")) map["targetId"] else "") as String, target = Target.from(map = map["target"] as Map), - userId = map["userId"] as String, - userName = map["userName"] as String, - topicId = map["topicId"] as String, - providerType = map["providerType"] as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + userName = (if (map.containsKey("userName")) map["userName"] else "") as String, + topicId = (if (map.containsKey("topicId")) map["topicId"] else "") as String, + providerType = (if (map.containsKey("providerType")) map["providerType"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Target.kt b/library/src/main/java/io/appwrite/models/Target.kt index 3e89e08d..d67a716c 100644 --- a/library/src/main/java/io/appwrite/models/Target.kt +++ b/library/src/main/java/io/appwrite/models/Target.kt @@ -79,15 +79,15 @@ data class Target( fun from( map: Map, ) = Target( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - name = map["name"] as String, - userId = map["userId"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, providerId = map["providerId"] as? String, - providerType = map["providerType"] as String, - identifier = map["identifier"] as String, - expired = map["expired"] as Boolean, + providerType = (if (map.containsKey("providerType")) map["providerType"] else "") as String, + identifier = (if (map.containsKey("identifier")) map["identifier"] else "") as String, + expired = (if (map.containsKey("expired")) map["expired"] else false) as Boolean, ) } } diff --git a/library/src/main/java/io/appwrite/models/Team.kt b/library/src/main/java/io/appwrite/models/Team.kt index 6cd7dbc9..dc780c8b 100644 --- a/library/src/main/java/io/appwrite/models/Team.kt +++ b/library/src/main/java/io/appwrite/models/Team.kt @@ -75,11 +75,11 @@ data class Team( map: Map, nestedType: Class ) = Team( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - name = map["name"] as String, - total = (map["total"] as Number).toLong(), + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), prefs = Preferences.from(map = map["prefs"] as Map, nestedType), ) } diff --git a/library/src/main/java/io/appwrite/models/TeamList.kt b/library/src/main/java/io/appwrite/models/TeamList.kt index 8e490f51..db4e1eb2 100644 --- a/library/src/main/java/io/appwrite/models/TeamList.kt +++ b/library/src/main/java/io/appwrite/models/TeamList.kt @@ -39,8 +39,8 @@ data class TeamList( map: Map, nestedType: Class ) = TeamList( - total = (map["total"] as Number).toLong(), - teams = (map["teams"] as List>).map { Team.from(map = it, nestedType) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + teams = ((if (map.containsKey("teams")) map["teams"] else listOf()) as List>).map { Team.from(map = it, nestedType) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Token.kt b/library/src/main/java/io/appwrite/models/Token.kt index 625e3085..c3762397 100644 --- a/library/src/main/java/io/appwrite/models/Token.kt +++ b/library/src/main/java/io/appwrite/models/Token.kt @@ -58,12 +58,12 @@ data class Token( fun from( map: Map, ) = Token( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - userId = map["userId"] as String, - secret = map["secret"] as String, - expire = map["expire"] as String, - phrase = map["phrase"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, + phrase = (if (map.containsKey("phrase")) map["phrase"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/TokenSecret.kt b/library/src/main/java/io/appwrite/models/TokenSecret.kt new file mode 100644 index 00000000..fe2dd92c --- /dev/null +++ b/library/src/main/java/io/appwrite/models/TokenSecret.kt @@ -0,0 +1,69 @@ +package io.appwrite.models + +import com.google.gson.annotations.SerializedName +import io.appwrite.extensions.jsonCast + +/** + * Token Secret + */ +data class TokenSecret( + /** + * Token ID. + */ + @SerializedName("\$id") + val id: String, + + /** + * Token creation date in ISO 8601 format. + */ + @SerializedName("\$createdAt") + val createdAt: String, + + /** + * User ID. + */ + @SerializedName("userId") + val userId: String, + + /** + * Token secret key. Exchange it for a session with the create session endpoint. + */ + @SerializedName("secret") + val secret: String, + + /** + * Token expiration date in ISO 8601 format. + */ + @SerializedName("expire") + val expire: String, + + /** + * Security phrase of a token. Empty if security phrase was not requested when creating a token. It includes randomly generated phrase which is also sent in the external resource such as email. + */ + @SerializedName("phrase") + val phrase: String, + +) { + fun toMap(): Map = mapOf( + "\$id" to id as Any, + "\$createdAt" to createdAt as Any, + "userId" to userId as Any, + "secret" to secret as Any, + "expire" to expire as Any, + "phrase" to phrase as Any, + ) + + companion object { + @Suppress("UNCHECKED_CAST") + fun from( + map: Map, + ) = TokenSecret( + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, + phrase = (if (map.containsKey("phrase")) map["phrase"] else "") as String, + ) + } +} diff --git a/library/src/main/java/io/appwrite/models/Transaction.kt b/library/src/main/java/io/appwrite/models/Transaction.kt index 8f09860e..bb5f39b3 100644 --- a/library/src/main/java/io/appwrite/models/Transaction.kt +++ b/library/src/main/java/io/appwrite/models/Transaction.kt @@ -58,12 +58,12 @@ data class Transaction( fun from( map: Map, ) = Transaction( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - status = map["status"] as String, - operations = (map["operations"] as Number).toLong(), - expiresAt = map["expiresAt"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + status = (if (map.containsKey("status")) map["status"] else "pending") as String, + operations = ((if (map.containsKey("operations")) map["operations"] else 0) as Number).toLong(), + expiresAt = (if (map.containsKey("expiresAt")) map["expiresAt"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/TransactionList.kt b/library/src/main/java/io/appwrite/models/TransactionList.kt index c55a7317..963f3732 100644 --- a/library/src/main/java/io/appwrite/models/TransactionList.kt +++ b/library/src/main/java/io/appwrite/models/TransactionList.kt @@ -30,8 +30,8 @@ data class TransactionList( fun from( map: Map, ) = TransactionList( - total = (map["total"] as Number).toLong(), - transactions = (map["transactions"] as List>).map { Transaction.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + transactions = ((if (map.containsKey("transactions")) map["transactions"] else listOf()) as List>).map { Transaction.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/User.kt b/library/src/main/java/io/appwrite/models/User.kt index 0eea3b00..22b4aeb5 100644 --- a/library/src/main/java/io/appwrite/models/User.kt +++ b/library/src/main/java/io/appwrite/models/User.kt @@ -264,31 +264,31 @@ data class User( map: Map, nestedType: Class ) = User( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - name = map["name"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, password = map["password"] as? String, hash = map["hash"] as? String, hashOptions = map["hashOptions"] as? Map, - registration = map["registration"] as String, - status = map["status"] as Boolean, - labels = map["labels"] as List, - passwordUpdate = map["passwordUpdate"] as String, - email = map["email"] as String, - phone = map["phone"] as String, - emailVerification = map["emailVerification"] as Boolean, + registration = (if (map.containsKey("registration")) map["registration"] else "") as String, + status = (if (map.containsKey("status")) map["status"] else true) as Boolean, + labels = (if (map.containsKey("labels")) map["labels"] else listOf()) as List, + passwordUpdate = (if (map.containsKey("passwordUpdate")) map["passwordUpdate"] else "") as String, + email = (if (map.containsKey("email")) map["email"] else "") as String, + phone = (if (map.containsKey("phone")) map["phone"] else "") as String, + emailVerification = (if (map.containsKey("emailVerification")) map["emailVerification"] else false) as Boolean, emailCanonical = map["emailCanonical"] as? String, emailIsFree = map["emailIsFree"] as? Boolean, emailIsDisposable = map["emailIsDisposable"] as? Boolean, emailIsCorporate = map["emailIsCorporate"] as? Boolean, emailIsCanonical = map["emailIsCanonical"] as? Boolean, passwordPwned = map["passwordPwned"] as? Boolean, - phoneVerification = map["phoneVerification"] as Boolean, - mfa = map["mfa"] as Boolean, + phoneVerification = (if (map.containsKey("phoneVerification")) map["phoneVerification"] else false) as Boolean, + mfa = (if (map.containsKey("mfa")) map["mfa"] else false) as Boolean, prefs = Preferences.from(map = map["prefs"] as Map, nestedType), - targets = (map["targets"] as List>).map { Target.from(map = it) }, - accessedAt = map["accessedAt"] as String, + targets = ((if (map.containsKey("targets")) map["targets"] else listOf()) as List>).map { Target.from(map = it) }, + accessedAt = (if (map.containsKey("accessedAt")) map["accessedAt"] else "") as String, impersonator = map["impersonator"] as? Boolean, impersonatorUserId = map["impersonatorUserId"] as? String, ) diff --git a/library/src/main/java/io/appwrite/services/Account.kt b/library/src/main/java/io/appwrite/services/Account.kt index 8df38221..e6201c46 100644 --- a/library/src/main/java/io/appwrite/services/Account.kt +++ b/library/src/main/java/io/appwrite/services/Account.kt @@ -1200,6 +1200,219 @@ class Account(client: Client) : Service(client) { nestedType = classOf(), ) + /** + * Get the list of verified passkeys registered by the currently logged in user. + * + * @param queries Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter and order on the following attributes: $id, $createdAt, $updatedAt, name, accessedAt + * @param total When set to false, the total count returned will be 0 and will not be calculated. + * @return [io.appwrite.models.PasskeyList] + */ + @JvmOverloads + suspend fun listPasskeys( + queries: List? = null, + total: Boolean? = null, + ): io.appwrite.models.PasskeyList { + val apiPath = "/account/passkeys" + val apiParams = mutableMapOf( + "queries" to queries, + "total" to total, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.PasskeyList = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.PasskeyList.from(map = it as Map) + } + return client.call( + "GET", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.PasskeyList::class.java, + converter, + ) + } + + /** + * Start registering a passkey for the currently logged in user. The session must have signed in or completed an MFA challenge within the last 10 minutes. Pass the returned `publicKey` options to `navigator.credentials.create()`, then complete the registration with [Update passkey verification](/docs/references/cloud/client-web/account#updatePasskeyVerification). The passkey stays pending until verified, and the challenge expires after 5 minutes. + * + * @param passkeyId Passkey ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars. + * @param name Passkey name, shown when listing passkeys. Max length: 128 chars. + * @return [io.appwrite.models.PasskeyChallenge] + */ + @JvmOverloads + suspend fun createPasskey( + passkeyId: String? = null, + name: String? = null, + ): io.appwrite.models.PasskeyChallenge { + val apiPath = "/account/passkeys" + val apiParams = mutableMapOf( + "passkeyId" to passkeyId, + "name" to name, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.PasskeyChallenge = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.PasskeyChallenge.from(map = it as Map) + } + return client.call( + "POST", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.PasskeyChallenge::class.java, + converter, + ) + } + + /** + * Get a verified passkey of the currently logged in user by its unique ID. + * + * @param passkeyId Passkey ID. + * @return [io.appwrite.models.Passkey] + */ + suspend fun getPasskey( + passkeyId: String, + ): io.appwrite.models.Passkey { + if (passkeyId.isEmpty()) { + throw AppwriteException("Missing required parameter: \"passkeyId\"") + } + val apiPath = ("/account/passkeys/{passkeyId}" + .replace("{passkeyId}", passkeyId) + ) + val apiParams = mutableMapOf() + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.Passkey = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.Passkey.from(map = it as Map) + } + return client.call( + "GET", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.Passkey::class.java, + converter, + ) + } + + /** + * Rename a passkey of the currently logged in user. + * + * @param passkeyId Passkey ID. + * @param name Passkey name. Max length: 128 chars. + * @return [io.appwrite.models.Passkey] + */ + suspend fun updatePasskey( + passkeyId: String, + name: String, + ): io.appwrite.models.Passkey { + if (passkeyId.isEmpty()) { + throw AppwriteException("Missing required parameter: \"passkeyId\"") + } + val apiPath = ("/account/passkeys/{passkeyId}" + .replace("{passkeyId}", passkeyId) + ) + val apiParams = mutableMapOf( + "name" to name, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.Passkey = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.Passkey.from(map = it as Map) + } + return client.call( + "PATCH", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.Passkey::class.java, + converter, + ) + } + + /** + * Delete a passkey from the currently logged in user. The passkey can no longer be used to sign in, although it may remain stored on the user's device. The session must have signed in or completed an MFA challenge within the last 10 minutes. + * + * @param passkeyId Passkey ID. + * @return [Any] + */ + suspend fun deletePasskey( + passkeyId: String, + ): Any { + if (passkeyId.isEmpty()) { + throw AppwriteException("Missing required parameter: \"passkeyId\"") + } + val apiPath = ("/account/passkeys/{passkeyId}" + .replace("{passkeyId}", passkeyId) + ) + val apiParams = mutableMapOf() + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + return client.call( + "DELETE", + apiPath, + apiHeaders, + apiParams, + responseType = Any::class.java, + ) + } + + /** + * Complete a passkey registration started with [Create passkey](/docs/references/cloud/client-web/account#createPasskey). Pass the JSON form of the credential returned by `navigator.credentials.create()`, for example `credential.toJSON()`. Each registration can be verified once; a failed attempt requires starting it again. + * + * @param passkeyId Passkey ID. + * @param credential Registration credential returned by the authenticator, in the JSON form produced by `PublicKeyCredential.toJSON()`. + * @return [io.appwrite.models.Passkey] + */ + suspend fun updatePasskeyVerification( + passkeyId: String, + credential: Map, + ): io.appwrite.models.Passkey { + if (passkeyId.isEmpty()) { + throw AppwriteException("Missing required parameter: \"passkeyId\"") + } + val apiPath = ("/account/passkeys/{passkeyId}/verification" + .replace("{passkeyId}", passkeyId) + ) + val apiParams = mutableMapOf( + "credential" to credential, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.Passkey = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.Passkey.from(map = it as Map) + } + return client.call( + "PUT", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.Passkey::class.java, + converter, + ) + } + /** * Update currently logged in user password. For validation, user is required to pass in the new password, and the old password. For users created with OAuth, Team Invites and Magic URL, oldPassword is optional. * @@ -1784,6 +1997,7 @@ class Account(client: Client) : Service(client) { * @param success URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param failure URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param scopes A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of 100 scopes are allowed, each 4096 characters long. + * @param state An opaque value your app generates and keeps, for example in a cookie. It is returned unchanged as the `state` query parameter on the success and failure URLs, so your app can check that the sign-in it receives is one it started. Printable ASCII only (RFC 6749 Appendix A.5). Max length: 256 chars. */ @JvmOverloads suspend fun createOAuth2Session( @@ -1792,6 +2006,7 @@ class Account(client: Client) : Service(client) { success: String? = null, failure: String? = null, scopes: List? = null, + state: String? = null, ) { val apiPath = ("/account/sessions/oauth2/{provider}" .replace("{provider}", provider.value) @@ -1800,6 +2015,7 @@ class Account(client: Client) : Service(client) { "success" to success, "failure" to failure, "scopes" to scopes, + "state" to state, "project" to client.config["project"], ) val apiQuery = mutableListOf() @@ -1810,11 +2026,11 @@ class Account(client: Client) : Service(client) { } is List<*> -> { (it.value as List<*>).forEach { v -> - apiQuery.add("${it.key}[]=${v.toString()}") + apiQuery.add("${Uri.encode(it.key)}[]=${Uri.encode(v.toString())}") } } else -> { - apiQuery.add("${it.key}=${it.value.toString()}") + apiQuery.add("${Uri.encode(it.key)}=${Uri.encode(it.value.toString())}") } } } @@ -2268,6 +2484,7 @@ class Account(client: Client) : Service(client) { * @param success URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param failure URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param scopes A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of 100 scopes are allowed, each 4096 characters long. + * @param state An opaque value your app generates and keeps, for example in a cookie. It is returned unchanged as the `state` query parameter on the success and failure URLs, so your app can check that the sign-in it receives is one it started. Printable ASCII only (RFC 6749 Appendix A.5). Max length: 256 chars. */ @JvmOverloads suspend fun createOAuth2Token( @@ -2276,6 +2493,7 @@ class Account(client: Client) : Service(client) { success: String? = null, failure: String? = null, scopes: List? = null, + state: String? = null, ) { val apiPath = ("/account/tokens/oauth2/{provider}" .replace("{provider}", provider.value) @@ -2284,6 +2502,7 @@ class Account(client: Client) : Service(client) { "success" to success, "failure" to failure, "scopes" to scopes, + "state" to state, "project" to client.config["project"], ) val apiQuery = mutableListOf() @@ -2294,11 +2513,11 @@ class Account(client: Client) : Service(client) { } is List<*> -> { (it.value as List<*>).forEach { v -> - apiQuery.add("${it.key}[]=${v.toString()}") + apiQuery.add("${Uri.encode(it.key)}[]=${Uri.encode(v.toString())}") } } else -> { - apiQuery.add("${it.key}=${it.value.toString()}") + apiQuery.add("${Uri.encode(it.key)}=${Uri.encode(it.value.toString())}") } } } @@ -2332,6 +2551,68 @@ class Account(client: Client) : Service(client) { } } + /** + * Start signing in with a passkey. No email or user ID is needed: pass the returned `publicKey` options to `navigator.credentials.get()` and the user picks one of their passkeys. Then call [Update passkey token](/docs/references/cloud/client-web/account#updatePasskeyToken) with the credential to receive a token, and exchange it with [Create session](/docs/references/cloud/client-web/account#createSession). The challenge expires after 5 minutes. + * + * @return [io.appwrite.models.PasskeyChallenge] + */ + suspend fun createPasskeyToken(): io.appwrite.models.PasskeyChallenge { + val apiPath = "/account/tokens/passkey" + val apiParams = mutableMapOf() + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.PasskeyChallenge = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.PasskeyChallenge.from(map = it as Map) + } + return client.call( + "POST", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.PasskeyChallenge::class.java, + converter, + ) + } + + /** + * Complete a passkey sign-in started with [Create passkey token](/docs/references/cloud/client-web/account#createPasskeyToken). Pass the challenge ID and the JSON form of the credential returned by `navigator.credentials.get()`, for example `credential.toJSON()`. The returned token includes its secret and expires after 1 minute: exchange it for a session with [Create session](/docs/references/cloud/client-web/account#createSession). Passkeys require user verification, so the session also satisfies MFA. Each challenge can only be used once. + * + * @param challengeId Challenge ID returned by createPasskeyToken. + * @param credential Authentication credential returned by the authenticator, in the JSON form produced by `PublicKeyCredential.toJSON()`. + * @return [io.appwrite.models.TokenSecret] + */ + suspend fun updatePasskeyToken( + challengeId: String, + credential: Map, + ): io.appwrite.models.TokenSecret { + val apiPath = "/account/tokens/passkey" + val apiParams = mutableMapOf( + "challengeId" to challengeId, + "credential" to credential, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.TokenSecret = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.TokenSecret.from(map = it as Map) + } + return client.call( + "PUT", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.TokenSecret::class.java, + converter, + ) + } + /** * Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST /v1/account/sessions/token](https://appwrite.io/docs/references/cloud/client-web/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes. * diff --git a/library/src/main/java/io/appwrite/services/Analytics.kt b/library/src/main/java/io/appwrite/services/Analytics.kt new file mode 100644 index 00000000..1949f76b --- /dev/null +++ b/library/src/main/java/io/appwrite/services/Analytics.kt @@ -0,0 +1,83 @@ +package io.appwrite.services + +import android.net.Uri +import io.appwrite.Client +import io.appwrite.Service +import io.appwrite.exceptions.AppwriteException +import io.appwrite.extensions.classOf +import io.appwrite.models.* +import okhttp3.Cookie +import java.io.File + +/** + * + */ +class Analytics(client: Client) : Service(client) { + /** + * Send a tracking event from a browser, native app, or server-side SDK. + * + * @param propertyId Analytics property ID. + * @param name Event name. "pageview" is just a conventional event name; events are not modeled specially. + * @param url Full page URL or screen identifier. + * @param domain Hostname (e.g. example.com). + * @param referrer Referrer URL. + * @param screenWidth Viewport width in CSS pixels. + * @param sessionHash Optional session hash (provided by SDK). + * @param scrollDepth Scroll depth percentage 0-100. + * @param engagementTime Engagement time in seconds, 0-4294967295. + * @param props Custom string properties as a flat key=value list (max 32 entries, alternating key,value). + * @param userId Override user ID. Requires API-key auth with analytics.write scope. + * @param ip Override IP address. Requires API-key auth with analytics.write scope. + * @param userAgent Override user agent. Requires API-key auth with analytics.write scope. + * @return [Any] + */ + @JvmOverloads + suspend fun createEvent( + propertyId: String, + name: String, + url: String, + domain: String? = null, + referrer: String? = null, + screenWidth: Long? = null, + sessionHash: String? = null, + scrollDepth: Long? = null, + engagementTime: Long? = null, + props: List? = null, + userId: String? = null, + ip: String? = null, + userAgent: String? = null, + ): Any { + if (propertyId.isEmpty()) { + throw AppwriteException("Missing required parameter: \"propertyId\"") + } + val apiPath = ("/analytics/properties/{propertyId}/events" + .replace("{propertyId}", propertyId) + ) + val apiParams = mutableMapOf( + "name" to name, + "url" to url, + "domain" to domain, + "referrer" to referrer, + "screenWidth" to screenWidth, + "sessionHash" to sessionHash, + "scrollDepth" to scrollDepth, + "engagementTime" to engagementTime, + "props" to props, + "userId" to userId, + "ip" to ip, + "userAgent" to userAgent, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + return client.call( + "POST", + apiPath, + apiHeaders, + apiParams, + responseType = Any::class.java, + ) + } +} diff --git a/library/src/main/java/io/appwrite/services/Oauth2.kt b/library/src/main/java/io/appwrite/services/Oauth2.kt index 030f4cef..f5cbc3cb 100644 --- a/library/src/main/java/io/appwrite/services/Oauth2.kt +++ b/library/src/main/java/io/appwrite/services/Oauth2.kt @@ -85,7 +85,7 @@ class Oauth2(client: Client) : Service(client) { prompt: String? = null, maxAge: Long? = null, authorizationDetails: String? = null, - resource: String? = null, + resource: List? = null, audience: String? = null, requestUri: String? = null, ): io.appwrite.models.Oauth2Authorize { @@ -157,7 +157,7 @@ class Oauth2(client: Client) : Service(client) { prompt: String? = null, maxAge: Long? = null, authorizationDetails: String? = null, - resource: String? = null, + resource: List? = null, audience: String? = null, requestUri: String? = null, ): io.appwrite.models.Oauth2Authorize { @@ -213,7 +213,7 @@ class Oauth2(client: Client) : Service(client) { clientId: String? = null, scope: String? = null, authorizationDetails: String? = null, - resource: String? = null, + resource: List? = null, audience: String? = null, ): io.appwrite.models.Oauth2DeviceAuthorization { val apiPath = ("/oauth2/{project_id}/device_authorization" @@ -468,7 +468,7 @@ class Oauth2(client: Client) : Service(client) { prompt: String? = null, maxAge: Long? = null, authorizationDetails: String? = null, - resource: String? = null, + resource: List? = null, audience: String? = null, ): io.appwrite.models.Oauth2PAR { val apiPath = ("/oauth2/{project_id}/par" @@ -642,7 +642,7 @@ class Oauth2(client: Client) : Service(client) { clientSecret: String? = null, codeVerifier: String? = null, redirectUri: String? = null, - resource: String? = null, + resource: List? = null, audience: String? = null, ): io.appwrite.models.Oauth2Token { val apiPath = ("/oauth2/{project_id}/token" diff --git a/library/src/main/java/io/appwrite/services/Push.kt b/library/src/main/java/io/appwrite/services/Push.kt deleted file mode 100644 index 48488c06..00000000 --- a/library/src/main/java/io/appwrite/services/Push.kt +++ /dev/null @@ -1,1018 +0,0 @@ -package io.appwrite.services - -import android.Manifest -import android.app.Activity -import android.app.Application -import android.content.Context -import android.content.pm.PackageManager -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.util.Log -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat -import com.hivemq.client.mqtt.datatypes.MqttQos -import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient -import io.appwrite.Client -import io.appwrite.ResolvedTopic -import io.appwrite.Service -import io.appwrite.Topic -import io.appwrite.exceptions.AppwriteException -import org.json.JSONArray -import org.json.JSONException -import org.json.JSONObject -import java.lang.ref.WeakReference -import java.net.URI -import java.util.UUID -import java.util.concurrent.CompletableFuture -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.atomic.AtomicBoolean - -private const val LOG_TAG = "AppwritePush" - -/** A background notification the user tapped. */ -data class PushNotificationOpened( - /** The topic the message was published to. */ - val topic: String, - /** The `data` sent with the message (e.g. `createPush`), parsed; empty when it had none. */ - val data: Map, -) { - internal companion object { - fun of(tap: PushTap): PushNotificationOpened { - val data = try { - JSONObject(tap.payload).optJSONObject("data")?.toMap() - } catch (e: JSONException) { - null - } - return PushNotificationOpened(tap.topic, data ?: emptyMap()) - } - - private fun JSONObject.toMap(): Map = keys().asSequence().associateWith { jsonValue(get(it)) } - - private fun jsonValue(value: Any?): Any? = when (value) { - JSONObject.NULL -> null - is JSONObject -> value.toMap() - is JSONArray -> (0 until value.length()).map { jsonValue(value.get(it)) } - else -> value - } - } -} - -/** - * Handle for a live subscription created with [Push.subscribe]. - * - * Unlike the realtime service's bare unsubscribe callable, a push subscription carries - * per-subscription state (background delivery, notification title), so [Push.subscribe] - * returns this handle: [unsubscribe] drops it (and closes the connection once the last one - * is gone) and [update] toggles its options live without resubscribing. - */ -class PushSubscription internal constructor( - private val unsubscribeAction: () -> Unit, - private val updateAction: (Boolean?, String?, Boolean?, Boolean?) -> Unit, -) { - /** Drop this subscription. Closes the connection once the last one is gone. */ - fun unsubscribe() = unsubscribeAction() - - /** - * Live-update this subscription's background delivery, notification title, `retry` (QoS) - * and/or `notifyInForeground`. Only the arguments you pass change; the rest stay as they - * were. Changing `retry` re-subscribes this subscription's topics at the new QoS. - */ - @JvmOverloads - fun update(background: Boolean? = null, title: String? = null, retry: Boolean? = null, notifyInForeground: Boolean? = null) = - updateAction(background, title, retry, notifyInForeground) -} - -/** - * Appwrite native push service — the realtime analog delivered over an MQTT broker, - * without FCM or APNS. - * - * While the app process is alive the connection stays open and each message is handed to - * your callback. A subscription can also opt into background delivery (`background = true`): - * it is saved, and keeps delivering while the app is backgrounded, after the process is - * killed, and after a reboot or an app update, until it is unsubscribed or [close] is called - * (e.g. on sign-out). No service is kept running for it: a scheduled job and alarm wake the - * process every 15 to 60 seconds to reconnect, and the broker keeps the messages sent in - * between and replays them (for `retry = true`). While the process is alive, messages arrive - * immediately. [setForeground] adds a foreground service for immediate delivery even after a kill - * and during Doze. Messages no in-app callback receives (the process was started in the - * background) go to a declared [PushReceiver], and are posted as a notification that opens - * the app unless it handles them. While any background subscription exists, all subscriptions - * share its connection. - * - * Background delivery reconnects with the credential saved at subscribe time, so use a session - * (the app's sign-in session, or `Client.setSession`) for it: a JWT expires after minutes. A refused credential stops - * background delivery and reaches [onError], also when it was registered later. - * - * The credential is read off the client: a JWT or session set on it (`Client.setJWT` / - * `Client.setSession`), else the session the app signed in with through it (e.g. - * `Account.createEmailPasswordSession`). - * - * The connection always keeps its session (clean start off) so the broker can redeliver - * missed messages, and reliability is chosen per subscription via [subscribe]'s - * `retry`. The broker owns the replay window server-side (no MQTT session-expiry - * is set). The client id defaults to one per user and install, so replay resumes across - * restarts; `Client.setPushClientId(...)` overrides it. TLS (and skipping cert verification via a `?tlsInsecure=true` query flag) is - * derived from the endpoint, so the whole connection is described in one place. - * - * ```kotlin - * val client = Client().setEndpoint("...").setProject("...").setJWT(jwt) - * val push = Push(client, context) - * val sub = push.subscribe("user/123/#") { message -> - * println("${'$'}{message.topic}: ${'$'}{message.data}") - * } - * // ...later - * sub.unsubscribe() - * - * // Or, with no topic, the signed-in user's own topic (`users/`): - * push.subscribe { message -> println(message.data) } - * ``` - */ -class Push( - client: Client, - context: Context, -) : Service(client) { - - // The broker location is taken from Client.setPushEndpoint() when set (e.g. - // "mqtts://host:8883"), otherwise from the regular endpoint. - private val pushEndpoint: String = client.endpointPush ?: client.endpoint - - // When no explicit push endpoint is set, default to a `push.` host on the regular endpoint - // (mirroring how realtime derives from the endpoint, on the push subdomain). - private val host: String = - (runCatching { URI(pushEndpoint).host }.getOrNull() ?: "localhost") - .let { if (client.endpointPush != null) it else "push.$it" } - - // Secure by default: TLS on when the endpoint scheme is secure (https/wss/mqtts/ssl), so - // the credential in the CONNECT packet is not sent in the clear. - private val tls: Boolean = - runCatching { URI(pushEndpoint).scheme }.getOrNull()?.lowercase() in listOf("https", "wss", "mqtts", "ssl") - - // Take the port from the push endpoint only (a regular http/https endpoint's port is the - // API port, not the broker's); otherwise fall back to the MQTT default for the TLS mode. - private val port: Int = - client.endpointPush?.let { runCatching { URI(it).port.takeIf { p -> p != -1 } }.getOrNull() } - ?: (if (tls) 8883 else 1883) - - // Skip TLS cert verification (self-signed brokers / testing) via a query flag on the push - // endpoint, e.g. "mqtts://host:8883?tlsInsecure=true" — so the whole connection is - // described by one URL, not a second config knob. - private val tlsInsecure: Boolean = - client.endpointPush?.let { - runCatching { - val query = URI(it).query ?: "" - query.split("&").any { p -> p == "tlsInsecure=true" || p == "tlsInsecure=1" } - }.getOrDefault(false) - } ?: false - - // Read on the MQTT client's threads to tell a closed/replaced client's failures apart. - @Volatile - private var mqtt: Mqtt5AsyncClient? = null - - // Set when [mqtt] is replaced or closed, so that client never reconnects in the background. - private var mqttRetired: AtomicBoolean? = null - - private var connectedConfig: PushConfig? = null - private val connectLock = Any() - - @Volatile - private var connectionEpoch = 0 - - // Local id -> subscription (filter + callback + per-sub options). The id is never sent to - // the broker; it only lets the same topic carry more than one callback. HiveMQ callbacks - // look their subscription up by id on delivery, because a callback stays attached to its - // filter until the filter itself is unsubscribed, which waits for the filter's last sub. - private val subscriptions = ConcurrentHashMap() - - // Whether the connection currently lives in the process-wide background host (its own MQTT - // client) rather than here. It moves there while any subscription, live or saved by an - // earlier run, wants background, and back here once none do — one connection either way. - private var backgroundActive = false - - // For background delivery: its saved state, scheduled runs, service and notifications. - private val appContext: Context = context.applicationContext - - init { - NotificationPermissionPrompt.track(context) - // Resume saved background delivery now, instead of at the next scheduled run, with the - // credential set on the client: a rotated session of the same user replaces the saved one. - val auth = runCatching { resolvePushAuth(client) }.getOrNull() - PushBackground.resume(appContext, auth?.first, auth?.second) - } - - private var onOpen: (() -> Unit)? = null - private var onClose: (() -> Unit)? = null - private var onError: ((Throwable) -> Unit)? = null - - private class Sub( - val filter: String, - val callback: (PushMessage) -> Unit, - var background: Boolean, - var title: String?, - var retry: Boolean, - var notifyInForeground: Boolean, - ) { - @Volatile - var client: Mqtt5AsyncClient? = null - } - - // The broker subscription for a filter uses the highest QoS any subscription on it wants, - // so a QoS-0 subscription never downgrades a QoS-1 one sharing the filter. - private fun effectiveQos(filter: String): MqttQos = - if (subscriptions.values.any { it.filter == filter && it.retry }) { - MqttQos.AT_LEAST_ONCE - } else { - MqttQos.AT_MOST_ONCE - } - - /** Register a callback invoked when the connection opens. */ - fun onOpen(callback: () -> Unit): Push { - onOpen = callback - return this - } - - /** Register a callback invoked when the connection closes. */ - fun onClose(callback: () -> Unit): Push { - onClose = callback - return this - } - - /** - * Register a callback invoked on every error the push service hits: a failed connect or - * automatic reconnect, a lost connection, a broker DISCONNECT, a rejected SUBSCRIBE or - * UNSUBSCRIBE, a foreground-service failure, and the failure of a [subscribe], - * [PushSubscription.update] or [PushSubscription.unsubscribe] call (which still throws to - * its caller). When the broker explains a refused connection or a DISCONNECT with an MQTT 5 - * Reason String, that is the error's message. - * - * Errors outside a call (on the MQTT client's or the foreground service's threads) are never - * thrown, so they cannot crash the app: with no callback registered they are logged. A - * failing call still throws to its caller. - */ - fun onError(callback: (Throwable) -> Unit): Push { - onError = callback - PushBackground.errorHandler = { deliverBackgroundError(it) } - // A refused credential that stopped background delivery while no onError was registered. - PushStore.takeStoppedError(appContext)?.let { report(AppwriteException(it)) } - return this - } - - /** - * Run background delivery in a foreground service (with a quiet ongoing - * notification), for immediate delivery even after the process is killed and during Doze, - * instead of only the scheduled runs. Saved, so it applies after restarts too; call it from - * the foreground (Android 12+ refuses to start the service from the background). Apps that - * never enable it can remove the service from their merged manifest. It only affects - * `background = true` subscriptions; `background = false` ones still deliver only while the - * app process is alive. - */ - fun setForeground(enabled: Boolean): Push { - PushBackground.setForeground(appContext, enabled) - return this - } - - /** - * The background notification whose tap launched the app, or null. Reported once; call it - * when the app starts, e.g. to open the screen the message is about. - * - * ```kotlin - * push.getInitialNotification()?.let { openSale(it.data["saleId"]) } - * ``` - */ - fun getInitialNotification(): PushNotificationOpened? = PushTaps.take()?.let { PushNotificationOpened.of(it) } - - /** - * Call [callback] on the main thread each time the user taps a background notification while - * the app is running, including in the background. Returns a function that stops listening. - * The tap that launched the app comes from [getInitialNotification] instead, as does a tap - * made while nothing listens. - * - * ```kotlin - * val stop = push.onNotificationOpened { openSale(it.data["saleId"]) } - * ``` - */ - fun onNotificationOpened(callback: (PushNotificationOpened) -> Unit): () -> Unit = - PushTaps.listen { callback(PushNotificationOpened.of(it)) } - - /** - * Subscribe to the signed-in user's own topic, `users/` — "messages for me" - * without knowing any topic, e.g. `push.subscribe { message -> ... }`. - * - * The user id is read locally from the client's credential: the `userId` claim of the JWT - * (`Client.setJWT`), else the `id` in the session (`Client.setSession`, or the session the app - * signed in with through the client). - * Throws when neither yields one. Like FCM/APNs delivery, it defaults to [background] `true` - * (keeps delivering while the app is backgrounded or closed) and [retry] - * `true` (QoS 1); pass either to override. Otherwise the options and the returned - * [PushSubscription] behave exactly as when subscribing to that topic; see the - * `List` overload. - */ - @JvmOverloads - fun subscribe( - background: Boolean = true, - title: String? = null, - retry: Boolean = true, - notifyInForeground: Boolean = false, - callback: (PushMessage) -> Unit, - ): PushSubscription { - val userId = reporting { resolvePushUserId(client) } - return subscribe(Topic.path(listOf("users", userId)), background, title, retry, notifyInForeground, callback) - } - - /** - * Subscribe to a single topic. Returns a [PushSubscription] handle. - * - * On Android 13+ the first `background = true` subscription asks for the - * `POST_NOTIFICATIONS` runtime permission; see [requestNotificationPermission]. - */ - @JvmOverloads - fun subscribe( - topic: String, - background: Boolean = false, - title: String? = null, - retry: Boolean = true, - notifyInForeground: Boolean = false, - callback: (PushMessage) -> Unit, - ): PushSubscription = - subscribe(listOf(topic), background, title, retry, notifyInForeground, callback) - - /** - * Subscribe to a single topic built with [io.appwrite.Topic], e.g. - * `push.subscribe(Topic.path(listOf("user", userId)).any()) { message -> ... }`. - * Returns a [PushSubscription] handle; see the `List` overload for the options. - */ - @JvmOverloads - fun subscribe( - topic: Topic, - background: Boolean = false, - title: String? = null, - retry: Boolean = true, - notifyInForeground: Boolean = false, - callback: (PushMessage) -> Unit, - ): PushSubscription = - subscribe(listOf(topic.toString()), background, title, retry, notifyInForeground, callback) - - /** - * Subscribe to a single topic ended with `all()`, e.g. - * `push.subscribe(Topic.path(listOf("user", userId)).all()) { message -> ... }`. - * Returns a [PushSubscription] handle; see the `List` overload for the options. - */ - @JvmOverloads - fun subscribe( - topic: ResolvedTopic, - background: Boolean = false, - title: String? = null, - retry: Boolean = true, - notifyInForeground: Boolean = false, - callback: (PushMessage) -> Unit, - ): PushSubscription = - subscribe(listOf(topic.toString()), background, title, retry, notifyInForeground, callback) - - /** - * Subscribe to a list mixing raw strings and topics built with [io.appwrite.Topic], e.g. - * `push.subscribe(listOf("news", Topic.path(listOf("chat")).any())) { message -> ... }`. - * Each element must be a `String`, `Topic` or `ResolvedTopic` (Kotlin has no common type for - * them, so this is checked at runtime). Returns a [PushSubscription] handle; see the - * `List` overload for the options. - */ - @JvmOverloads - @JvmName("subscribeTopics") - fun subscribe( - topics: List, - background: Boolean = false, - title: String? = null, - retry: Boolean = true, - notifyInForeground: Boolean = false, - callback: (PushMessage) -> Unit, - ): PushSubscription { - val filters = topics.mapIndexed { index, topic -> - require(topic is String || topic is Topic || topic is ResolvedTopic) { - "topics[$index] must be a String, Topic or ResolvedTopic, got ${topic::class.simpleName}" - } - topic.toString() - } - return subscribe(filters, background, title, retry, notifyInForeground, callback) - } - - /** - * Subscribe to one or more topics. Returns a [PushSubscription] handle. - * - * Topics are MQTT filters: pass raw strings, or build them with [io.appwrite.Topic] - * through the `Topic` overloads, e.g. `push.subscribe(Topic.path(listOf("user")).any()) { ... }`. - * - * For a foreground subscription this blocks until the broker has acknowledged every - * SUBSCRIBE (SUBACK): the broker only starts routing a topic once its subscription is - * registered, so publishing before the ack races the message ahead of the subscription - * and it is dropped (no retained replay on MQTT). - * - * Set [background] to keep receiving while the app is backgrounded or closed, also after the - * process is killed or the device restarts, and post a notification per matching message - * with [title] (defaulting to the message topic) while the app is not on screen; set - * [notifyInForeground] to also post one while it is. See the class docs. [retry] chooses this - * subscription's QoS - * (`true` -> QoS 1 default, `false` -> QoS 0). Toggle any of these later with - * [PushSubscription.update]. - * - * Note: a `background` subscription hands the connection to the background host, which - * connects and subscribes asynchronously and retries failures on its next run — so it - * returns once the host has been handed the subscription, **not** after SUBACK (the SUBACK - * guarantee above holds for foreground subscriptions). [onOpen]/[onClose] observe only the - * in-process (foreground) connection, not the background one; [onError] observes both. - */ - @JvmOverloads - fun subscribe( - topics: List, - background: Boolean = false, - title: String? = null, - retry: Boolean = true, - notifyInForeground: Boolean = false, - callback: (PushMessage) -> Unit, - ): PushSubscription = reporting { - val ids = mutableListOf() - for (topic in topics) { - val id = UUID.randomUUID().toString() - ids.add(id) - subscriptions[id] = Sub(topic, callback, background, title, retry, notifyInForeground) - } - - // If any SUBSCRIBE fails, roll this call's registrations back and return to a - // consistent host for whatever subscriptions remain, then rethrow. - try { - if (hostedInBackground()) { - hostInBackground() - } else { - subscribeInProcess(ids) - } - } catch (e: Exception) { - for (id in ids) { - subscriptions.remove(id) - } - if (subscriptions.isEmpty()) { - releaseAll() - } else { - reconcile() - } - throw e - } - - PushSubscription( - { reporting { unsubscribeIds(ids) } }, - { bg, t, rd, nf -> reporting { updateIds(ids, bg, t, rd, nf) } }, - ) - } - - // Every error the service hits goes through here: to onError when one is registered. It is - // never thrown from here (these errors arrive on HiveMQ's or the service's threads, where a - // throw would crash the app), so with no onError it is logged, like Realtime does. - private fun report(error: Throwable) { - val callback = onError - if (callback == null) { - Log.e(LOG_TAG, "Push error", error) - return - } - try { - callback(error) - } catch (callbackError: Exception) { - Log.e(LOG_TAG, "Push onError threw", callbackError) - } - } - - // Run a call that fails by throwing to its caller: the failure also reaches onError when one - // is registered, and is still thrown to the caller, who can catch it. - private fun reporting(block: () -> T): T = try { - block() - } catch (e: Exception) { - if (onError != null) { - report(e) - } - throw e - } - - // report() for an error raised on the MQTT client's (or the background host's) thread. - private fun reportAsync(error: Throwable) = report(error) - - // A background host error, for its errorHandler: delivered to onError when one is registered. - // Returns false when none is, so the host logs it (and keeps a refusal for a later onError). - private fun deliverBackgroundError(error: Throwable): Boolean { - if (onError == null) { - return false - } - report(error) - return true - } - - // Report a fire-and-forget request's failure (e.g. a SUBACK/UNSUBACK error code) unless - // [client] was closed or replaced meanwhile, which fails its pending requests on purpose. - private fun CompletableFuture<*>.reportFailure(client: Mqtt5AsyncClient) { - whenComplete { _, error -> - if (error != null && mqtt === client) { - reportAsync(error) - } - } - } - - // In-process host: connect and register this call's new filters, blocking on each SUBACK. - private fun subscribeInProcess(ids: List) { - while (true) { - val (client, epoch) = synchronized(connectLock) { connect() to connectionEpoch } - try { - for (id in ids) { - val sub = subscriptions[id] ?: continue - if (sub.client === client) { - continue - } - client.subscribeWith() - .topicFilter(sub.filter) - .qos(effectiveQos(sub.filter)) - .callback { publish -> subscriptions[id]?.callback?.invoke(publish.toPushMessage()) } - .send() - .get() - sub.client = client - } - return - } catch (e: Exception) { - if (epoch == connectionEpoch) { - throw e - } - } - } - } - - // Close the in-process client for good: retired first, so one still reconnecting in the - // background does not come back as a second connection for the same client id. - private fun dropClient() { - mqttRetired?.set(true) - mqttRetired = null - mqtt?.disconnect() - mqtt = null - } - - // Whether this Push reported the background host's connection open. - @Volatile - private var backgroundOpen = false - - // The background host's connection opened or closed: report it once per change. - private fun backgroundConnection(open: Boolean) { - if (backgroundOpen == open) { - return - } - backgroundOpen = open - try { - if (open) onOpen?.invoke() else onClose?.invoke() - } catch (e: Exception) { - Log.e(LOG_TAG, "Push connection callback threw", e) - } - } - - // Stop listening to the background host, reporting its connection closing when this Push - // reported it open, as the in-process connection reports its own close. - private fun leaveBackgroundHost() { - PushBackground.connectionHandler = null - backgroundConnection(false) - } - - // Relocate the connection into the background host and sync it with the current - // subscription set (filters, per-sub callbacks/titles), saving the background ones. - private fun hostInBackground() { - NotificationPermissionPrompt.ask(appContext) - dropClient() - subscriptions.values.forEach { it.client = null } - PushBackground.errorHandler = { deliverBackgroundError(it) } - PushBackground.connectionHandler = { open -> backgroundConnection(open) } - // Joining a connection a background run already opened: report it open now. - if (PushBackground.isConnected) { - backgroundConnection(true) - } - PushBackground.host(appContext, buildConfig(), currentListeners()) - backgroundActive = true - } - - // Bring the in-process connection up and (re)register every filter — used when the last - // background subscription goes away and the connection relocates back in-process. - private fun ensureInProcessSubscribed() { - val client = connect() - for ((id, sub) in subscriptions) { - if (sub.client === client) { - continue - } - client.subscribeWith() - .topicFilter(sub.filter) - .qos(effectiveQos(sub.filter)) - .callback { publish -> subscriptions[id]?.callback?.invoke(publish.toPushMessage()) } - .send() - .reportFailure(client) - sub.client = client - } - } - - // Re-subscribe a filter in-process at its new effective QoS. Unsubscribe first so the - // per-subscribe HiveMQ callbacks are not duplicated, then re-add one per sub on the filter. - private fun resubscribeFilterInProcess(filter: String) { - val client = mqtt ?: return - client.unsubscribeWith().topicFilter(filter).send().reportFailure(client) - val qos = effectiveQos(filter) - for ((id, sub) in subscriptions.filter { it.value.filter == filter }) { - client.subscribeWith() - .topicFilter(filter) - .qos(qos) - .callback { publish -> subscriptions[id]?.callback?.invoke(publish.toPushMessage()) } - .send() - .reportFailure(client) - sub.client = client - } - } - - // Ensure the correct host (in-process vs service) for whatever subscriptions remain, and - // sync it. Called after an unsubscribe/update may have flipped whether background is wanted. - // Only acts on a host change; an in-process update that changes QoS is applied by the - // caller (so an unchanged in-process host is not needlessly resubscribed). - private fun reconcile() { - if (subscriptions.isEmpty()) { - return - } - if (hostedInBackground()) { - // Relocate to (or re-sync) the background host, which applies added/removed/QoS-changed topics. - hostInBackground() - } else if (backgroundActive) { - // Relocate back in-process. - backgroundActive = false - leaveBackgroundHost() - PushBackground.release(appContext) - ensureInProcessSubscribed() - } - } - - private fun unsubscribeIds(ids: List) { - val wasService = backgroundActive - val maybeDowngrade = mutableSetOf() - for (id in ids) { - val entry = subscriptions.remove(id) - val filter = entry?.filter - if (entry?.retry == true && filter != null) { - maybeDowngrade.add(filter) - } - // In-process only: drop the broker filter once its last local callback is gone. - if (!wasService && filter != null && subscriptions.values.none { it.filter == filter }) { - mqtt?.let { client -> client.unsubscribeWith().topicFilter(filter).send().reportFailure(client) } - } - } - if (subscriptions.isEmpty()) { - releaseAll() - return - } - // A removed sub may have been the last background one (relocate in-process) or just - // change the service's topic/QoS set (the service re-sync applies the new QoS). - if (wasService) { - reconcile() - return - } - // In-process, a filter that lost its last QoS-1 sub but still has QoS-0 subs must be - // re-subscribed lower, or the broker keeps replaying to at-most-once subs. - for (filter in maybeDowngrade) { - if (subscriptions.values.any { it.filter == filter } && effectiveQos(filter) == MqttQos.AT_MOST_ONCE) { - resubscribeFilterInProcess(filter) - } - } - } - - private fun updateIds(ids: List, background: Boolean?, title: String?, retry: Boolean?, notifyInForeground: Boolean?) { - val changedFilters = mutableSetOf() - for (id in ids) { - val sub = subscriptions[id] ?: continue - if (background != null) { - sub.background = background - } - if (title != null) { - sub.title = title - } - if (notifyInForeground != null) { - sub.notifyInForeground = notifyInForeground - } - if (retry != null && retry != sub.retry) { - sub.retry = retry - changedFilters.add(sub.filter) - } - } - val wasService = backgroundActive - // Handle any host change (relocation); a relocated host resubscribes everything fresh. - reconcile() - // Apply QoS changes to a host that did not relocate: the service re-sync above already - // covers the service host, so only the steady in-process host needs an explicit resub. - if (!hostedInBackground() && !wasService) { - for (filter in changedFilters) { - resubscribeFilterInProcess(filter) - } - } - } - - // The connection lives in the background host while any live subscription wants background, - // or an earlier run saved background subscriptions that are still delivered: two connections - // with the same client id would take over each other's broker session. - private fun hostedInBackground(): Boolean = - subscriptions.values.any { it.background } || PushBackground.hasSaved(appContext) - - private fun currentListeners(): List = - subscriptions.values.map { - PushListener(setOf(it.filter), it.callback, it.background, it.title, it.retry, notifyInForeground = it.notifyInForeground) - } - - /** - * Tear down the connection and drop all subscriptions, including the background ones saved - * by an earlier run, and stop background delivery. Call it on sign-out. - */ - fun close() { - dropClient() - subscriptions.clear() - backgroundActive = false - leaveBackgroundHost() - PushBackground.stop(appContext) - } - - // The last live subscription is gone: close the connection, keeping any background - // subscriptions saved by an earlier run. - private fun releaseAll() { - dropClient() - subscriptions.clear() - if (backgroundActive) { - backgroundActive = false - leaveBackgroundHost() - PushBackground.release(appContext) - } - } - - private fun connect(): Mqtt5AsyncClient = synchronized(connectLock) { - val config = buildConfig() - // Reuse the existing client even while it is mid automatic-reconnect. Building a second - // client here would give two clients the same stable id, so they would fight over the - // broker session and callbacks could stay attached to the abandoned one. - mqtt?.let { existing -> - if (config == connectedConfig) { - return existing - } - connectionEpoch++ - dropClient() - } - - // The listeners surface the lifecycle to onOpen/onClose and every connection error to - // onError, except a refused initial CONNECT: pushConnect throws that one, and the - // failing call reports it. - val retired = AtomicBoolean(false) - val client = buildPushClient( - config, - onConnected = { onOpen?.invoke() }, - onDisconnected = { onClose?.invoke() }, - onError = { reportAsync(it) }, - retired = retired, - ) - mqtt = client - mqttRetired = retired - connectedConfig = config - - try { - client.pushConnect(config) - } catch (e: Exception) { - retired.set(true) - client.disconnect() - if (mqtt === client) { - mqtt = null - connectedConfig = null - } - throw e - } - - for ((id, sub) in subscriptions) { - if (sub.client == null || sub.client === client) { - continue - } - client.subscribeWith() - .topicFilter(sub.filter) - .qos(effectiveQos(sub.filter)) - .callback { publish -> subscriptions[id]?.callback?.invoke(publish.toPushMessage()) } - .send() - .reportFailure(client) - sub.client = client - } - - client - } - - private fun buildConfig(): PushConfig { - val (authMethod, credential) = resolvePushAuth(client) - return PushConfig( - host = host, - port = port, - tls = tls, - tlsInsecure = tlsInsecure, - // HiveMQ cannot connect with an empty id (it expects the broker to assign one), so - // when the app did not call Client.setPushClientId() derive a stable id per user and - // install. The broker keeps the replay position per user and client id, so each - // device resumes its own backlog across restarts, and two devices of one user do not - // take over each other's session. - clientId = client.pushClientId?.takeIf { it.isNotEmpty() } - ?: defaultPushClientId(appContext, authMethod, credential), - keepAlive = KEEP_ALIVE_SECONDS, - authMethod = authMethod, - credential = credential, - project = client.config["project"] ?: "", - ) - } - - companion object { - // Fixed keep-alive (PINGREQ interval) in seconds — not exposed as an option. - private const val KEEP_ALIVE_SECONDS = 60 - - /** The notification tap intent's extra holding the message topic. */ - const val EXTRA_TOPIC = PushBackground.EXTRA_TOPIC - - /** The notification tap intent's extra holding the message payload as a string. */ - const val EXTRA_PAYLOAD = PushBackground.EXTRA_PAYLOAD - - /** - * What background delivery can rely on: exact alarms, battery-optimisation exemption and - * foreground mode. When [PushBackgroundStatus.bestEffort] is true, wake-ups are inexact and - * messages can arrive late while the app is closed; explain why before asking with - * [requestExactAlarms] or [requestIgnoreBatteryOptimizations]. - */ - @JvmStatic - fun backgroundStatus(context: Context): PushBackgroundStatus = PushBackground.backgroundStatus(context) - - /** - * Open the system screen where the user allows exact alarms (Android 12+), for punctual - * background wake-ups. Call it from a user action. Returns false when there is nothing to - * ask: already allowed, older Android, or the app does not declare `SCHEDULE_EXACT_ALARM`. - */ - @JvmStatic - fun requestExactAlarms(context: Context): Boolean = PushBackground.requestExactAlarms(context) - - /** - * Ask the user to exempt the app from battery optimisation. Call it from a user action. - * Returns false when there is nothing to ask: already exempt, or the app does not declare - * `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS`. - */ - @JvmStatic - fun requestIgnoreBatteryOptimizations(context: Context): Boolean = - PushBackground.requestIgnoreBatteryOptimizations(context) - - /** - * Request the `POST_NOTIFICATIONS` runtime permission (Android 13+) from [activity]. The - * first `background = true` subscription asks on its own, from the visible activity (the - * one `Push` was created with, else the next one to resume); call this to ask at a moment - * of your choosing instead. On older versions it is a no-op. - */ - @JvmStatic - @JvmOverloads - fun requestNotificationPermission(activity: Activity, requestCode: Int = 9412) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - return - } - if (ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS) - != PackageManager.PERMISSION_GRANTED - ) { - ActivityCompat.requestPermissions( - activity, - arrayOf(Manifest.permission.POST_NOTIFICATIONS), - requestCode, - ) - } - } - } -} - -/** - * The session set on the client (Client.setSession), else the one the app signed in with through it - * (e.g. Account.createEmailPasswordSession), read from its cookie store. - */ -internal fun pushSession(client: Client): String? { - client.config["session"]?.takeIf { it.isNotEmpty() }?.let { return it } - val name = "a_session_${client.config["project"] ?: ""}" - return try { - client.getCookies(client.endpoint) - .firstOrNull { it.name == name && it.value.isNotEmpty() } - ?.let { Uri.decode(it.value) } - } catch (e: IllegalArgumentException) { - null - } -} - -/** Read the credential set on the client (via Client.setJWT / setSession), else its sign-in session. */ -internal fun resolvePushAuth(client: Client): Pair { - val jwt = client.config["jwt"] ?: client.config["jWT"] - val session = pushSession(client) - return when { - !jwt.isNullOrEmpty() -> "appwrite-jwt" to jwt - !session.isNullOrEmpty() -> "appwrite-session" to session - else -> throw AppwriteException( - "No credential: sign in with this client, or call Client.setJWT() or Client.setSession() first.", - ) - } -} - -/** - * The signed-in user's id for the topic-less [Push.subscribe], read locally in the same order as - * [resolvePushAuth]: the JWT's `userId` claim, else the `id` in the session secret (standard - * base64 of `{"id": ..., "secret": ...}`). Throws when neither yields one. - */ -internal fun resolvePushUserId(client: Client): String { - val jwt = client.config["jwt"] ?: client.config["jWT"] - val session = pushSession(client) - val userId = when { - !jwt.isNullOrEmpty() -> userIdFromJwt(jwt) - !session.isNullOrEmpty() -> userIdFromSession(session) - else -> "" - } - if (userId.isEmpty()) { - throw AppwriteException( - "subscribe() without a topic needs a signed-in user: sign in with this client, or set a JWT or session on it", - ) - } - return userId -} - -// Asks once per process for POST_NOTIFICATIONS (Android 13+) when background delivery starts, from -// the app's visible activity, or from the next one to resume when none is. An activity Push is -// created with counts as visible, since it resumed before the lifecycle callbacks were registered. -private object NotificationPermissionPrompt { - private const val REQUEST_CODE = 9412 - - private var tracking = false - private var asked = false - private var waiting = false - private var visible: WeakReference? = null - - fun track(context: Context) { - if (context is Activity && !context.isFinishing) { - val ask = synchronized(this) { - visible = WeakReference(context) - waiting.also { waiting = false } - } - if (ask) { - context.runOnUiThread { request(context) } - } - } - val app = context.applicationContext as? Application ?: return - synchronized(this) { - if (tracking) { - return - } - tracking = true - } - app.registerActivityLifecycleCallbacks( - object : Application.ActivityLifecycleCallbacks { - override fun onActivityResumed(activity: Activity) { - val ask = synchronized(this@NotificationPermissionPrompt) { - visible = WeakReference(activity) - waiting.also { waiting = false } - } - if (ask) { - request(activity) - } - } - - override fun onActivityPaused(activity: Activity) { - synchronized(this@NotificationPermissionPrompt) { - if (visible?.get() === activity) { - visible = null - } - } - } - - override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit - - override fun onActivityStarted(activity: Activity) = Unit - - override fun onActivityStopped(activity: Activity) = Unit - - override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit - - override fun onActivityDestroyed(activity: Activity) = Unit - }, - ) - } - - fun ask(context: Context) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || granted(context)) { - return - } - val activity = synchronized(this) { - if (asked) { - return - } - asked = true - visible?.get().also { - if (it == null) { - waiting = true - } - } - } ?: return - activity.runOnUiThread { request(activity) } - } - - private fun granted(context: Context): Boolean = - ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED - - private fun request(activity: Activity) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !granted(activity)) { - ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.POST_NOTIFICATIONS), REQUEST_CODE) - } - } -} diff --git a/library/src/main/java/io/appwrite/services/PushBackground.kt b/library/src/main/java/io/appwrite/services/PushBackground.kt deleted file mode 100644 index e06dfabe..00000000 --- a/library/src/main/java/io/appwrite/services/PushBackground.kt +++ /dev/null @@ -1,1457 +0,0 @@ -package io.appwrite.services - -import android.Manifest -import android.app.ActivityManager -import android.app.AlarmManager -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.app.job.JobInfo -import android.app.job.JobScheduler -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.net.ConnectivityManager -import android.net.Network -import android.net.Uri -import android.os.Build -import android.os.PowerManager -import android.os.SystemClock -import android.provider.Settings -import android.util.AtomicFile -import android.util.Log -import android.webkit.CookieManager -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import androidx.core.content.ContextCompat -import com.hivemq.client.mqtt.MqttGlobalPublishFilter -import com.hivemq.client.mqtt.datatypes.MqttQos -import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient -import com.hivemq.client.mqtt.mqtt5.exceptions.Mqtt5ConnAckException -import com.hivemq.client.mqtt.mqtt5.message.publish.Mqtt5Publish -import io.appwrite.exceptions.AppwriteException -import org.json.JSONArray -import org.json.JSONObject -import java.io.ByteArrayOutputStream -import java.io.File -import java.io.IOException -import java.io.InputStream -import java.net.HttpURLConnection -import java.net.URL -import java.util.UUID -import java.util.concurrent.CopyOnWriteArrayList -import java.util.concurrent.ExecutionException -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicInteger - -private const val LOG_TAG = "AppwritePush" - -/** What a message's `notification` block asks a background notification to show; [present] is false without one. */ -internal data class PushNotificationContent(val present: Boolean, val title: String?, val body: String?, val image: String?) - -/** A background subscription saved across restarts: its filter, QoS choice and notification title. */ -internal data class PushEntry( - val filter: String, - val retry: Boolean, - val title: String?, - val notifyInForeground: Boolean = false, -) - -/** - * The adaptive heartbeat: its interval, the current streak of failed heartbeats, and when the - * interval last shrank (wall-clock ms, as it must survive reboots). - */ -internal data class PushHeartbeat( - val intervalMs: Long, - val failures: Int = 0, - val shrunkAt: Long = 0L, -) - -/** - * The process-wide host of the background push connection, modelled on how socket push - * services survive on Android without keeping a service alive. - * - * The background subscriptions and their connection config are saved to private storage (see - * [PushStore]), so delivery survives the process being killed, a reboot and an app update. A - * job ([PushJobService]) that reschedules itself on every run, backed by an alarm - * ([PushAlarmReceiver]), wakes the process every 15 seconds (60 when the app may use exact - * alarms or is exempt from battery optimisation). Each run holds a short wakelock and - * reconnects when the connection is gone or the default network changed, sends a heartbeat - * when one is due, and re-arms the job. While the process is alive between runs the socket - * stays open and messages arrive immediately; while it is dead the broker keeps the backlog - * for this client id and replays it when the next run subscribes again. - * - * With foreground mode enabled ([Push.setForeground]) a foreground service ([PushService]) also keeps - * the process at foreground priority, so delivery is immediate after a kill and during Doze. - * - * Messages go to the live in-app callbacks that match them. When none match (the process was - * started in the background), a declared [PushReceiver] gets the message, and a notification - * is posted unless it returns true. - */ -internal object PushBackground { - const val CHANNEL_ID = "appwrite-push" - const val NOTIFICATION_ID = 1911 - const val ACTION_TICK = "io.appwrite.push.TICK" - const val ACTION_MESSAGE = "io.appwrite.push.MESSAGE" - const val NOTIFICATION_ICON = "io.appwrite.push.notification_icon" - const val EXTRA_TOPIC = "io.appwrite.push.TOPIC" - const val EXTRA_PAYLOAD = "io.appwrite.push.PAYLOAD" - - private const val SERVICE_CHANNEL_ID = "appwrite-push-service" - const val JOB_ID = 0x50555348 - const val NEXT_JOB_ID = 0x5055534A - const val EXPEDITED_JOB_ID = 0x5055534B - const val WATCHDOG_JOB_ID = 0x50555349 - private const val WATCHDOG_INTERVAL_MS = 15 * 60 * 1_000L - private const val INTERVAL_MS = 15_000L - private const val PRIVILEGED_INTERVAL_MS = 60_000L - private const val RESTART_DELAY_MS = 3_000L - // An upper bound covering a run's connect (20 s) and its requests (10 s each); the wakelock is - // released as soon as the run finishes. - private const val WAKE_LOCK_MS = 60_000L - private const val KEEP_ALIVE_SECONDS = 300 - - // The heartbeat interval: below the broker's 1.5 x keep-alive expiry, measured on a clock that - // counts deep sleep. Networks that drop idle connections sooner (carrier NATs) make heartbeats - // fail; after HEARTBEAT_FAILURES in a row the interval shrinks by a quarter, down to - // MIN_HEARTBEAT_MS, and returns to the default HEARTBEAT_RESET_MS after the last shrink, in - // case the device moved to a better network. - const val HEARTBEAT_MS = 180_000L - private const val MIN_HEARTBEAT_MS = 60_000L - private const val HEARTBEAT_FAILURES = 3 - private const val HEARTBEAT_RESET_MS = 3 * 24 * 60 * 60 * 1_000L - private const val REQUEST_TIMEOUT_SECONDS = 10L - private const val IMAGE_TIMEOUT_MS = 5_000 - - // How long a scheduled run stays up after connecting so the broker can replay what was missed: - // until nothing arrived for DRAIN_QUIET_MS and nothing is being delivered, at most the run's cap. - const val JOB_DRAIN_MS = 15_000L - const val RECEIVER_DRAIN_MS = 5_000L - private const val DRAIN_QUIET_MS = 2_000L - private const val DRAIN_POLL_MS = 200L - private const val IMAGE_MAX_BYTES = 5 * 1024 * 1024 - private const val IMAGE_MAX_PX = 1_024 - private const val CONNECT_TIMEOUT_SECONDS = 20L - - // How long a message waits for a listener that acknowledges it itself (the React Native and - // Flutter bridges, once their callback has run) before it is acknowledged anyway, so one stuck - // callback cannot hold back the acknowledgements after it. - private const val ACK_TIMEOUT_SECONDS = 10L - - // Unsubscribing a filter nothing subscribes to is a harmless round trip the broker always - // acknowledges, so it proves the connection is alive. The broker ignores client PUBLISHes - // without a PUBACK, so a publish cannot serve as the heartbeat. - private const val HEARTBEAT_FILTER = "\$heartbeat" - - @Volatile - var channelName: String = "Notifications" - - @Volatile - var ongoingTitle: String = "Appwrite" - - // Reports errors to the Push instance that hosts its subscriptions here. Returns whether the - // app's onError received the error (false when it registered none). - @Volatile - var errorHandler: ((Throwable) -> Boolean)? = null - - // Tells the Push instance that hosts its subscriptions here when the connection opens (true) - // or closes (false). - @Volatile - var connectionHandler: ((Boolean) -> Unit)? = null - - // Whether the live connection (not a retired one) is open. - @Volatile - var isConnected = false - private set - - @Volatile - var serviceRunning = false - - // Whether a refused service start was reported, so a job that retries it every run does not - // report the same failure every 15 seconds. - @Volatile - private var serviceStartReported = false - - // The live in-app subscriptions hosted here, each with its callback. Empty after the process - // was restarted in the background, until the app subscribes again. - val listeners = CopyOnWriteArrayList() - - // Guards the saved state below and its copy on disk. - private val lock = Any() - private var loaded = false - private var config: PushConfig? = null - - // Background subscriptions saved by an earlier run that no live subscription has taken over. - private var saved: List = emptyList() - - // Every MQTT call runs on this thread, in order, so the connection state below needs no lock. - private val worker = Executors.newSingleThreadScheduledExecutor { runnable -> - Thread(runnable, "AppwritePushBackground").apply { isDaemon = true } - } - private var mqtt: Mqtt5AsyncClient? = null - - // Set when [mqtt] is replaced or closed, so that client stops reconnecting and delivering. - private var mqttRetired: AtomicBoolean? = null - - // Messages being delivered, and when one last arrived or finished, for a run's drain window. - private val delivering = AtomicInteger(0) - - @Volatile - private var lastDelivery = 0L - private var connectedConfig: PushConfig? = null - private var connectedNetwork: Network? = null - private var lastHeartbeat = 0L - private var heartbeatState: PushHeartbeat? = null - private val subscribed = mutableMapOf() - private var networkCallback: ConnectivityManager.NetworkCallback? = null - - // Worker thread only. While a caller waits for a converge (host with a completion), its - // failures go to that caller, which reports them, instead of onError. - private var waiting = false - private var failure: Throwable? = null - - /** - * Host [newListeners] (the app's live subscriptions) on the background connection with - * [newConfig], save the background ones, and arm the scheduled wake-ups. Saved subscriptions - * from an earlier run stay unless a live subscription on the same filter takes them over - * (with background on or off), or [newConfig] is for another client id (another user), whose - * saved subscriptions are dropped. - * - * With [done], it is called once the connection is up and every filter is subscribed (SUBACK), - * or with the failure, which is then not reported to onError: the caller reports it. - */ - fun host( - context: Context, - newConfig: PushConfig, - newListeners: List, - done: ((Throwable?) -> Unit)? = null, - ) { - val app = context.applicationContext - synchronized(lock) { - load(app) - if (config != null && config?.clientId != newConfig.clientId) { - saved = emptyList() - } - // A live subscription on a saved filter takes it over, whether or not it wants background: - // one that turned background off, or is later unsubscribed, must not leave the saved - // entry delivering in the background. - saved = saved.filter { entry -> newListeners.none { entry.filter in it.topics } } - config = newConfig - listeners.clear() - listeners.addAll(newListeners) - persist(app) - } - activate(app, done) - } - - /** - * Stop hosting the app's live subscriptions, keeping the saved ones from an earlier run. - * Shuts down when none remain. - */ - fun release(context: Context) { - val app = context.applicationContext - val active = synchronized(lock) { - listeners.clear() - persist(app) - isActive() - } - if (active) { - worker.execute { converge(app, heartbeat = false) } - } else { - shutdown(app) - } - } - - /** Stop background delivery completely and forget every saved subscription. */ - fun stop(context: Context) { - val app = context.applicationContext - synchronized(lock) { - listeners.clear() - config = null - saved = emptyList() - loaded = true - PushStore.clearState(app) - } - shutdown(app) - } - - /** Whether subscriptions saved by an earlier run are still delivered in the background. */ - fun hasSaved(context: Context): Boolean = synchronized(lock) { - load(context.applicationContext) - saved.isNotEmpty() - } - - /** Persist the foreground-mode choice and start or stop the foreground service to match. */ - fun setForeground(context: Context, enabled: Boolean) { - val app = context.applicationContext - PushStore.setForeground(app, enabled) - serviceStartReported = false - ensureService(app) - } - - /** Whether the foreground service should run: foreground mode is enabled and there is work to do. */ - fun wantsService(context: Context): Boolean { - val app = context.applicationContext - return PushStore.foreground(app) && synchronized(lock) { - load(app) - isActive() - } - } - - /** - * Resume saved background delivery when the app starts, without waiting for the next run, with - * the app's current credential. A rotated session of the same user replaces the saved one; a - * different user drops the saved subscriptions, as does no credential when - * [signedOutWhenMissing] (the caller read the session store, so none means signed out). - * While a live Push hosts subscriptions here, it does nothing: that Push owns the connection. - */ - fun resume(context: Context, authMethod: String? = null, credential: String? = null, signedOutWhenMissing: Boolean = false) { - val app = context.applicationContext - val resume = synchronized(lock) { - load(app) - val current = config - when { - !isActive() || current == null -> null - // A live Push hosts here and owns the connection and its credential; resuming only - // reconciles saved delivery, so it leaves a live host alone. - listeners.isNotEmpty() -> null - // No credential: signed out when the caller can tell, else possibly not set yet. - authMethod == null || credential == null -> if (signedOutWhenMissing) false else null - current.authMethod == authMethod && current.credential == credential -> true - // A new credential for the same user (a rotated session) replaces the saved one, - // so the stale one is never sent and refused. - sameUser(current.authMethod, current.credential, authMethod, credential) -> { - config = current.copy(authMethod = authMethod, credential = credential) - persist(app) - true - } - // Another user signed in: the saved subscriptions were not theirs. - else -> false - } - } - when (resume) { - true -> tick(app) {} - false -> stop(app) - null -> Unit - } - } - - private enum class CredentialRefresh { UNCHANGED, CHANGED, GONE } - - // Bring the saved credential up to date with the session cookie the app - // signs in with, when it comes from one (see [PushConfig.sessionCookieUrl]): a rotated session of - // the same user replaces it, and no session or another user's is GONE. UNCHANGED when there is - // no cookie source or it cannot be read. - private fun refreshCredential(context: Context): CredentialRefresh { - val current = synchronized(lock) { config } ?: return CredentialRefresh.UNCHANGED - val url = current.sessionCookieUrl ?: return CredentialRefresh.UNCHANGED - val session = readSessionCookie(url, current.project) ?: return CredentialRefresh.UNCHANGED - return when { - session.isEmpty() -> CredentialRefresh.GONE - current.authMethod == "appwrite-session" && session == current.credential -> CredentialRefresh.UNCHANGED - sameUser(current.authMethod, current.credential, "appwrite-session", session) -> { - synchronized(lock) { - config = current.copy(authMethod = "appwrite-session", credential = session) - persist(context.applicationContext) - } - CredentialRefresh.CHANGED - } - else -> CredentialRefresh.GONE - } - } - - // The credential to send for a connection made with [connected]: the session re-read from the - // cookie store when one backs it (so an automatic reconnect uses a rotated session), else the - // one it connected with. Only a newer credential of the same user, for the same connection - // (broker, client id, project), is borrowed: a connection being replaced by [host] keeps its - // own. Null when the app signed out or another user signed in: authentication is aborted and - // delivery stops. - private fun currentCredential(context: Context, connected: PushConfig): String? { - if (refreshCredential(context) == CredentialRefresh.GONE) { - worker.execute { stop(context) } - return null - } - val current = synchronized(lock) { config } ?: return connected.credential - val sameConnection = current.host == connected.host && current.port == connected.port && - current.clientId == connected.clientId && current.project == connected.project && - current.authMethod == connected.authMethod - return if (sameConnection && sameUser(connected.authMethod, connected.credential, current.authMethod, current.credential)) { - current.credential - } else { - connected.credential - } - } - - // The `a_session_` cookie for [url] in the WebView cookie store, decoded; "" when there - // is none, and null when the store cannot be read. - private fun readSessionCookie(url: String, project: String): String? = runCatching { - val name = "a_session_$project" - CookieManager.getInstance().getCookie(url) - ?.split(";") - ?.map { it.trim() } - ?.firstOrNull { it.substringBefore("=") == name } - ?.substringAfter("=") - ?.let { Uri.decode(it) } - ?: "" - }.getOrNull() - - private fun sameUser(savedMethod: String, saved: String, method: String, credential: String): Boolean { - fun userId(method: String, credential: String) = - if (method == "appwrite-session") userIdFromSession(credential) else userIdFromJwt(credential) - val user = userId(method, credential) - return user.isNotEmpty() && user == userId(savedMethod, saved) - } - - /** - * One scheduled run: under a wakelock, reconnect if needed, send a heartbeat when one is due, - * re-arm the next run, and call [done] once finished. Shuts down when nothing is saved. - * - * A run of a chained job ([jobId]) arms the next run under the other chained id: scheduling the - * id that is running would stop the run in progress. With [drainMs], the run stays up after - * connecting while the broker replays what was missed (at most that long), so a process woken - * from the background is not cached and frozen before the messages are delivered. - */ - fun tick(context: Context, jobId: Int? = null, drainMs: Long = 0, done: () -> Unit) { - val app = context.applicationContext - val wakeLock = acquireWakeLock(app) - worker.execute { - try { - val active = synchronized(lock) { - load(app) - isActive() - } - if (active) { - // Arm the next run first, so a run cut short (killed, or a slow broker outlasting - // the wakelock) still leaves the chain armed; the watchdog job restarts a chain - // that was dropped anyway. A refused credential in converge() shuts down, - // cancelling them again. - schedule(app, INTERVAL_MS, runningJobId = jobId) - ensureWatchdog(app) - ensureService(app) - registerNetworkCallback(app) - converge(app, heartbeat = true) - if (drainMs > 0 && mqtt != null) { - awaitDeliveries(drainMs) - } - } else { - shutdown(app) - } - } catch (e: Exception) { - report(e) - } finally { - releaseWakeLock(wakeLock) - done() - } - } - } - - // Worker thread only. Wait until no message arrived for DRAIN_QUIET_MS and none is being - // delivered, at most [maxMs]. Timed with the real monotonic clock ([nowMs]), which keeps moving - // while this thread sleeps. - private fun awaitDeliveries(maxMs: Long) { - val start = nowMs() - if (lastDelivery < start) { - lastDelivery = start - } - while (nowMs() - start < maxMs) { - if (delivering.get() == 0 && nowMs() - lastDelivery >= DRAIN_QUIET_MS) { - return - } - Thread.sleep(DRAIN_POLL_MS) - } - } - - private fun nowMs(): Long = System.nanoTime() / 1_000_000 - - /** - * Android 12+: hand a run to an expedited job, which starts at once with network access and - * keeps the process runnable until it finishes. A broadcast receiver woken in a cached process - * runs at background priority and can be frozen before it connects. Returns false when the - * job cannot be scheduled (older Android, or the expedited quota is used up), so the caller - * runs the tick itself. - */ - fun runExpedited(context: Context): Boolean { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { - return false - } - return try { - val job = JobInfo.Builder(EXPEDITED_JOB_ID, ComponentName(context, PushJobService::class.java)) - .setExpedited(true) - .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) - .build() - context.getSystemService(JobScheduler::class.java)?.schedule(job) == JobScheduler.RESULT_SUCCESS - } catch (e: Exception) { - false - } - } - - /** Run once more shortly after the task was removed, so an OEM kill that follows is undone. */ - fun scheduleRestart(context: Context) { - schedule(context.applicationContext, RESTART_DELAY_MS, alarmOnly = true) - } - - /** - * Forget everything held in memory, as the process dying does, keeping only what is saved. - * For tests of what happens after the app's process was killed. - */ - internal fun dropProcessState(context: Context) { - synchronized(lock) { - listeners.clear() - config = null - saved = emptyList() - loaded = false - } - errorHandler = null - connectionHandler = null - unregisterNetworkCallback(context.applicationContext) - worker.submit { disconnect() }.get() - } - - /** Report an error to the hosting Push instance's onError; logged when there is none. */ - fun report(error: Throwable): Boolean { - val delivered = try { - errorHandler?.invoke(error) ?: false - } catch (callbackError: Exception) { - Log.e(LOG_TAG, "Push onError threw", callbackError) - true - } - if (!delivered) { - Log.e(LOG_TAG, "Push background error", error) - } - return delivered - } - - // Must hold [lock]. - private fun load(context: Context) { - if (loaded) { - return - } - loaded = true - val state = PushStore.loadState(context) ?: return - config = state.first - saved = state.second - } - - // Must hold [lock]. - private fun isActive(): Boolean = config != null && (saved.isNotEmpty() || listeners.isNotEmpty()) - - // Must hold [lock]. Every background subscription, live or saved, one per filter and title. - // Subscriptions sharing a filter and title are saved once, with QoS 1 when any of them wants it. - private fun entries(): List = - (saved + liveEntries()) - .groupBy { it.filter to it.title } - .map { (key, group) -> PushEntry(key.first, group.any { it.retry }, key.second, group.any { it.notifyInForeground }) } - - // Must hold [lock]. - private fun liveEntries(): List = listeners.filter { it.background }.flatMap { listener -> - listener.topics.map { PushEntry(it, listener.retry, listener.title, listener.notifyInForeground) } - } - - // Must hold [lock]. - private fun persist(context: Context) { - val current = config - val entries = entries() - if (current == null || entries.isEmpty()) { - PushStore.clearState(context) - } else { - PushStore.saveState(context, current, entries) - } - } - - // Must hold [lock]. The broker subscriptions wanted, at the highest QoS any subscription on a - // filter asks for. - private fun wanted(): Map { - val qos = linkedMapOf() - fun want(filter: String, retry: Boolean) { - if (retry) { - qos[filter] = MqttQos.AT_LEAST_ONCE - } else if (filter !in qos) { - qos[filter] = MqttQos.AT_MOST_ONCE - } - } - for (listener in listeners) { - listener.topics.forEach { want(it, listener.retry) } - } - saved.forEach { want(it.filter, it.retry) } - return qos - } - - private fun activate(context: Context, done: ((Throwable?) -> Unit)? = null) { - registerNetworkCallback(context) - schedule(context, INTERVAL_MS) - ensureWatchdog(context) - ensureService(context) - worker.execute { converge(context, heartbeat = false, done) } - } - - private fun shutdown(context: Context) { - cancelSchedule(context) - unregisterNetworkCallback(context) - if (serviceRunning) { - context.stopService(Intent(context, PushService::class.java)) - } - worker.execute { disconnect() } - } - - // Worker thread only. Bring the connection to the wanted state: reconnect when the config or - // the default network changed or a due heartbeat fails, then subscribe and unsubscribe the - // difference. With [done], failures go to it (see [waiting]) instead of onError. - private fun converge(context: Context, heartbeat: Boolean, done: ((Throwable?) -> Unit)? = null) { - waiting = done != null - failure = null - try { - convergeOnce(context, heartbeat) - } catch (e: Exception) { - fail(e) - } finally { - waiting = false - done?.invoke(failure) - } - } - - // Worker thread only. Record a failure for the waiting caller, or report it. - private fun fail(error: Throwable) { - if (failure == null) { - failure = error - } - if (!waiting) { - report(error) - } - } - - private fun convergeOnce(context: Context, heartbeat: Boolean) { - // Signed out, or signed in as someone else, since the subscriptions were saved. - if (refreshCredential(context) == CredentialRefresh.GONE) { - stop(context) - return - } - val (current, wanted) = synchronized(lock) { config to wanted() } - if (current == null || wanted.isEmpty()) { - disconnect() - return - } - var client = mqtt - if (client != null && (current != connectedConfig || networkChanged(context))) { - disconnect() - client = null - } - if (client != null && heartbeat && SystemClock.elapsedRealtime() - lastHeartbeat >= heartbeatInterval(context)) { - val alive = sendHeartbeat(client) - recordHeartbeat(context, alive) - if (alive) { - lastHeartbeat = SystemClock.elapsedRealtime() - } else { - disconnect() - client = null - } - } - if (client == null) { - client = connect(context, current) ?: return - } - for (filter in subscribed.keys - wanted.keys) { - request { client.unsubscribeWith().topicFilter(filter).send().get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS) } - subscribed.remove(filter) - } - for ((filter, qos) in wanted) { - if (subscribed[filter] != qos && subscribe(client, filter, qos)) { - subscribed[filter] = qos - } - } - } - - // Worker thread only. Connect with [config], or report why not and return null. A refused - // credential stops background delivery: retrying it would only be refused again. - private fun connect(context: Context, config: PushConfig): Mqtt5AsyncClient? { - // A failed first connect of a waited-for converge goes to the waiting caller only. - val quiet = waiting - val connected = AtomicBoolean(false) - val retired = AtomicBoolean(false) - val client = buildPushClient( - config.copy(keepAlive = KEEP_ALIVE_SECONDS), - onConnected = { - if (!retired.get()) { - isConnected = true - connectionHandler?.invoke(true) - } - }, - onDisconnected = { - if (!retired.get()) { - isConnected = false - connectionHandler?.invoke(false) - } - }, - onError = { error -> - if (!quiet || connected.get()) { - report(error) - } - }, - onAuthRefused = { error -> worker.execute { refused(context, error) } }, - credential = { currentCredential(context, config) }, - retired = retired, - ) - // Registered before connecting, so the backlog the broker replays right after each - // SUBSCRIBE reaches the app however the subscription was made (including the ones the - // client restores itself after an automatic reconnect). Acknowledged by hand, once the - // message was delivered (see [deliver]). - client.publishes( - MqttGlobalPublishFilter.ALL, - { publish -> - if (!retired.get()) { - deliver(context, publish) - } - }, - true, - ) - try { - client.sendPushConnect(config.copy(keepAlive = KEEP_ALIVE_SECONDS), CONNECT_TIMEOUT_SECONDS) { - currentCredential(context, config) - } - } catch (e: Exception) { - val cause = (e as? ExecutionException)?.cause ?: e - retired.set(true) - client.disconnect() - val error = connectError(cause) - when { - isAuthRefusal(cause) -> refused(context, error) - waiting -> fail(error) - // A network failure already reached onError through the client's disconnect - // listener; a refused CONNECT and a timeout did not. - cause is Mqtt5ConnAckException || cause is TimeoutException -> report(error) - } - return null - } - connected.set(true) - mqtt = client - mqttRetired = retired - connectedConfig = config - connectedNetwork = activeNetwork(context) - lastHeartbeat = SystemClock.elapsedRealtime() - subscribed.clear() - return client - } - - // Worker thread only. - private fun subscribe(client: Mqtt5AsyncClient, filter: String, qos: MqttQos): Boolean = request { - val subAck = client.subscribeWith().topicFilter(filter).qos(qos).send().get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS) - val code = subAck.reasonCodes.firstOrNull { it.isError } - if (code != null) { - throw AppwriteException( - subAck.reasonString.map { it.toString() }.orElse("Subscribing to $filter was refused (reason $code)"), - ) - } - } - - // Worker thread only. The current heartbeat interval, back at the default once a shrink is - // old enough. - private fun heartbeatInterval(context: Context): Long { - val state = heartbeatState ?: PushStore.heartbeat(context).also { heartbeatState = it } - if (state.intervalMs < HEARTBEAT_MS && System.currentTimeMillis() - state.shrunkAt >= HEARTBEAT_RESET_MS) { - saveHeartbeat(context, PushHeartbeat(HEARTBEAT_MS)) - return HEARTBEAT_MS - } - return state.intervalMs - } - - // Worker thread only. Count a failed heartbeat (a connection the network dropped silently), - // shrinking the interval after HEARTBEAT_FAILURES in a row; a delivered one ends the streak. - private fun recordHeartbeat(context: Context, alive: Boolean) { - val state = heartbeatState ?: PushStore.heartbeat(context) - val next = when { - alive -> state.copy(failures = 0) - state.failures + 1 < HEARTBEAT_FAILURES -> state.copy(failures = state.failures + 1) - else -> PushHeartbeat(maxOf(MIN_HEARTBEAT_MS, state.intervalMs * 3 / 4), 0, System.currentTimeMillis()) - } - if (next != state) { - saveHeartbeat(context, next) - } - } - - private fun saveHeartbeat(context: Context, state: PushHeartbeat) { - heartbeatState = state - PushStore.setHeartbeat(context, state) - } - - // Worker thread only. - private fun sendHeartbeat(client: Mqtt5AsyncClient): Boolean = try { - client.unsubscribeWith().topicFilter(HEARTBEAT_FILTER).send().get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS) - true - } catch (e: Exception) { - false - } - - // Run a broker request, recording or reporting its failure. Returns whether it succeeded. - private fun request(block: () -> Unit): Boolean = try { - block() - true - } catch (e: Exception) { - fail((e as? ExecutionException)?.cause ?: e) - false - } - - // Worker thread only. - private fun disconnect() { - mqttRetired?.set(true) - mqttRetired = null - // A retired client's own disconnect is not reported, so report the live one closing here. - if (isConnected) { - isConnected = false - connectionHandler?.invoke(false) - } - mqtt?.disconnect() - mqtt = null - connectedConfig = null - connectedNetwork = null - subscribed.clear() - } - - // Worker thread only. The broker refused the credential: report it and stop. With no onError - // registered the message is also saved, and the next onError registration receives it. A - // waiting caller receives it instead. - private fun refused(context: Context, error: Throwable) { - // The saved credential may be a session the app has since rotated: retry once with the - // current one. Refused again, it is unchanged and delivery stops below. - if (!waiting) { - when (refreshCredential(context)) { - CredentialRefresh.CHANGED -> { - converge(context, heartbeat = false) - return - } - CredentialRefresh.GONE -> { - stop(context) - return - } - CredentialRefresh.UNCHANGED -> Unit - } - } - if (waiting) { - fail(error) - } else if (!report(error)) { - PushStore.setStoppedError(context, error.message ?: "Push connection refused") - } - stop(context) - } - - // Deliver a message, then acknowledge it: right away, or once every listener that acknowledges - // messages itself has done so (at most ACK_TIMEOUT_SECONDS later). - // A message counts as being delivered until it is acknowledged: for the React Native and Flutter - // bridges, once their callback has run (or the acknowledgement times out), not when the event is - // handed over, so a run's drain window waits for it. - private fun deliver(context: Context, publish: Mqtt5Publish) { - delivering.incrementAndGet() - lastDelivery = nowMs() - deliverAcknowledged(context, publish) { - lastDelivery = nowMs() - delivering.decrementAndGet() - } - } - - private fun deliverAcknowledged(context: Context, publish: Mqtt5Publish, settled: () -> Unit) { - val acknowledged = AtomicBoolean(false) - val acknowledge = { - if (acknowledged.compareAndSet(false, true)) { - runCatching { publish.acknowledge() } - settled() - } - } - val pending = AtomicInteger(1) - val settle = { - if (pending.decrementAndGet() == 0) { - acknowledge() - } - } - try { - deliverMessage(context, publish.toPushMessage()) { - pending.incrementAndGet() - val once = AtomicBoolean(false) - val ack: () -> Unit = { - if (once.compareAndSet(false, true)) { - settle() - } - } - ack - } - } finally { - settle() - } - if (!acknowledged.get()) { - ackTimer.schedule({ acknowledge() }, ACK_TIMEOUT_SECONDS, TimeUnit.SECONDS) - } - } - - // Deliver a message to the matching live callbacks; with none, to the declared PushReceiver - // and/or a notification per saved subscription that matches it. [deferAck] hands a listener - // that acknowledges messages itself its acknowledgement. - private fun deliverMessage(context: Context, message: PushMessage, deferAck: () -> () -> Unit) { - val matching = listeners.filter { listener -> listener.topics.any { matchesTopic(it, message.topic) } } - // Saved background subscriptions no live one has taken over; they deliver even when a live - // foreground-only subscription on an overlapping filter received the message too. - val entries = synchronized(lock) { saved }.filter { matchesTopic(it.filter, message.topic) } - if (matching.isEmpty() && entries.isEmpty()) { - return - } - for (listener in matching) { - try { - val acknowledging = listener.acknowledgingCallback - if (acknowledging != null) { - acknowledging(message, deferAck()) - } else { - listener.callback(message) - } - } catch (e: Exception) { - report(e) - } - } - val wakeLock = acquireWakeLock(context) - try { - // The app's PushReceiver gets only messages no live callback received. - val handled = matching.isEmpty() && deliverToReceivers(context, message) - val content = notificationContent(message) - // Background subscriptions notify while the app is not visible; on screen, only those - // that opted in with notifyInForeground do, since the app shows the message itself. A - // message no live callback received still notifies, as the app has not shown it. - val foreground = appInForeground() - val shown = foreground && matching.isNotEmpty() - val titles = matching.filter { it.background && (!foreground || it.notifyInForeground) }.map { it.title ?: message.topic } + - if (handled) emptyList() else entries.filter { !shown || it.notifyInForeground }.map { it.title ?: message.topic } - // A title the server sent replaces every subscription's, so one notification is posted. - titles.map { content.title ?: it }.distinct().forEach { notify(context, message, it, content) } - } finally { - releaseWakeLock(wakeLock) - } - } - - // Call every PushReceiver the app declares. Returns whether one handled the message. - private fun deliverToReceivers(context: Context, message: PushMessage): Boolean { - val intent = Intent(ACTION_MESSAGE).setPackage(context.packageName) - val receivers = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.packageManager.queryBroadcastReceivers(intent, PackageManager.ResolveInfoFlags.of(0)) - } else { - @Suppress("DEPRECATION") - context.packageManager.queryBroadcastReceivers(intent, 0) - } - var handled = false - for (info in receivers) { - val name = info.activityInfo?.name ?: continue - try { - val receiver = Class.forName(name, true, context.classLoader).getDeclaredConstructor().newInstance() - if (receiver is PushReceiver && receiver.onMessage(context, message)) { - handled = true - } - } catch (e: Exception) { - report(e) - } - } - return handled - } - - /** - * Post a notification for [message] that opens the app, with the message in its extras. It shows - * the server's title, body and image from [content], falling back to [title] and the raw payload. - */ - fun notify(context: Context, message: PushMessage, title: String, content: PushNotificationContent = notificationContent(message)) { - val manager = NotificationManagerCompat.from(context) - if (!manager.areNotificationsEnabled()) { - return - } - createChannels(context) - val id = 31 * message.hashCode() + title.hashCode() - val body = content.body ?: message.data.takeIf { !content.present } - val builder = NotificationCompat.Builder(context, CHANNEL_ID) - .setContentTitle(content.title ?: title) - .setSmallIcon(notificationIcon(context)) - .setAutoCancel(true) - if (body != null) { - builder.setContentText(body).setStyle(NotificationCompat.BigTextStyle().bigText(body)) - } - content.image?.let { loadImage(it) }?.let { image -> - builder.setLargeIcon(image) - .setStyle(NotificationCompat.BigPictureStyle().bigPicture(image).bigLargeIcon(null as Bitmap?).setSummaryText(body)) - } - val open = Intent(context, PushOpenActivity::class.java) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_ANIMATION) - .putExtra(EXTRA_TOPIC, message.topic) - .putExtra(EXTRA_PAYLOAD, message.data) - builder.setContentIntent( - PendingIntent.getActivity(context, id, open, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE), - ) - try { - manager.notify(id, builder.build()) - } catch (e: SecurityException) { - // POST_NOTIFICATIONS not granted; nothing to show. - } - } - - // Whether one of the app's activities is visible: a foreground service alone does not count. - private fun appInForeground(): Boolean = ActivityManager.RunningAppProcessInfo() - .also { ActivityManager.getMyMemoryState(it) } - .importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND - - /** The server's `notification` block in [message]: nulls when the payload has none or is not JSON. */ - fun notificationContent(message: PushMessage): PushNotificationContent { - val notification = runCatching { JSONObject(message.data) }.getOrNull()?.optJSONObject("notification") - ?: return PushNotificationContent(false, null, null, null) - fun field(name: String) = (notification.opt(name) as? String)?.takeIf { it.isNotEmpty() } - return PushNotificationContent(true, field("title"), field("body"), field("image")) - } - - // Downloads notification images, so a slow one is abandoned without holding up delivery. - // Acknowledgement timeouts, apart from [worker] so they fire while a run's drain window holds it. - private val ackTimer = Executors.newSingleThreadScheduledExecutor { runnable -> - Thread(runnable, "AppwritePushAck").apply { isDaemon = true } - } - - private val imageLoader = Executors.newCachedThreadPool { runnable -> - Thread(runnable, "AppwritePushImage").apply { isDaemon = true } - } - - // Download a notification image, or null when it cannot be fetched and decoded within - // IMAGE_TIMEOUT_MS overall. On timeout the connection is closed, which ends a read in progress. - private fun loadImage(url: String): Bitmap? { - val connection = runCatching { URL(url).openConnection() as HttpURLConnection }.getOrNull() ?: return null - connection.connectTimeout = IMAGE_TIMEOUT_MS - connection.readTimeout = IMAGE_TIMEOUT_MS - val download = imageLoader.submit { connection.inputStream.use { readLimited(it, IMAGE_MAX_BYTES) }?.let { decodeImage(it) } } - return try { - download.get(IMAGE_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS) - } catch (e: Exception) { - download.cancel(true) - null - } finally { - connection.disconnect() - } - } - - // At most [limit] bytes of [input], or null when it holds more. - private fun readLimited(input: InputStream, limit: Int): ByteArray? { - val out = ByteArrayOutputStream() - val buffer = ByteArray(8_192) - while (true) { - val read = input.read(buffer) - if (read < 0) { - return out.toByteArray() - } - if (out.size() + read > limit) { - return null - } - out.write(buffer, 0, read) - } - } - - // Decode [bytes] downsampled so neither side exceeds IMAGE_MAX_PX, so a large photo cannot - // exhaust memory while a notification is posted. - private fun decodeImage(bytes: ByteArray): Bitmap? { - val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true } - BitmapFactory.decodeByteArray(bytes, 0, bytes.size, bounds) - if (bounds.outWidth <= 0 || bounds.outHeight <= 0) { - return null - } - var sample = 1 - while (bounds.outWidth / sample > IMAGE_MAX_PX || bounds.outHeight / sample > IMAGE_MAX_PX) { - sample *= 2 - } - return BitmapFactory.decodeByteArray(bytes, 0, bytes.size, BitmapFactory.Options().apply { inSampleSize = sample }) - } - - /** The ongoing notification the foreground service shows, on its own quiet channel. */ - fun ongoingNotification(context: Context) = NotificationCompat.Builder(context, SERVICE_CHANNEL_ID) - .also { createChannels(context) } - .setContentTitle(ongoingTitle) - .setContentText("Listening for messages") - .setSmallIcon(notificationIcon(context)) - .setPriority(NotificationCompat.PRIORITY_LOW) - .setOngoing(true) - .build() - - private fun createChannels(context: Context) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - return - } - val manager = context.getSystemService(NotificationManager::class.java) ?: return - manager.createNotificationChannel(NotificationChannel(CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_HIGH)) - manager.createNotificationChannel( - NotificationChannel(SERVICE_CHANNEL_ID, "Background connection", NotificationManager.IMPORTANCE_LOW), - ) - } - - // The icon named by the app's ``, else a - // generic one. - private fun notificationIcon(context: Context): Int { - val metaData = runCatching { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.packageManager.getApplicationInfo(context.packageName, PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong())) - } else { - @Suppress("DEPRECATION") - context.packageManager.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA) - }.metaData - }.getOrNull() - return metaData?.getInt(NOTIFICATION_ICON, 0)?.takeIf { it != 0 } ?: android.R.drawable.stat_notify_chat - } - - private fun ensureService(context: Context) { - if (!wantsService(context)) { - if (serviceRunning) { - context.stopService(Intent(context, PushService::class.java)) - } - return - } - if (serviceRunning) { - return - } - try { - ContextCompat.startForegroundService(context, Intent(context, PushService::class.java)) - } catch (e: Exception) { - // Android 12+ refuses a foreground-service start from the background; the scheduled - // runs keep delivering, and the next start from the foreground succeeds. - if (!serviceStartReported) { - serviceStartReported = true - report(e) - } - } - } - - // Arm the next run in [delayMs]: the job, and an alarm just after it in case the job is late. - // The chained job alternates between two ids, so a run ([runningJobId]) never schedules its - // own id, which would stop it; the other id is cancelled only when it is not the one running. - private fun schedule(context: Context, delayMs: Long, alarmOnly: Boolean = false, runningJobId: Int? = null) { - val interval = interval(context, delayMs) - if (!alarmOnly) { - val next = if (runningJobId == JOB_ID) NEXT_JOB_ID else JOB_ID - if (runningJobId == null) { - context.getSystemService(JobScheduler::class.java)?.cancel(if (next == JOB_ID) NEXT_JOB_ID else JOB_ID) - } - scheduleJob(context, delayMs, next) - } - val alarms = context.getSystemService(AlarmManager::class.java) ?: return - val at = SystemClock.elapsedRealtime() + interval + 1_000L - val intent = tickIntent(context) - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !alarms.canScheduleExactAlarms()) { - alarms.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, at, intent) - } else { - alarms.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, at, intent) - } - } catch (e: SecurityException) { - alarms.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, at, intent) - } - } - - private fun interval(context: Context, delayMs: Long): Long = - if (delayMs == INTERVAL_MS && privileged(context)) PRIVILEGED_INTERVAL_MS else delayMs - - private fun scheduleJob(context: Context, delayMs: Long, id: Int) { - try { - val job = JobInfo.Builder(id, ComponentName(context, PushJobService::class.java)) - .setMinimumLatency(interval(context, delayMs)) - .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) - .setPersisted(true) - .build() - context.getSystemService(JobScheduler::class.java)?.schedule(job) - } catch (e: Exception) { - report(e) - } - } - - // A periodic job, independent of the chain of runs, that restarts the chain when a run was - // dropped (the process killed between runs, a deferred alarm, a throttled job). Scheduled once, - // so its period is not reset by every run. - private fun ensureWatchdog(context: Context) { - val jobs = context.getSystemService(JobScheduler::class.java) ?: return - try { - if (jobs.allPendingJobs.any { it.id == WATCHDOG_JOB_ID }) { - return - } - jobs.schedule( - JobInfo.Builder(WATCHDOG_JOB_ID, ComponentName(context, PushJobService::class.java)) - .setPeriodic(WATCHDOG_INTERVAL_MS) - .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) - .setPersisted(true) - .build(), - ) - } catch (e: Exception) { - report(e) - } - } - - private fun cancelSchedule(context: Context) { - context.getSystemService(JobScheduler::class.java)?.cancel(JOB_ID) - context.getSystemService(JobScheduler::class.java)?.cancel(NEXT_JOB_ID) - context.getSystemService(JobScheduler::class.java)?.cancel(EXPEDITED_JOB_ID) - context.getSystemService(JobScheduler::class.java)?.cancel(WATCHDOG_JOB_ID) - context.getSystemService(AlarmManager::class.java)?.cancel(tickIntent(context)) - } - - private fun tickIntent(context: Context): PendingIntent = PendingIntent.getBroadcast( - context, - 0, - Intent(context, PushAlarmReceiver::class.java).setAction(ACTION_TICK), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, - ) - - /** What background delivery can rely on now; see [PushBackgroundStatus]. */ - fun backgroundStatus(context: Context): PushBackgroundStatus { - val app = context.applicationContext - return PushBackgroundStatus( - exactAlarms = Build.VERSION.SDK_INT < Build.VERSION_CODES.S || - app.getSystemService(AlarmManager::class.java)?.canScheduleExactAlarms() == true, - ignoringBatteryOptimizations = app.getSystemService(PowerManager::class.java)?.isIgnoringBatteryOptimizations(app.packageName) == true, - foregroundService = PushStore.foreground(app), - ) - } - - /** - * Open the system screen where the user allows exact alarms (Android 12+). Returns false when - * there is nothing to ask: already allowed, older Android, or the app does not declare - * `SCHEDULE_EXACT_ALARM`. - */ - fun requestExactAlarms(context: Context): Boolean { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || backgroundStatus(context).exactAlarms) { - return false - } - return openSettings(context, Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM, Manifest.permission.SCHEDULE_EXACT_ALARM) - } - - /** - * Ask the user to exempt the app from battery optimisation. Returns false when there is - * nothing to ask: already exempt, or the app does not declare - * `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS`. - */ - fun requestIgnoreBatteryOptimizations(context: Context): Boolean { - if (backgroundStatus(context).ignoringBatteryOptimizations) { - return false - } - return openSettings( - context, - Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, - Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, - ) - } - - // Open a settings screen for this package when the app declares [permission]. - private fun openSettings(context: Context, action: String, permission: String): Boolean { - val app = context.applicationContext - val declared = runCatching { - @Suppress("DEPRECATION") - app.packageManager.getPackageInfo(app.packageName, PackageManager.GET_PERMISSIONS).requestedPermissions - }.getOrNull()?.contains(permission) == true - if (!declared) { - return false - } - return try { - app.startActivity(Intent(action, Uri.parse("package:${app.packageName}")).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) - true - } catch (e: Exception) { - false - } - } - - // Allowed exact alarms or exempt from battery optimisation: the alarm is reliable, so the - // runs can be spaced further apart. - private fun privileged(context: Context): Boolean = - Build.VERSION.SDK_INT < Build.VERSION_CODES.S || - context.getSystemService(AlarmManager::class.java)?.canScheduleExactAlarms() == true || - context.getSystemService(PowerManager::class.java)?.isIgnoringBatteryOptimizations(context.packageName) == true - - private fun activeNetwork(context: Context): Network? = - runCatching { context.getSystemService(ConnectivityManager::class.java)?.activeNetwork }.getOrNull() - - private fun networkChanged(context: Context): Boolean { - val now = activeNetwork(context) - return now != null && connectedNetwork != null && now != connectedNetwork - } - - // Reconnect as soon as the default network changes (e.g. from cellular to Wi-Fi) while the - // process is alive, instead of waiting for the next run to notice. - private fun registerNetworkCallback(context: Context) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - return - } - synchronized(lock) { - if (networkCallback != null) { - return - } - val callback = object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - worker.execute { - if (mqtt != null && network != connectedNetwork) { - converge(context, heartbeat = false) - } - } - } - } - try { - context.getSystemService(ConnectivityManager::class.java)?.registerDefaultNetworkCallback(callback) - networkCallback = callback - } catch (e: Exception) { - report(e) - } - } - } - - private fun unregisterNetworkCallback(context: Context) { - val callback = synchronized(lock) { networkCallback.also { networkCallback = null } } ?: return - runCatching { context.getSystemService(ConnectivityManager::class.java)?.unregisterNetworkCallback(callback) } - } - - private fun acquireWakeLock(context: Context): PowerManager.WakeLock? = runCatching { - context.getSystemService(PowerManager::class.java) - ?.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Appwrite:push") - ?.apply { - setReferenceCounted(false) - acquire(WAKE_LOCK_MS) - } - }.getOrNull() - - private fun releaseWakeLock(wakeLock: PowerManager.WakeLock?) { - runCatching { - if (wakeLock?.isHeld == true) { - wakeLock.release() - } - } - } -} - -/** - * Background push state in the app's no-backup directory, so a restored backup on another - * device never carries this install's id or credential: the saved subscriptions with their - * connection config (cleared when background delivery stops), and the settings that outlive - * it (the install id, the foreground-mode choice, a refusal waiting for onError). - */ -internal object PushStore { - private const val STATE_FILE = "appwrite_push_state.json" - private const val SETTINGS_FILE = "appwrite_push_settings.json" - - fun loadState(context: Context): Pair>? = synchronized(this) { - val json = read(context, STATE_FILE) ?: return null - runCatching { - val config = json.getJSONObject("config").let { - PushConfig( - host = it.getString("host"), - port = it.getInt("port"), - tls = it.getBoolean("tls"), - tlsInsecure = it.getBoolean("tlsInsecure"), - clientId = it.getString("clientId"), - keepAlive = it.getInt("keepAlive"), - authMethod = it.getString("authMethod"), - credential = it.getString("credential"), - project = it.getString("project"), - sessionCookieUrl = if (it.isNull("sessionCookieUrl")) null else it.optString("sessionCookieUrl").ifEmpty { null }, - ) - } - val list = json.getJSONArray("entries") - val entries = (0 until list.length()).map { index -> - val entry = list.getJSONObject(index) - PushEntry( - filter = entry.getString("filter"), - retry = entry.getBoolean("retry"), - title = if (entry.isNull("title")) null else entry.getString("title"), - notifyInForeground = entry.optBoolean("notifyInForeground", false), - ) - } - config to entries - }.getOrNull() - } - - fun saveState(context: Context, config: PushConfig, entries: List) = synchronized(this) { - val json = JSONObject() - .put( - "config", - JSONObject() - .put("host", config.host) - .put("port", config.port) - .put("tls", config.tls) - .put("tlsInsecure", config.tlsInsecure) - .put("clientId", config.clientId) - .put("keepAlive", config.keepAlive) - .put("authMethod", config.authMethod) - .put("credential", config.credential) - .put("project", config.project) - .put("sessionCookieUrl", config.sessionCookieUrl ?: JSONObject.NULL), - ) - .put( - "entries", - JSONArray(entries.map { JSONObject().put("filter", it.filter).put("retry", it.retry).put("title", it.title ?: JSONObject.NULL).put("notifyInForeground", it.notifyInForeground) }), - ) - write(context, STATE_FILE, json) - } - - fun clearState(context: Context) = synchronized(this) { - file(context, STATE_FILE).delete() - } - - /** A random id for this install, created once, which keeps the broker's replay position. */ - fun installId(context: Context): String = synchronized(this) { - val settings = read(context, SETTINGS_FILE) ?: JSONObject() - settings.optString("installId").ifEmpty { - val id = UUID.randomUUID().toString() - write(context, SETTINGS_FILE, settings.put("installId", id)) - id - } - } - - fun foreground(context: Context): Boolean = synchronized(this) { - read(context, SETTINGS_FILE)?.optBoolean("foreground", false) ?: false - } - - fun setForeground(context: Context, enabled: Boolean) = synchronized(this) { - write(context, SETTINGS_FILE, (read(context, SETTINGS_FILE) ?: JSONObject()).put("foreground", enabled)) - } - - /** The saved adaptive heartbeat; the default interval until one was saved. */ - fun heartbeat(context: Context): PushHeartbeat = synchronized(this) { - val json = read(context, SETTINGS_FILE)?.optJSONObject("heartbeat") ?: return PushHeartbeat(PushBackground.HEARTBEAT_MS) - PushHeartbeat( - intervalMs = json.optLong("intervalMs", PushBackground.HEARTBEAT_MS), - failures = json.optInt("failures", 0), - shrunkAt = json.optLong("shrunkAt", 0L), - ) - } - - fun setHeartbeat(context: Context, state: PushHeartbeat) = synchronized(this) { - val heartbeat = JSONObject() - .put("intervalMs", state.intervalMs) - .put("failures", state.failures) - .put("shrunkAt", state.shrunkAt) - write(context, SETTINGS_FILE, (read(context, SETTINGS_FILE) ?: JSONObject()).put("heartbeat", heartbeat)) - } - - fun setStoppedError(context: Context, message: String) = synchronized(this) { - write(context, SETTINGS_FILE, (read(context, SETTINGS_FILE) ?: JSONObject()).put("stoppedError", message)) - } - - /** The refusal that stopped background delivery while no onError was registered, once. */ - fun takeStoppedError(context: Context): String? = synchronized(this) { - val settings = read(context, SETTINGS_FILE) ?: return null - val message = settings.optString("stoppedError").ifEmpty { return null } - settings.remove("stoppedError") - write(context, SETTINGS_FILE, settings) - message - } - - private fun file(context: Context, name: String) = AtomicFile(File(context.noBackupFilesDir, name)) - - private fun read(context: Context, name: String): JSONObject? = - runCatching { JSONObject(String(file(context, name).readFully(), Charsets.UTF_8)) }.getOrNull() - - private fun write(context: Context, name: String, json: JSONObject) { - val file = file(context, name) - val out = try { - file.startWrite() - } catch (e: IOException) { - Log.e(LOG_TAG, "Could not save push state", e) - return - } - try { - out.write(json.toString().toByteArray(Charsets.UTF_8)) - file.finishWrite(out) - } catch (e: IOException) { - file.failWrite(out) - Log.e(LOG_TAG, "Could not save push state", e) - } - } -} diff --git a/library/src/main/java/io/appwrite/services/PushCore.kt b/library/src/main/java/io/appwrite/services/PushCore.kt deleted file mode 100644 index 15f86b6a..00000000 --- a/library/src/main/java/io/appwrite/services/PushCore.kt +++ /dev/null @@ -1,454 +0,0 @@ -package io.appwrite.services - -import android.content.Context -import android.os.Build -import android.util.Base64 -import com.hivemq.client.mqtt.MqttClient -import com.hivemq.client.mqtt.datatypes.MqttUtf8String -import com.hivemq.client.mqtt.lifecycle.MqttClientDisconnectedContext -import com.hivemq.client.mqtt.lifecycle.MqttDisconnectSource -import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient -import com.hivemq.client.mqtt.mqtt5.Mqtt5ClientConfig -import com.hivemq.client.mqtt.mqtt5.auth.Mqtt5EnhancedAuthMechanism -import com.hivemq.client.mqtt.mqtt5.exceptions.Mqtt5ConnAckException -import com.hivemq.client.mqtt.mqtt5.exceptions.Mqtt5DisconnectException -import com.hivemq.client.mqtt.mqtt5.lifecycle.Mqtt5ClientReconnector -import com.hivemq.client.mqtt.mqtt5.message.auth.Mqtt5Auth -import com.hivemq.client.mqtt.mqtt5.message.auth.Mqtt5AuthBuilder -import com.hivemq.client.mqtt.mqtt5.message.auth.Mqtt5EnhancedAuthBuilder -import com.hivemq.client.mqtt.mqtt5.message.connect.Mqtt5Connect -import com.hivemq.client.mqtt.mqtt5.message.connect.connack.Mqtt5ConnAck -import com.hivemq.client.mqtt.mqtt5.message.connect.connack.Mqtt5ConnAckReasonCode -import com.hivemq.client.mqtt.mqtt5.message.disconnect.Mqtt5Disconnect -import com.hivemq.client.mqtt.mqtt5.message.disconnect.Mqtt5DisconnectReasonCode -import com.hivemq.client.mqtt.mqtt5.message.publish.Mqtt5Publish -import io.appwrite.exceptions.AppwriteException -import org.json.JSONObject -import java.security.KeyStore -import java.security.Provider -import java.security.cert.X509Certificate -import java.util.concurrent.CompletableFuture -import java.util.concurrent.ExecutionException -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicReference -import javax.net.ssl.ManagerFactoryParameters -import javax.net.ssl.TrustManager -import javax.net.ssl.TrustManagerFactory -import javax.net.ssl.TrustManagerFactorySpi -import javax.net.ssl.X509TrustManager - -/** - * A message delivered on a subscribed topic — the MQTT push analog of a realtime event. - */ -data class PushMessage( - val topic: String, - val payload: ByteArray, - val qos: Int, -) { - /** The payload decoded as UTF-8 text, as it was sent. */ - val data: String - get() = String(payload, Charsets.UTF_8) -} - -/** - * What background delivery can rely on. Without exact alarms, the scheduled wake-ups are inexact - * and Doze can defer them, so delivery is [bestEffort] unless foreground mode keeps the - * connection open. - */ -data class PushBackgroundStatus( - /** The app may schedule exact alarms (`SCHEDULE_EXACT_ALARM`, granted). */ - val exactAlarms: Boolean, - /** The app is exempt from battery optimisation. */ - val ignoringBatteryOptimizations: Boolean, - /** Foreground mode (`setForeground(true)`) keeps the connection open in a service. */ - val foregroundService: Boolean, -) { - /** Wake-ups may be deferred by Doze, so messages can arrive late while the app is closed. */ - val bestEffort: Boolean - get() = !exactAlarms && !foregroundService -} - -/** Connection parameters shared between the in-process client and the foreground service. */ -internal data class PushConfig( - val host: String, - val port: Int, - val tls: Boolean, - val tlsInsecure: Boolean, - val clientId: String, - val keepAlive: Int, - val authMethod: String, - val credential: String, - val project: String, - // The endpoint whose `a_session_` cookie in the WebView cookie store holds the app's - // session, when its credential comes from there (React Native). Background runs read it, so - // they keep up with a rotated session while no app code runs. - val sessionCookieUrl: String? = null, -) - -/** - * The userId claim from an Appwrite JWT, used as the default MQTT client id so the - * broker's replay cursor is stable per user. Returns "" when the token is not a decodable JWT - * (e.g. a session secret), so the caller can fall back to the raw credential. - */ -internal fun userIdFromJwt(jwt: String): String { - val parts = jwt.split('.') - if (parts.size < 2) { - return "" - } - return runCatching { - val json = String( - Base64.decode(parts[1], Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING), - Charsets.UTF_8, - ) - JSONObject(json).optString("userId", "") - }.getOrDefault("") -} - -/** The user `id` from an Appwrite session secret, or "" when it is not decodable. */ -internal fun userIdFromSession(session: String): String = runCatching { - val json = String(Base64.decode(session, Base64.DEFAULT), Charsets.UTF_8) - JSONObject(json).optString("id", "") -}.getOrDefault("") - -/** - * The default MQTT client id: one per user and install (`-`, or the install id - * alone when the credential names no user). The broker keeps the replay position per user and - * client id, so each device resumes its own backlog across restarts, and two devices of one user - * do not take over each other's session. - */ -internal fun defaultPushClientId(context: Context, authMethod: String, credential: String): String { - val userId = if (authMethod == "appwrite-session") userIdFromSession(credential) else userIdFromJwt(credential) - val installId = PushStore.installId(context) - return if (userId.isEmpty()) installId else "$userId-$installId" -} - -/** - * Build (but do not connect) an MQTT 5 client from [config]. [onError] receives every connection - * error (a lost connection, a failed automatic reconnect, a broker DISCONNECT) except a refused - * initial CONNECT, which [pushConnect] throws instead. - */ -internal fun buildPushClient( - config: PushConfig, - onConnected: (() -> Unit)? = null, - onDisconnected: (() -> Unit)? = null, - onError: ((Throwable) -> Unit)? = null, - onAuthRefused: ((Throwable) -> Unit)? = null, - credential: () -> String? = { config.credential }, - retired: AtomicBoolean? = null, -): Mqtt5AsyncClient { - // config.clientId is stable per user (see buildConfig), so a reconnect rebuild reuses it - // and resumes the same session. - var builder = MqttClient.builder() - .useMqttVersion5() - .serverHost(config.host) - .serverPort(config.port) - // Recover from an unexpected disconnect (network drop, broker restart) with HiveMQ's - // exponential backoff. A user-initiated disconnect() (see close()) does not trigger it. - // Because clean start is off and the broker resumes the same session, subscriptions and - // the QoS-1 replay cursor persist, so delivery continues without resubscribing (which - // would stack a second per-subscription callback). - .automaticReconnectWithDefaultConfig() - - if (config.clientId.isNotEmpty()) { - builder = builder.identifier(config.clientId) - } - - // Whether this client has connected yet: until it has, a refused CONNECT is final. - val connected = AtomicBoolean(false) - val client = AtomicReference() - builder = builder.addConnectedListener { - // A reconnect that was already under way when the client was retired disconnects at once, - // so it never competes with its replacement for the broker session. - if (retired?.get() == true) { - client.get()?.disconnect() - return@addConnectedListener - } - connected.set(true) - onConnected?.invoke() - } - builder = builder.addDisconnectedListener { context -> - // A client its owner replaced or closed ([retired]) never reconnects: one still retrying - // in the background would otherwise come back as a second connection for the same id. - if (retired?.get() == true) { - context.reconnector.reconnect(false) - onDisconnected?.invoke() - return@addDisconnectedListener - } - // A refused first CONNECT (bad credential, rate limit) is final: stop the automatic - // reconnect so pushConnect fails with the broker's reason instead of retrying forever. - val refused = !connected.get() && context.cause is Mqtt5ConnAckException - // With [onAuthRefused], a credential refused later (a failed automatic reconnect or - // re-auth) is final too, instead of being retried with the same credential forever. - val authRefused = onAuthRefused != null && connected.get() && - context.source != MqttDisconnectSource.USER && isAuthRefusal(context.cause) - // Authentication was aborted because the app has no current credential (signed out): there - // is nothing to reconnect with, so the attempt fails now instead of retrying until timeout. - val noCredential = generateSequence(context.cause) { it.cause }.any { it is PushNoCredentialException } - if (refused || authRefused || noCredential) { - context.reconnector.reconnect(false) - } else if (context.reconnector.isReconnect) { - // HiveMQ's automatic reconnect sends a CONNECT it builds itself, without user - // properties: the broker then misses the project and refuses the credential. Reconnect - // with the same CONNECT this client connects with instead. - (context.reconnector as? Mqtt5ClientReconnector)?.connect(pushConnectMessage(config, credential)) - } - onDisconnected?.invoke() - if (!refused) { - val error = pushDisconnectError(context) - if (authRefused && error != null) { - // Reported by onAuthRefused, which also stops for good. - onAuthRefused?.invoke(error) - } else { - error?.let { onError?.invoke(it) } - } - } - } - - if (config.tls) { - builder = if (config.tlsInsecure) { - builder.sslConfig() - .trustManagerFactory(PushInsecureTrustManagerFactory) - .hostnameVerifier { _, _ -> true } - .applySslConfig() - } else { - builder.sslWithDefaultConfig() - } - } - - return builder.buildAsync().also { client.set(it) } -} - -/** - * Connect with Appwrite enhanced auth; blocks until CONNACK. Throws an - * [AppwriteException] on failure, whose message is the broker's MQTT 5 - * Reason String when it refused the CONNECT with one. - */ -internal fun Mqtt5AsyncClient.pushConnect(config: PushConfig) { - try { - sendPushConnect(config) - } catch (e: Exception) { - throw connectError((e as? ExecutionException)?.cause ?: e) - } -} - -/** The error a failed CONNECT reports: the broker's Reason String when it refused one. */ -internal fun connectError(cause: Throwable): AppwriteException = - if (cause is Mqtt5ConnAckException) { - connAckError(cause) - } else { - AppwriteException(cause.message ?: "MQTT connection failed") - } - -/** - * Whether [error] is the broker refusing the credential (a CONNECT or re-auth), which retrying - * with the same credential cannot fix, unlike a rate limit or a network failure. - */ -internal fun isAuthRefusal(error: Throwable?): Boolean = when (error) { - is Mqtt5ConnAckException -> error.mqttMessage.reasonCode in setOf( - Mqtt5ConnAckReasonCode.NOT_AUTHORIZED, - Mqtt5ConnAckReasonCode.BAD_USER_NAME_OR_PASSWORD, - Mqtt5ConnAckReasonCode.BAD_AUTHENTICATION_METHOD, - Mqtt5ConnAckReasonCode.BANNED, - ) - is Mqtt5DisconnectException -> error.mqttMessage.reasonCode == Mqtt5DisconnectReasonCode.NOT_AUTHORIZED - else -> false -} - -/** - * Send the CONNECT and wait for the CONNACK, at most [timeoutSeconds] when given. [credential] - * supplies the credential each time one is sent, including on the client's automatic reconnects - * and re-auths, so they can use a session rotated since this CONNECT; null aborts the attempt. - */ -internal fun Mqtt5AsyncClient.sendPushConnect( - config: PushConfig, - timeoutSeconds: Long? = null, - credential: () -> String? = { config.credential }, -) { - val connAck = connect(pushConnectMessage(config, credential)) - if (timeoutSeconds == null) { - connAck.get() - } else { - connAck.get(timeoutSeconds, TimeUnit.SECONDS) - } -} - -/** - * The CONNECT for [config]: enhanced auth with the credential from [credential], and the project - * as a user property, which the broker resolves the credential against. - */ -internal fun pushConnectMessage(config: PushConfig, credential: () -> String?): Mqtt5Connect = - Mqtt5Connect.builder() - .enhancedAuth(PushAuthMechanism(config.authMethod, config.project, credential)) - // Clean start is always off, so the broker keeps this client's session and can - // redeliver missed messages to QoS-1 subscriptions on reconnect. - .cleanStart(false) - .keepAlive(config.keepAlive) - .userProperties() - .add("projectId", config.project) - .applyUserProperties() - .build() - -/** - * The error a lost connection reports, or null when it is not an error (a disconnect the app - * asked for, or a broker DISCONNECT with a success reason code). The broker explains a refused - * CONNECT or a DISCONNECT it sends with an MQTT 5 Reason String, which becomes the message. - */ -internal fun pushDisconnectError(context: MqttClientDisconnectedContext): Throwable? { - val cause = context.cause - return when { - context.source == MqttDisconnectSource.USER -> null - cause is Mqtt5ConnAckException -> connAckError(cause) - cause is Mqtt5DisconnectException && context.source == MqttDisconnectSource.SERVER -> { - val disconnect = cause.mqttMessage - if (disconnect.reasonCode.isError) { - AppwriteException( - disconnect.reasonString.map { it.toString() } - .orElse("Disconnected by the broker (reason ${disconnect.reasonCode})"), - ) - } else { - null - } - } - else -> cause - } -} - -/** A refused CONNECT, with the CONNACK Reason String as its message when the broker sent one. */ -internal fun connAckError(e: Mqtt5ConnAckException): AppwriteException = - AppwriteException( - e.mqttMessage.reasonString.map { it.toString() }.orElse(e.message ?: "MQTT connection refused"), - ) - -internal fun Mqtt5Publish.toPushMessage(): PushMessage = - PushMessage( - topic = topic.toString(), - payload = payloadAsBytes, - qos = qos.code, - ) - -/** - * Single-step MQTT 5 enhanced auth: the method and credential are placed in the CONNECT - * packet and the broker accepts them in the CONNACK, with no AUTH round-trip. The credential is - * read from [credential] each time, so an automatic reconnect or re-auth sends the current one; - * when it has none, the attempt fails instead of sending a credential known to be stale. - */ -internal class PushAuthMechanism( - private val method: String, - private val project: String, - private val credential: () -> String?, -) : Mqtt5EnhancedAuthMechanism { - - override fun getMethod(): MqttUtf8String = MqttUtf8String.of(method) - - override fun getTimeout(): Int = 30 - - override fun onAuth( - clientConfig: Mqtt5ClientConfig, - connect: Mqtt5Connect, - authBuilder: Mqtt5EnhancedAuthBuilder, - ): CompletableFuture { - val current = credential() ?: return failedAuth() - authBuilder.data(current.toByteArray(Charsets.UTF_8)) - return CompletableFuture.completedFuture(null) - } - - override fun onReAuth( - clientConfig: Mqtt5ClientConfig, - authBuilder: Mqtt5AuthBuilder, - ): CompletableFuture { - val current = credential() ?: return failedAuth() - // The broker resolves a re-auth against the project in its user properties too. - authBuilder.data(current.toByteArray(Charsets.UTF_8)) - .userProperties() - .add("projectId", project) - .applyUserProperties() - return CompletableFuture.completedFuture(null) - } - - override fun onContinue( - clientConfig: Mqtt5ClientConfig, - auth: Mqtt5Auth, - authBuilder: Mqtt5AuthBuilder, - ): CompletableFuture = CompletableFuture.completedFuture(true) - - override fun onAuthSuccess( - clientConfig: Mqtt5ClientConfig, - connAck: Mqtt5ConnAck, - ): CompletableFuture = CompletableFuture.completedFuture(true) - - override fun onReAuthSuccess( - clientConfig: Mqtt5ClientConfig, - auth: Mqtt5Auth, - ): CompletableFuture = CompletableFuture.completedFuture(true) - - override fun onAuthRejected(clientConfig: Mqtt5ClientConfig, connAck: Mqtt5ConnAck) = Unit - - override fun onReAuthRejected(clientConfig: Mqtt5ClientConfig, disconnect: Mqtt5Disconnect) = Unit - - override fun onAuthError(clientConfig: Mqtt5ClientConfig, cause: Throwable) = Unit - - override fun onReAuthError(clientConfig: Mqtt5ClientConfig, cause: Throwable) = Unit - - private fun failedAuth(): CompletableFuture = - CompletableFuture().apply { completeExceptionally(PushNoCredentialException()) } -} - -/** Authentication aborted because there is no current credential to send (the app signed out). */ -internal class PushNoCredentialException : IllegalStateException("No current credential to authenticate with") - -/** - * A [TrustManagerFactory] that accepts any certificate — used when `tlsInsecure` is set - * (for a broker whose certificate does not chain to a public root). - */ -internal object PushInsecureTrustManagerFactory : TrustManagerFactory( - object : TrustManagerFactorySpi() { - override fun engineInit(keyStore: KeyStore?) = Unit - - override fun engineInit(parameters: ManagerFactoryParameters?) = Unit - - override fun engineGetTrustManagers(): Array = arrayOf( - object : X509TrustManager { - override fun checkClientTrusted(chain: Array?, authType: String?) = Unit - - override fun checkServerTrusted(chain: Array?, authType: String?) = Unit - - override fun getAcceptedIssuers(): Array = emptyArray() - }, - ) - }, - @Suppress("DEPRECATION") object : Provider("AppwriteInsecure", 1.0, "insecure trust-all") {}, - "AppwriteInsecure", -) - -/** A live in-app subscription hosted in the background: its topics, callback, notification opt-in and QoS. */ -internal class PushListener( - val topics: Set, - val callback: (PushMessage) -> Unit, - val background: Boolean, - val title: String?, - val retry: Boolean, - // Called instead of [callback] when set, with the acknowledgement to run once the message was - // handled: the React Native and Flutter bridges acknowledge after their callback has run. - val acknowledgingCallback: ((PushMessage, () -> Unit) -> Unit)? = null, - // Also post a background notification while the app is visible. - val notifyInForeground: Boolean = false, -) - -/** MQTT topic-filter match with '+' (single level) and '#' (multi level). */ -internal fun matchesTopic(filter: String, topic: String): Boolean { - val filterParts = filter.split('/') - val topicParts = topic.split('/') - for (i in filterParts.indices) { - if (filterParts[i] == "#") { - return true - } - if (i >= topicParts.size) { - return false - } - if (filterParts[i] != "+" && filterParts[i] != topicParts[i]) { - return false - } - } - return filterParts.size == topicParts.size -} diff --git a/library/src/main/java/io/appwrite/services/PushOpenActivity.kt b/library/src/main/java/io/appwrite/services/PushOpenActivity.kt deleted file mode 100644 index 354ca6f0..00000000 --- a/library/src/main/java/io/appwrite/services/PushOpenActivity.kt +++ /dev/null @@ -1,57 +0,0 @@ -package io.appwrite.services - -import android.app.Activity -import android.content.Intent -import android.os.Bundle - -class PushOpenActivity : Activity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val topic = intent.getStringExtra(PushBackground.EXTRA_TOPIC) - val payload = intent.getStringExtra(PushBackground.EXTRA_PAYLOAD) - packageManager.getLaunchIntentForPackage(packageName)?.let { launch -> - launch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP) - if (topic != null && payload != null) { - launch.putExtra(PushBackground.EXTRA_TOPIC, topic).putExtra(PushBackground.EXTRA_PAYLOAD, payload) - } - startActivity(launch) - } - // After opening the app, so a screen a tap callback opens lands on top of it. - if (topic != null && payload != null) { - PushTaps.record(PushTap(topic, payload)) - } - finish() - } -} - -internal data class PushTap(val topic: String, val payload: String) - -internal object PushTaps { - private var pending: PushTap? = null - private val listeners = mutableListOf<(PushTap) -> Unit>() - - fun record(tap: PushTap) { - val deliver = synchronized(this) { - listeners.toList().also { - if (it.isEmpty()) { - pending = tap - } - } - } - deliver.forEach { it(tap) } - } - - @Synchronized - fun take(): PushTap? = pending.also { pending = null } - - fun listen(onTap: (PushTap) -> Unit): () -> Unit { - synchronized(this) { - listeners.add(onTap) - } - return { - synchronized(this) { - listeners.remove(onTap) - } - } - } -} diff --git a/library/src/main/java/io/appwrite/services/PushReceiver.kt b/library/src/main/java/io/appwrite/services/PushReceiver.kt deleted file mode 100644 index 92d04727..00000000 --- a/library/src/main/java/io/appwrite/services/PushReceiver.kt +++ /dev/null @@ -1,33 +0,0 @@ -package io.appwrite.services - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent - -/** - * Handles background push messages in code when no in-app callback is listening, e.g. after - * the process was started in the background to reconnect. Subclass it and declare it in your - * manifest with the push message action: - * - * ```xml - * - * - * - * - * - * ``` - * - * The SDK calls [onMessage] directly on a background thread, under a wakelock of about ten - * seconds, so hand longer work to WorkManager. Messages a live callback receives do not - * reach it. - */ -abstract class PushReceiver : BroadcastReceiver() { - /** - * Handle [message]. Return true when you handled it yourself, so the SDK does not post its - * default notification; return false to have it posted as well. - */ - abstract fun onMessage(context: Context, message: PushMessage): Boolean - - // Messages arrive through onMessage, never as a broadcast. - final override fun onReceive(context: Context, intent: Intent) = Unit -} diff --git a/library/src/main/java/io/appwrite/services/PushService.kt b/library/src/main/java/io/appwrite/services/PushService.kt deleted file mode 100644 index e2a05f89..00000000 --- a/library/src/main/java/io/appwrite/services/PushService.kt +++ /dev/null @@ -1,67 +0,0 @@ -package io.appwrite.services - -import android.app.Service -import android.content.Intent -import android.content.pm.ServiceInfo -import android.os.Build -import android.os.IBinder -import androidx.core.app.ServiceCompat - -/** - * The foreground mode of background push delivery ([Push.setForeground]): a foreground service - * that keeps the process at foreground priority, so the connection held by the process stays - * up and delivers immediately, also during Doze. The system restarts it after a kill, and it - * restores the saved subscriptions. Started by the SDK; not meant to be started directly. - */ -class PushService : Service() { - override fun onBind(intent: Intent?): IBinder? = null - - override fun onCreate() { - super.onCreate() - PushBackground.serviceRunning = true - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - if (!PushBackground.wantsService(this)) { - stopSelf() - return START_NOT_STICKY - } - try { - startForegroundCompat() - } catch (e: Exception) { - // E.g. a background start refused on Android 12+; the scheduled runs keep delivering. - stopSelf() - PushBackground.report(e) - return START_NOT_STICKY - } - PushBackground.tick(this) {} - // Recreated after a kill with a null intent; the saved subscriptions are restored then. - return START_STICKY - } - - private fun startForegroundCompat() { - val notification = PushBackground.ongoingNotification(this) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - ServiceCompat.startForeground( - this, - PushBackground.NOTIFICATION_ID, - notification, - ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING, - ) - } else { - startForeground(PushBackground.NOTIFICATION_ID, notification) - } - } - - // Some devices kill the process shortly after its task is swiped away; a run a few seconds - // later restarts the service. - override fun onTaskRemoved(rootIntent: Intent?) { - PushBackground.scheduleRestart(this) - super.onTaskRemoved(rootIntent) - } - - override fun onDestroy() { - PushBackground.serviceRunning = false - super.onDestroy() - } -} diff --git a/library/src/main/java/io/appwrite/services/PushWakeups.kt b/library/src/main/java/io/appwrite/services/PushWakeups.kt deleted file mode 100644 index c6d56165..00000000 --- a/library/src/main/java/io/appwrite/services/PushWakeups.kt +++ /dev/null @@ -1,55 +0,0 @@ -package io.appwrite.services - -import android.app.job.JobParameters -import android.app.job.JobService -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent - -/** - * The scheduled run of background push delivery: reconnects and re-arms itself. Scheduled - * by the SDK; not meant to be started directly. - */ -class PushJobService : JobService() { - override fun onStartJob(params: JobParameters): Boolean { - // A chained job arms the next under the other chained id; the watchdog and an expedited - // run (handed over by the alarm) only run once. Each stays up while missed messages arrive. - val chained = params.jobId.takeIf { it == PushBackground.JOB_ID || it == PushBackground.NEXT_JOB_ID } - PushBackground.tick(this, chained, PushBackground.JOB_DRAIN_MS) { jobFinished(params, false) } - return true - } - - // The run re-arms the next one itself, and the watchdog repeats on its own, so a stopped run - // needs no retry. - override fun onStopJob(params: JobParameters): Boolean = false -} - -/** - * The alarm that backs up the scheduled job when it runs late. Scheduled by the SDK; not meant - * to be sent directly. - */ -class PushAlarmReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (intent.action != PushBackground.ACTION_TICK) { - return - } - // On Android 12+ the run goes to an expedited job, which keeps the process runnable while it - // connects and the broker replays; it runs here when the system refuses one. - if (PushBackground.runExpedited(context)) { - return - } - val pending = goAsync() - PushBackground.tick(context, drainMs = PushBackground.RECEIVER_DRAIN_MS) { pending.finish() } - } -} - -/** Resumes background push delivery after a reboot or an app update. */ -class PushBootReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (intent.action != Intent.ACTION_BOOT_COMPLETED && intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) { - return - } - val pending = goAsync() - PushBackground.tick(context) { pending.finish() } - } -} diff --git a/library/src/main/java/io/appwrite/services/Teams.kt b/library/src/main/java/io/appwrite/services/Teams.kt index 7227eba6..8a4bb87d 100644 --- a/library/src/main/java/io/appwrite/services/Teams.kt +++ b/library/src/main/java/io/appwrite/services/Teams.kt @@ -539,6 +539,7 @@ class Teams(client: Client) : Service(client) { * @param phone Phone number. Format this number with a leading '+' and a country code, e.g., +16175551212. * @param url URL to redirect the user back to your app from the invitation email. This parameter is not required when an API key is supplied. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param name Name of the new team member. Max length: 128 chars. + * @param membershipId Membership ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars. * @return [io.appwrite.models.Membership] */ @JvmOverloads @@ -550,6 +551,7 @@ class Teams(client: Client) : Service(client) { phone: String? = null, url: String? = null, name: String? = null, + membershipId: String? = null, ): io.appwrite.models.Membership { if (teamId.isEmpty()) { throw AppwriteException("Missing required parameter: \"teamId\"") @@ -564,6 +566,7 @@ class Teams(client: Client) : Service(client) { "roles" to roles, "url" to url, "name" to name, + "membershipId" to membershipId, ) val apiHeaders = mutableMapOf( "X-Appwrite-Project" to client.config["project"].orEmpty(), diff --git a/library/src/test/java/io/appwrite/TopicTest.kt b/library/src/test/java/io/appwrite/TopicTest.kt deleted file mode 100644 index 0dd65bf0..00000000 --- a/library/src/test/java/io/appwrite/TopicTest.kt +++ /dev/null @@ -1,53 +0,0 @@ -package io.appwrite - -import org.junit.Assert.assertEquals -import org.junit.Assert.assertThrows -import org.junit.Test - -class TopicTest { - private fun assertPathFails(levels: List) { - assertThrows(IllegalArgumentException::class.java) { Topic.path(levels) } - assertThrows(IllegalArgumentException::class.java) { Topic.any().path(levels) } - } - - @Test - fun pathJoinsLevels() { - assertEquals("user/123/notification", Topic.path(listOf("user", "123", "notification")).toString()) - } - - @Test - fun pathAppendsLevels() { - assertEquals( - "org/42/user/123/notification", - Topic.path(listOf("org", "42", "user", "123")).path(listOf("notification")).toString(), - ) - } - - @Test - fun anyAppendsSingleLevelWildcard() { - assertEquals("user/+/notification", Topic.path(listOf("user")).any().path(listOf("notification")).toString()) - assertEquals("chat/+/+/message", Topic.path(listOf("chat")).any().any().path(listOf("message")).toString()) - } - - @Test - fun allEndsTopic() { - assertEquals("org/+/logs/#", Topic.path(listOf("org")).any().path(listOf("logs")).all().toString()) - } - - @Test - fun staticAnyAndAll() { - assertEquals("+/notification", Topic.any().path(listOf("notification")).toString()) - assertEquals("#", Topic.all().toString()) - } - - @Test - fun pathRejectsInvalidLevels() { - assertPathFails(emptyList()) - assertPathFails(listOf("user", "")) - assertPathFails(listOf("user/123")) - assertPathFails(listOf("+")) - assertPathFails(listOf("user", "a+b")) - assertPathFails(listOf("user", "#")) - assertPathFails(listOf("a#b")) - } -} From d35f30684ab5dc5a902f7e2f672bcab785c2a754 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 14 Oct 2026 18:22:39 +0100 Subject: [PATCH 2/4] feat: SDK update for the 2.5.x majors --- CHANGELOG.md | 37 ++- README.md | 21 +- .../java/account/create-o-auth-2-session.md | 1 + .../java/account/create-o-auth-2-token.md | 1 + .../java/account/create-passkey-token.md | 22 ++ docs/examples/java/account/create-passkey.md | 27 ++ docs/examples/java/account/delete-passkey.md | 26 ++ docs/examples/java/account/get-passkey.md | 26 ++ docs/examples/java/account/list-passkeys.md | 27 ++ .../java/account/update-passkey-token.md | 27 ++ .../account/update-passkey-verification.md | 27 ++ docs/examples/java/account/update-passkey.md | 27 ++ docs/examples/java/analytics/create-event.md | 38 +++ docs/examples/java/oauth2/authorize-post.md | 2 +- docs/examples/java/oauth2/authorize.md | 2 +- .../oauth2/create-device-authorization.md | 2 +- docs/examples/java/oauth2/create-par.md | 2 +- docs/examples/java/oauth2/create-token.md | 2 +- docs/examples/java/teams/create-membership.md | 1 + .../kotlin/account/create-o-auth-2-session.md | 1 + .../kotlin/account/create-o-auth-2-token.md | 1 + .../kotlin/account/create-passkey-token.md | 13 + .../examples/kotlin/account/create-passkey.md | 16 + .../examples/kotlin/account/delete-passkey.md | 15 + docs/examples/kotlin/account/get-passkey.md | 15 + docs/examples/kotlin/account/list-passkeys.md | 16 + .../kotlin/account/update-passkey-token.md | 16 + .../account/update-passkey-verification.md | 16 + .../examples/kotlin/account/update-passkey.md | 16 + .../examples/kotlin/analytics/create-event.md | 27 ++ docs/examples/kotlin/oauth2/authorize-post.md | 2 +- docs/examples/kotlin/oauth2/authorize.md | 2 +- .../oauth2/create-device-authorization.md | 2 +- docs/examples/kotlin/oauth2/create-par.md | 2 +- docs/examples/kotlin/oauth2/create-token.md | 2 +- .../kotlin/teams/create-membership.md | 1 + library/src/main/java/io/appwrite/Client.kt | 4 +- .../main/java/io/appwrite/models/Account.kt | 30 +- .../java/io/appwrite/models/AlgoArgon2.kt | 8 +- .../java/io/appwrite/models/AlgoBcrypt.kt | 2 +- .../main/java/io/appwrite/models/AlgoMd5.kt | 2 +- .../java/io/appwrite/models/AlgoPhpass.kt | 2 +- .../java/io/appwrite/models/AlgoScrypt.kt | 10 +- .../io/appwrite/models/AlgoScryptModified.kt | 8 +- .../main/java/io/appwrite/models/AlgoSha.kt | 2 +- .../src/main/java/io/appwrite/models/App.kt | 52 ++-- .../io/appwrite/models/AppInstallation.kt | 18 +- .../io/appwrite/models/AppInstallationList.kt | 4 +- .../main/java/io/appwrite/models/AppKey.kt | 16 +- .../java/io/appwrite/models/AppKeyList.kt | 4 +- .../main/java/io/appwrite/models/AppScope.kt | 10 +- .../java/io/appwrite/models/AppScopeList.kt | 4 +- .../main/java/io/appwrite/models/AppSecret.kt | 16 +- .../java/io/appwrite/models/AppSecretList.kt | 4 +- .../io/appwrite/models/AppSecretPlaintext.kt | 16 +- .../main/java/io/appwrite/models/AppsList.kt | 4 +- .../main/java/io/appwrite/models/Continent.kt | 4 +- .../java/io/appwrite/models/ContinentList.kt | 4 +- .../main/java/io/appwrite/models/Country.kt | 4 +- .../java/io/appwrite/models/CountryList.kt | 4 +- .../main/java/io/appwrite/models/Currency.kt | 14 +- .../java/io/appwrite/models/CurrencyList.kt | 4 +- .../main/java/io/appwrite/models/Document.kt | 14 +- .../java/io/appwrite/models/DocumentList.kt | 4 +- .../main/java/io/appwrite/models/Execution.kt | 32 +- .../java/io/appwrite/models/ExecutionList.kt | 4 +- .../src/main/java/io/appwrite/models/File.kt | 32 +- .../main/java/io/appwrite/models/FileList.kt | 4 +- .../main/java/io/appwrite/models/Headers.kt | 4 +- .../main/java/io/appwrite/models/Identity.kt | 22 +- .../java/io/appwrite/models/IdentityList.kt | 4 +- .../src/main/java/io/appwrite/models/Jwt.kt | 2 +- .../main/java/io/appwrite/models/Language.kt | 6 +- .../java/io/appwrite/models/LanguageList.kt | 4 +- .../main/java/io/appwrite/models/Locale.kt | 14 +- .../java/io/appwrite/models/LocaleCode.kt | 4 +- .../java/io/appwrite/models/LocaleCodeList.kt | 4 +- .../java/io/appwrite/models/Membership.kt | 30 +- .../java/io/appwrite/models/MembershipList.kt | 4 +- .../java/io/appwrite/models/MfaChallenge.kt | 8 +- .../java/io/appwrite/models/MfaFactors.kt | 10 +- .../io/appwrite/models/MfaRecoveryCodes.kt | 2 +- .../main/java/io/appwrite/models/MfaType.kt | 4 +- .../java/io/appwrite/models/Oauth2Approve.kt | 2 +- .../io/appwrite/models/Oauth2Authorize.kt | 4 +- .../java/io/appwrite/models/Oauth2Consent.kt | 20 +- .../io/appwrite/models/Oauth2ConsentList.kt | 4 +- .../io/appwrite/models/Oauth2ConsentToken.kt | 22 +- .../appwrite/models/Oauth2ConsentTokenList.kt | 4 +- .../models/Oauth2DeviceAuthorization.kt | 12 +- .../java/io/appwrite/models/Oauth2Grant.kt | 24 +- .../io/appwrite/models/Oauth2Organization.kt | 2 +- .../appwrite/models/Oauth2OrganizationList.kt | 4 +- .../main/java/io/appwrite/models/Oauth2PAR.kt | 4 +- .../java/io/appwrite/models/Oauth2Project.kt | 6 +- .../io/appwrite/models/Oauth2ProjectList.kt | 4 +- .../java/io/appwrite/models/Oauth2Reject.kt | 2 +- .../java/io/appwrite/models/Oauth2Token.kt | 10 +- .../main/java/io/appwrite/models/Passkey.kt | 61 ++++ .../io/appwrite/models/PasskeyChallenge.kt | 61 ++++ .../java/io/appwrite/models/PasskeyList.kt | 37 +++ .../src/main/java/io/appwrite/models/Phone.kt | 6 +- .../main/java/io/appwrite/models/PhoneList.kt | 4 +- .../main/java/io/appwrite/models/Presence.kt | 12 +- .../java/io/appwrite/models/PresenceList.kt | 4 +- .../src/main/java/io/appwrite/models/Row.kt | 14 +- .../main/java/io/appwrite/models/RowList.kt | 4 +- .../main/java/io/appwrite/models/Session.kt | 58 ++-- .../java/io/appwrite/models/SessionList.kt | 4 +- .../java/io/appwrite/models/Subscriber.kt | 16 +- .../main/java/io/appwrite/models/Target.kt | 16 +- .../src/main/java/io/appwrite/models/Team.kt | 10 +- .../main/java/io/appwrite/models/TeamList.kt | 4 +- .../src/main/java/io/appwrite/models/Token.kt | 12 +- .../java/io/appwrite/models/TokenSecret.kt | 69 +++++ .../java/io/appwrite/models/Transaction.kt | 12 +- .../io/appwrite/models/TransactionList.kt | 4 +- .../src/main/java/io/appwrite/models/User.kt | 30 +- .../main/java/io/appwrite/services/Account.kt | 289 +++++++++++++++++- .../java/io/appwrite/services/Analytics.kt | 83 +++++ .../main/java/io/appwrite/services/Oauth2.kt | 10 +- .../main/java/io/appwrite/services/Push.kt | 12 +- .../io/appwrite/services/PushBackground.kt | 35 ++- .../java/io/appwrite/services/PushCore.kt | 25 +- .../main/java/io/appwrite/services/Teams.kt | 3 + 125 files changed, 1502 insertions(+), 460 deletions(-) create mode 100644 docs/examples/java/account/create-passkey-token.md create mode 100644 docs/examples/java/account/create-passkey.md create mode 100644 docs/examples/java/account/delete-passkey.md create mode 100644 docs/examples/java/account/get-passkey.md create mode 100644 docs/examples/java/account/list-passkeys.md create mode 100644 docs/examples/java/account/update-passkey-token.md create mode 100644 docs/examples/java/account/update-passkey-verification.md create mode 100644 docs/examples/java/account/update-passkey.md create mode 100644 docs/examples/java/analytics/create-event.md create mode 100644 docs/examples/kotlin/account/create-passkey-token.md create mode 100644 docs/examples/kotlin/account/create-passkey.md create mode 100644 docs/examples/kotlin/account/delete-passkey.md create mode 100644 docs/examples/kotlin/account/get-passkey.md create mode 100644 docs/examples/kotlin/account/list-passkeys.md create mode 100644 docs/examples/kotlin/account/update-passkey-token.md create mode 100644 docs/examples/kotlin/account/update-passkey-verification.md create mode 100644 docs/examples/kotlin/account/update-passkey.md create mode 100644 docs/examples/kotlin/analytics/create-event.md create mode 100644 library/src/main/java/io/appwrite/models/Passkey.kt create mode 100644 library/src/main/java/io/appwrite/models/PasskeyChallenge.kt create mode 100644 library/src/main/java/io/appwrite/models/PasskeyList.kt create mode 100644 library/src/main/java/io/appwrite/models/TokenSecret.kt create mode 100644 library/src/main/java/io/appwrite/services/Analytics.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 21f1b0c0..cc5c4d6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,29 +1,28 @@ # Change Log -## 29.1.0-rc.3 +## 30.0.0 -* Added: first background subscription auto-requests `POST_NOTIFICATIONS` (Android 13+) from the visible activity -* Fixed: `onOpen`/`onClose` callbacks fire correctly when joining or leaving the background host -* Fixed: notification taps are recorded after launching the app, so tap-opened screens land on top +* Breaking: requests send `x-appwrite-response-format: 2.5.0`; responses follow the 2.5 shapes +* Breaking: missing response fields map to their documented defaults +* Breaking: `oauth2` `resource` params now take `List` on the authorize, PAR, device and token methods +* Breaking: `Push.subscribe` takes `notifyInForeground` before the callback; Java callers must pass it +* Added: `notifyInForeground` option on `Push.subscribe` +* Added: `PushOpenActivity` for opening notifications +* Fixed: a file exactly `CHUNK_SIZE` bytes long uploads in a single request -## 29.1.0-rc.2 +## 29.2.0 -* Added: `getInitialNotification()`, `onNotificationOpened()`, and the `PushNotificationOpened` data class (topic + data) -* Added: `onOpen`/`onClose` callbacks now fire for background push connections -* Added: push auth falls back to the client's cookie-store session -* Fixed: a foreground message with no live callback now still posts a notification -* Updated: notification taps route through an internal `PushOpenActivity` +* Added: `analytics.createEvent` in a new `Analytics` service to send tracking events +* Added: optional `state` on `account.createOAuth2Session` and `account.createOAuth2Token`, returned unchanged on the success and failure URLs +* Added: optional `membershipId` on `teams.createMembership` +* Fixed: `avatars.updatePhoto` sends `PUT`, so uploading a profile photo no longer returns 404 +* Fixed: OAuth2 query parameters (`success`, `failure`, `scopes`, `state`) are URL-encoded, so values containing `&`, `+` or `#` survive the redirect -## 29.1.0-rc.1 +## 29.1.0 -* Updated: regenerated with sdk-generator 5.5.0; no SDK code changes - -## 29.1.0-rc.0 - -* Added: optional `method` param on `Client.chunkedUpload()` to set the upload HTTP method -* Added: background push notifications render the server `notification` title, body, and image -* Updated: `Avatars.updatePhoto()` uploads via `PUT` -* Fixed: an exactly-chunk-sized file uploads in a single request +* Added: `account` passkey methods `listPasskeys`, `createPasskey`, `getPasskey`, `updatePasskey`, `deletePasskey` and `updatePasskeyVerification` +* Added: `account.createPasskeyToken` and `account.updatePasskeyToken` for passkey sign-in +* Added: `Passkey`, `PasskeyList`, `PasskeyChallenge` and `TokenSecret` models ## 29.0.0 diff --git a/README.md b/README.md index afcc18e1..8e5ff5c1 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ ![Maven Central](https://img.shields.io/maven-central/v/io.appwrite/sdk-for-android.svg?color=green&style=flat-square) ![License](https://img.shields.io/github/license/appwrite/sdk-for-android.svg?style=flat-square) -![Version](https://img.shields.io/badge/api%20version-2.3.0-blue.svg?style=flat-square) +![Version](https://img.shields.io/badge/api%20version-2.5.0-blue.svg?style=flat-square) [![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator) [![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite) [![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord) -**This SDK targets Appwrite server version 2.3.x as shipped on Appwrite Cloud.** Self-hosted releases can lag behind Cloud — if you run an older self-hosted build, use a matching older SDK from [previous releases](https://github.com/appwrite/sdk-for-android/releases) when APIs differ. +**This SDK targets Appwrite server version 2.5.x as shipped on Appwrite Cloud.** Self-hosted releases can lag behind Cloud — if you run an older self-hosted build, use a matching older SDK from [previous releases](https://github.com/appwrite/sdk-for-android/releases) when APIs differ. Appwrite is an open-source backend as a service server that abstracts and simplifies complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the Android SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs) @@ -38,7 +38,7 @@ repositories { Next, add the dependency to your project's `build.gradle(.kts)` file: ```groovy -implementation("io.appwrite:sdk-for-android:29.1.0-rc.3") +implementation("io.appwrite:sdk-for-android:30.0.0") ``` ### Maven @@ -49,7 +49,7 @@ Add this to your project's `pom.xml` file: io.appwrite sdk-for-android - 29.1.0-rc.3 + 30.0.0 ``` @@ -79,8 +79,10 @@ app every 15 to 60 seconds to reconnect, and the broker replays what was sent in While the app is not on screen, each message is also posted as a notification that opens your launch activity with `Push.EXTRA_TOPIC` and `Push.EXTRA_PAYLOAD` in its extras. It shows the -title, body and image sent with `createPush`, falling back to the subscription's `title` and the -raw payload for other messages. On Android 13 and later it needs the `POST_NOTIFICATIONS` +title, body and image sent with `createPush`, falling back to the subscription's `title`, and to +the raw payload as the body for messages that are not a JSON object. A push with neither a title +nor a body, such as a data-only push, posts no notification; it still reaches your callback or +`PushReceiver`. On Android 13 and later it needs the `POST_NOTIFICATIONS` permission: the first background subscription asks for it from the visible activity (the one `Push` is created with, else the next one to resume), or call `Push.requestNotificationPermission(activity)` to ask yourself. Set its @@ -119,6 +121,13 @@ The SDK uses exact alarms on its own whenever the app may schedule them. To allo nothing to ask, including when the permission is not declared. The SDK never opens these screens on its own. +If `backgroundRestricted` is true, the user restricted the app's background usage and messages can +wait until the app is opened. Exact alarms don't lift it, and neither does foreground mode: Android +stops a restricted app's foreground service once it leaves the screen. Ask the user to choose +Unrestricted in the app's battery settings (Settings > Apps > your app > Battery); granting the +battery optimisation exemption also lets the SDK's alarms and jobs run, and `bestEffort` then +reflects only the exact-alarm state. + If the user force-stops the app (Settings > Force stop, and on some devices swiping it away from recents), Android cancels its alarms and jobs: nothing is delivered until the app is opened again, and the broker then replays what was sent meanwhile. diff --git a/docs/examples/java/account/create-o-auth-2-session.md b/docs/examples/java/account/create-o-auth-2-session.md index da547421..f2a07eff 100644 --- a/docs/examples/java/account/create-o-auth-2-session.md +++ b/docs/examples/java/account/create-o-auth-2-session.md @@ -17,6 +17,7 @@ account.createOAuth2Session( "https://example.com", // success (optional) "https://example.com", // failure (optional) List.of(), // scopes (optional) + "", // state (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/java/account/create-o-auth-2-token.md b/docs/examples/java/account/create-o-auth-2-token.md index ae685850..885c066f 100644 --- a/docs/examples/java/account/create-o-auth-2-token.md +++ b/docs/examples/java/account/create-o-auth-2-token.md @@ -17,6 +17,7 @@ account.createOAuth2Token( "https://example.com", // success (optional) "https://example.com", // failure (optional) List.of(), // scopes (optional) + "", // state (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/java/account/create-passkey-token.md b/docs/examples/java/account/create-passkey-token.md new file mode 100644 index 00000000..12db5fe5 --- /dev/null +++ b/docs/examples/java/account/create-passkey-token.md @@ -0,0 +1,22 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.createPasskeyToken(new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); +})); +``` diff --git a/docs/examples/java/account/create-passkey.md b/docs/examples/java/account/create-passkey.md new file mode 100644 index 00000000..39a9a29d --- /dev/null +++ b/docs/examples/java/account/create-passkey.md @@ -0,0 +1,27 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.createPasskey( + "", // passkeyId (optional) + "", // name (optional) + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/delete-passkey.md b/docs/examples/java/account/delete-passkey.md new file mode 100644 index 00000000..a59d3848 --- /dev/null +++ b/docs/examples/java/account/delete-passkey.md @@ -0,0 +1,26 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.deletePasskey( + "", // passkeyId + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/get-passkey.md b/docs/examples/java/account/get-passkey.md new file mode 100644 index 00000000..5d988e86 --- /dev/null +++ b/docs/examples/java/account/get-passkey.md @@ -0,0 +1,26 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.getPasskey( + "", // passkeyId + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/list-passkeys.md b/docs/examples/java/account/list-passkeys.md new file mode 100644 index 00000000..6b04b1fb --- /dev/null +++ b/docs/examples/java/account/list-passkeys.md @@ -0,0 +1,27 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.listPasskeys( + List.of(), // queries (optional) + false, // total (optional) + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/update-passkey-token.md b/docs/examples/java/account/update-passkey-token.md new file mode 100644 index 00000000..6370aaaf --- /dev/null +++ b/docs/examples/java/account/update-passkey-token.md @@ -0,0 +1,27 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.updatePasskeyToken( + "", // challengeId + Map.of("a", "b"), // credential + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/update-passkey-verification.md b/docs/examples/java/account/update-passkey-verification.md new file mode 100644 index 00000000..53eed3a1 --- /dev/null +++ b/docs/examples/java/account/update-passkey-verification.md @@ -0,0 +1,27 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.updatePasskeyVerification( + "", // passkeyId + Map.of("a", "b"), // credential + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/update-passkey.md b/docs/examples/java/account/update-passkey.md new file mode 100644 index 00000000..3cf1078a --- /dev/null +++ b/docs/examples/java/account/update-passkey.md @@ -0,0 +1,27 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.updatePasskey( + "", // passkeyId + "", // name + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/analytics/create-event.md b/docs/examples/java/analytics/create-event.md new file mode 100644 index 00000000..e2b8dbe0 --- /dev/null +++ b/docs/examples/java/analytics/create-event.md @@ -0,0 +1,38 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Analytics; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Analytics analytics = new Analytics(client); + +analytics.createEvent( + "", // propertyId + "", // name + "https://example.com", // url + "", // domain (optional) + "", // referrer (optional) + 0L, // screenWidth (optional) + "", // sessionHash (optional) + 0L, // scrollDepth (optional) + 0L, // engagementTime (optional) + List.of(), // props (optional) + "", // userId (optional) + "", // ip (optional) + "", // userAgent (optional) + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/oauth2/authorize-post.md b/docs/examples/java/oauth2/authorize-post.md index 99ed6b8e..077546fd 100644 --- a/docs/examples/java/oauth2/authorize-post.md +++ b/docs/examples/java/oauth2/authorize-post.md @@ -23,7 +23,7 @@ oauth2.authorizePost( "", // prompt (optional) 0L, // max_age (optional) "", // authorization_details (optional) - "", // resource (optional) + List.of(), // resource (optional) "", // audience (optional) "", // request_uri (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/java/oauth2/authorize.md b/docs/examples/java/oauth2/authorize.md index 62a452c8..d5b72cf1 100644 --- a/docs/examples/java/oauth2/authorize.md +++ b/docs/examples/java/oauth2/authorize.md @@ -23,7 +23,7 @@ oauth2.authorize( "", // prompt (optional) 0L, // max_age (optional) "", // authorization_details (optional) - "", // resource (optional) + List.of(), // resource (optional) "", // audience (optional) "", // request_uri (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/java/oauth2/create-device-authorization.md b/docs/examples/java/oauth2/create-device-authorization.md index 0b65a1ab..c2fc4adb 100644 --- a/docs/examples/java/oauth2/create-device-authorization.md +++ b/docs/examples/java/oauth2/create-device-authorization.md @@ -15,7 +15,7 @@ oauth2.createDeviceAuthorization( "", // client_id (optional) "", // scope (optional) "", // authorization_details (optional) - "", // resource (optional) + List.of(), // resource (optional) "", // audience (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/java/oauth2/create-par.md b/docs/examples/java/oauth2/create-par.md index e65bdb54..e1f14de9 100644 --- a/docs/examples/java/oauth2/create-par.md +++ b/docs/examples/java/oauth2/create-par.md @@ -23,7 +23,7 @@ oauth2.createPAR( "", // prompt (optional) 0L, // max_age (optional) "", // authorization_details (optional) - "", // resource (optional) + List.of(), // resource (optional) "", // audience (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/java/oauth2/create-token.md b/docs/examples/java/oauth2/create-token.md index 39b6fa25..5c808df3 100644 --- a/docs/examples/java/oauth2/create-token.md +++ b/docs/examples/java/oauth2/create-token.md @@ -20,7 +20,7 @@ oauth2.createToken( "", // client_secret (optional) "", // code_verifier (optional) "https://example.com", // redirect_uri (optional) - "", // resource (optional) + List.of(), // resource (optional) "", // audience (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/java/teams/create-membership.md b/docs/examples/java/teams/create-membership.md index d7fba7c1..b6d23a6a 100644 --- a/docs/examples/java/teams/create-membership.md +++ b/docs/examples/java/teams/create-membership.md @@ -19,6 +19,7 @@ teams.createMembership( "+12065550100", // phone (optional) "https://example.com", // url (optional) "", // name (optional) + "", // membershipId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/kotlin/account/create-o-auth-2-session.md b/docs/examples/kotlin/account/create-o-auth-2-session.md index cde941c7..c9b3a92a 100644 --- a/docs/examples/kotlin/account/create-o-auth-2-session.md +++ b/docs/examples/kotlin/account/create-o-auth-2-session.md @@ -15,5 +15,6 @@ account.createOAuth2Session( success = "https://example.com", // (optional) failure = "https://example.com", // (optional) scopes = listOf(), // (optional) + state = "", // (optional) ) ``` diff --git a/docs/examples/kotlin/account/create-o-auth-2-token.md b/docs/examples/kotlin/account/create-o-auth-2-token.md index 5e905649..3d5b0d77 100644 --- a/docs/examples/kotlin/account/create-o-auth-2-token.md +++ b/docs/examples/kotlin/account/create-o-auth-2-token.md @@ -15,5 +15,6 @@ account.createOAuth2Token( success = "https://example.com", // (optional) failure = "https://example.com", // (optional) scopes = listOf(), // (optional) + state = "", // (optional) ) ``` diff --git a/docs/examples/kotlin/account/create-passkey-token.md b/docs/examples/kotlin/account/create-passkey-token.md new file mode 100644 index 00000000..52b6bf3d --- /dev/null +++ b/docs/examples/kotlin/account/create-passkey-token.md @@ -0,0 +1,13 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.createPasskeyToken() +``` diff --git a/docs/examples/kotlin/account/create-passkey.md b/docs/examples/kotlin/account/create-passkey.md new file mode 100644 index 00000000..69a06515 --- /dev/null +++ b/docs/examples/kotlin/account/create-passkey.md @@ -0,0 +1,16 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.createPasskey( + passkeyId = "", // (optional) + name = "", // (optional) +) +``` diff --git a/docs/examples/kotlin/account/delete-passkey.md b/docs/examples/kotlin/account/delete-passkey.md new file mode 100644 index 00000000..08bf8fd6 --- /dev/null +++ b/docs/examples/kotlin/account/delete-passkey.md @@ -0,0 +1,15 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.deletePasskey( + passkeyId = "", +) +``` diff --git a/docs/examples/kotlin/account/get-passkey.md b/docs/examples/kotlin/account/get-passkey.md new file mode 100644 index 00000000..ddf9e833 --- /dev/null +++ b/docs/examples/kotlin/account/get-passkey.md @@ -0,0 +1,15 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.getPasskey( + passkeyId = "", +) +``` diff --git a/docs/examples/kotlin/account/list-passkeys.md b/docs/examples/kotlin/account/list-passkeys.md new file mode 100644 index 00000000..74c8add8 --- /dev/null +++ b/docs/examples/kotlin/account/list-passkeys.md @@ -0,0 +1,16 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.listPasskeys( + queries = listOf(), // (optional) + total = false, // (optional) +) +``` diff --git a/docs/examples/kotlin/account/update-passkey-token.md b/docs/examples/kotlin/account/update-passkey-token.md new file mode 100644 index 00000000..f48e29f9 --- /dev/null +++ b/docs/examples/kotlin/account/update-passkey-token.md @@ -0,0 +1,16 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.updatePasskeyToken( + challengeId = "", + credential = mapOf( "a" to "b" ), +) +``` diff --git a/docs/examples/kotlin/account/update-passkey-verification.md b/docs/examples/kotlin/account/update-passkey-verification.md new file mode 100644 index 00000000..41c016a8 --- /dev/null +++ b/docs/examples/kotlin/account/update-passkey-verification.md @@ -0,0 +1,16 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.updatePasskeyVerification( + passkeyId = "", + credential = mapOf( "a" to "b" ), +) +``` diff --git a/docs/examples/kotlin/account/update-passkey.md b/docs/examples/kotlin/account/update-passkey.md new file mode 100644 index 00000000..c8c2bc34 --- /dev/null +++ b/docs/examples/kotlin/account/update-passkey.md @@ -0,0 +1,16 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.updatePasskey( + passkeyId = "", + name = "", +) +``` diff --git a/docs/examples/kotlin/analytics/create-event.md b/docs/examples/kotlin/analytics/create-event.md new file mode 100644 index 00000000..820e0bac --- /dev/null +++ b/docs/examples/kotlin/analytics/create-event.md @@ -0,0 +1,27 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Analytics + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val analytics = Analytics(client) + +val result = analytics.createEvent( + propertyId = "", + name = "", + url = "https://example.com", + domain = "", // (optional) + referrer = "", // (optional) + screenWidth = 0, // (optional) + sessionHash = "", // (optional) + scrollDepth = 0, // (optional) + engagementTime = 0, // (optional) + props = listOf(), // (optional) + userId = "", // (optional) + ip = "", // (optional) + userAgent = "", // (optional) +) +``` diff --git a/docs/examples/kotlin/oauth2/authorize-post.md b/docs/examples/kotlin/oauth2/authorize-post.md index 88bc4ee0..ea490994 100644 --- a/docs/examples/kotlin/oauth2/authorize-post.md +++ b/docs/examples/kotlin/oauth2/authorize-post.md @@ -21,7 +21,7 @@ val result = oauth2.authorizePost( prompt = "", // (optional) maxAge = 0, // (optional) authorizationDetails = "", // (optional) - resource = "", // (optional) + resource = listOf(), // (optional) audience = "", // (optional) requestUri = "", // (optional) ) diff --git a/docs/examples/kotlin/oauth2/authorize.md b/docs/examples/kotlin/oauth2/authorize.md index 4d307e8d..6c598343 100644 --- a/docs/examples/kotlin/oauth2/authorize.md +++ b/docs/examples/kotlin/oauth2/authorize.md @@ -21,7 +21,7 @@ val result = oauth2.authorize( prompt = "", // (optional) maxAge = 0, // (optional) authorizationDetails = "", // (optional) - resource = "", // (optional) + resource = listOf(), // (optional) audience = "", // (optional) requestUri = "", // (optional) ) diff --git a/docs/examples/kotlin/oauth2/create-device-authorization.md b/docs/examples/kotlin/oauth2/create-device-authorization.md index ab307c37..d4fb9b02 100644 --- a/docs/examples/kotlin/oauth2/create-device-authorization.md +++ b/docs/examples/kotlin/oauth2/create-device-authorization.md @@ -13,7 +13,7 @@ val result = oauth2.createDeviceAuthorization( clientId = "", // (optional) scope = "", // (optional) authorizationDetails = "", // (optional) - resource = "", // (optional) + resource = listOf(), // (optional) audience = "", // (optional) ) ``` diff --git a/docs/examples/kotlin/oauth2/create-par.md b/docs/examples/kotlin/oauth2/create-par.md index 752c64eb..213096c1 100644 --- a/docs/examples/kotlin/oauth2/create-par.md +++ b/docs/examples/kotlin/oauth2/create-par.md @@ -21,7 +21,7 @@ val result = oauth2.createPAR( prompt = "", // (optional) maxAge = 0, // (optional) authorizationDetails = "", // (optional) - resource = "", // (optional) + resource = listOf(), // (optional) audience = "", // (optional) ) ``` diff --git a/docs/examples/kotlin/oauth2/create-token.md b/docs/examples/kotlin/oauth2/create-token.md index 5e23344f..a0e4f20e 100644 --- a/docs/examples/kotlin/oauth2/create-token.md +++ b/docs/examples/kotlin/oauth2/create-token.md @@ -18,7 +18,7 @@ val result = oauth2.createToken( clientSecret = "", // (optional) codeVerifier = "", // (optional) redirectUri = "https://example.com", // (optional) - resource = "", // (optional) + resource = listOf(), // (optional) audience = "", // (optional) ) ``` diff --git a/docs/examples/kotlin/teams/create-membership.md b/docs/examples/kotlin/teams/create-membership.md index 219119af..2f73c039 100644 --- a/docs/examples/kotlin/teams/create-membership.md +++ b/docs/examples/kotlin/teams/create-membership.md @@ -17,5 +17,6 @@ val result = teams.createMembership( phone = "+12065550100", // (optional) url = "https://example.com", // (optional) name = "", // (optional) + membershipId = "", // (optional) ) ``` diff --git a/library/src/main/java/io/appwrite/Client.kt b/library/src/main/java/io/appwrite/Client.kt index 3df991a3..2b7b52bd 100644 --- a/library/src/main/java/io/appwrite/Client.kt +++ b/library/src/main/java/io/appwrite/Client.kt @@ -101,8 +101,8 @@ class Client @JvmOverloads constructor( "x-sdk-name" to "Android", "x-sdk-platform" to "client", "x-sdk-language" to "android", - "x-sdk-version" to "29.1.0-rc.3", - "x-appwrite-response-format" to "2.3.0" + "x-sdk-version" to "30.0.0", + "x-appwrite-response-format" to "2.5.0" ) config = mutableMapOf() diff --git a/library/src/main/java/io/appwrite/models/Account.kt b/library/src/main/java/io/appwrite/models/Account.kt index 38865ff8..2d461144 100644 --- a/library/src/main/java/io/appwrite/models/Account.kt +++ b/library/src/main/java/io/appwrite/models/Account.kt @@ -237,28 +237,28 @@ data class Account( map: Map, nestedType: Class ) = Account( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - name = map["name"] as String, - registration = map["registration"] as String, - status = map["status"] as Boolean, - labels = map["labels"] as List, - passwordUpdate = map["passwordUpdate"] as String, - email = map["email"] as String, - phone = map["phone"] as String, - emailVerification = map["emailVerification"] as Boolean, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + registration = (if (map.containsKey("registration")) map["registration"] else "") as String, + status = (if (map.containsKey("status")) map["status"] else true) as Boolean, + labels = (if (map.containsKey("labels")) map["labels"] else listOf()) as List, + passwordUpdate = (if (map.containsKey("passwordUpdate")) map["passwordUpdate"] else "") as String, + email = (if (map.containsKey("email")) map["email"] else "") as String, + phone = (if (map.containsKey("phone")) map["phone"] else "") as String, + emailVerification = (if (map.containsKey("emailVerification")) map["emailVerification"] else false) as Boolean, emailCanonical = map["emailCanonical"] as? String, emailIsFree = map["emailIsFree"] as? Boolean, emailIsDisposable = map["emailIsDisposable"] as? Boolean, emailIsCorporate = map["emailIsCorporate"] as? Boolean, emailIsCanonical = map["emailIsCanonical"] as? Boolean, passwordPwned = map["passwordPwned"] as? Boolean, - phoneVerification = map["phoneVerification"] as Boolean, - mfa = map["mfa"] as Boolean, + phoneVerification = (if (map.containsKey("phoneVerification")) map["phoneVerification"] else false) as Boolean, + mfa = (if (map.containsKey("mfa")) map["mfa"] else false) as Boolean, prefs = Preferences.from(map = map["prefs"] as Map, nestedType), - targets = (map["targets"] as List>).map { Target.from(map = it) }, - accessedAt = map["accessedAt"] as String, + targets = ((if (map.containsKey("targets")) map["targets"] else listOf()) as List>).map { Target.from(map = it) }, + accessedAt = (if (map.containsKey("accessedAt")) map["accessedAt"] else "") as String, impersonator = map["impersonator"] as? Boolean, impersonatorUserId = map["impersonatorUserId"] as? String, ) diff --git a/library/src/main/java/io/appwrite/models/AlgoArgon2.kt b/library/src/main/java/io/appwrite/models/AlgoArgon2.kt index 39773328..1d26dfd3 100644 --- a/library/src/main/java/io/appwrite/models/AlgoArgon2.kt +++ b/library/src/main/java/io/appwrite/models/AlgoArgon2.kt @@ -44,10 +44,10 @@ data class AlgoArgon2( fun from( map: Map, ) = AlgoArgon2( - type = map["type"] as String, - memoryCost = (map["memoryCost"] as Number).toLong(), - timeCost = (map["timeCost"] as Number).toLong(), - threads = (map["threads"] as Number).toLong(), + type = (if (map.containsKey("type")) map["type"] else "argon2") as String, + memoryCost = ((if (map.containsKey("memoryCost")) map["memoryCost"] else 0) as Number).toLong(), + timeCost = ((if (map.containsKey("timeCost")) map["timeCost"] else 0) as Number).toLong(), + threads = ((if (map.containsKey("threads")) map["threads"] else 0) as Number).toLong(), ) } } diff --git a/library/src/main/java/io/appwrite/models/AlgoBcrypt.kt b/library/src/main/java/io/appwrite/models/AlgoBcrypt.kt index 38317807..2a99dad7 100644 --- a/library/src/main/java/io/appwrite/models/AlgoBcrypt.kt +++ b/library/src/main/java/io/appwrite/models/AlgoBcrypt.kt @@ -23,7 +23,7 @@ data class AlgoBcrypt( fun from( map: Map, ) = AlgoBcrypt( - type = map["type"] as String, + type = (if (map.containsKey("type")) map["type"] else "bcrypt") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/AlgoMd5.kt b/library/src/main/java/io/appwrite/models/AlgoMd5.kt index e46eb99b..d683ea70 100644 --- a/library/src/main/java/io/appwrite/models/AlgoMd5.kt +++ b/library/src/main/java/io/appwrite/models/AlgoMd5.kt @@ -23,7 +23,7 @@ data class AlgoMd5( fun from( map: Map, ) = AlgoMd5( - type = map["type"] as String, + type = (if (map.containsKey("type")) map["type"] else "md5") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/AlgoPhpass.kt b/library/src/main/java/io/appwrite/models/AlgoPhpass.kt index d457a81d..3eba6cad 100644 --- a/library/src/main/java/io/appwrite/models/AlgoPhpass.kt +++ b/library/src/main/java/io/appwrite/models/AlgoPhpass.kt @@ -23,7 +23,7 @@ data class AlgoPhpass( fun from( map: Map, ) = AlgoPhpass( - type = map["type"] as String, + type = (if (map.containsKey("type")) map["type"] else "phpass") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/AlgoScrypt.kt b/library/src/main/java/io/appwrite/models/AlgoScrypt.kt index 92147046..789061fe 100644 --- a/library/src/main/java/io/appwrite/models/AlgoScrypt.kt +++ b/library/src/main/java/io/appwrite/models/AlgoScrypt.kt @@ -51,11 +51,11 @@ data class AlgoScrypt( fun from( map: Map, ) = AlgoScrypt( - type = map["type"] as String, - costCpu = (map["costCpu"] as Number).toLong(), - costMemory = (map["costMemory"] as Number).toLong(), - costParallel = (map["costParallel"] as Number).toLong(), - length = (map["length"] as Number).toLong(), + type = (if (map.containsKey("type")) map["type"] else "scrypt") as String, + costCpu = ((if (map.containsKey("costCpu")) map["costCpu"] else 8) as Number).toLong(), + costMemory = ((if (map.containsKey("costMemory")) map["costMemory"] else 14) as Number).toLong(), + costParallel = ((if (map.containsKey("costParallel")) map["costParallel"] else 1) as Number).toLong(), + length = ((if (map.containsKey("length")) map["length"] else 64) as Number).toLong(), ) } } diff --git a/library/src/main/java/io/appwrite/models/AlgoScryptModified.kt b/library/src/main/java/io/appwrite/models/AlgoScryptModified.kt index 539828ac..ff306fa4 100644 --- a/library/src/main/java/io/appwrite/models/AlgoScryptModified.kt +++ b/library/src/main/java/io/appwrite/models/AlgoScryptModified.kt @@ -44,10 +44,10 @@ data class AlgoScryptModified( fun from( map: Map, ) = AlgoScryptModified( - type = map["type"] as String, - salt = map["salt"] as String, - saltSeparator = map["saltSeparator"] as String, - signerKey = map["signerKey"] as String, + type = (if (map.containsKey("type")) map["type"] else "scryptMod") as String, + salt = (if (map.containsKey("salt")) map["salt"] else "") as String, + saltSeparator = (if (map.containsKey("saltSeparator")) map["saltSeparator"] else "") as String, + signerKey = (if (map.containsKey("signerKey")) map["signerKey"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/AlgoSha.kt b/library/src/main/java/io/appwrite/models/AlgoSha.kt index 45fb0104..5c52f087 100644 --- a/library/src/main/java/io/appwrite/models/AlgoSha.kt +++ b/library/src/main/java/io/appwrite/models/AlgoSha.kt @@ -23,7 +23,7 @@ data class AlgoSha( fun from( map: Map, ) = AlgoSha( - type = map["type"] as String, + type = (if (map.containsKey("type")) map["type"] else "sha") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/App.kt b/library/src/main/java/io/appwrite/models/App.kt index f4a715c8..9125cdcf 100644 --- a/library/src/main/java/io/appwrite/models/App.kt +++ b/library/src/main/java/io/appwrite/models/App.kt @@ -198,32 +198,32 @@ data class App( fun from( map: Map, ) = App( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - name = map["name"] as String, - description = map["description"] as String, - clientUri = map["clientUri"] as String, - logoUri = map["logoUri"] as String, - privacyPolicyUrl = map["privacyPolicyUrl"] as String, - termsUrl = map["termsUrl"] as String, - contacts = map["contacts"] as List, - tagline = map["tagline"] as String, - tags = map["tags"] as List, - labels = map["labels"] as List, - images = map["images"] as List, - supportUrl = map["supportUrl"] as String, - dataDeletionUrl = map["dataDeletionUrl"] as String, - redirectUris = map["redirectUris"] as List, - postLogoutRedirectUris = map["postLogoutRedirectUris"] as List, - enabled = map["enabled"] as Boolean, - type = map["type"] as String, - deviceFlow = map["deviceFlow"] as Boolean, - teamId = map["teamId"] as String, - userId = map["userId"] as String, - installationScopes = map["installationScopes"] as List, - installationRedirectUrl = map["installationRedirectUrl"] as String, - secrets = (map["secrets"] as List>).map { AppSecret.from(map = it) }, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + description = (if (map.containsKey("description")) map["description"] else "") as String, + clientUri = (if (map.containsKey("clientUri")) map["clientUri"] else "") as String, + logoUri = (if (map.containsKey("logoUri")) map["logoUri"] else "") as String, + privacyPolicyUrl = (if (map.containsKey("privacyPolicyUrl")) map["privacyPolicyUrl"] else "") as String, + termsUrl = (if (map.containsKey("termsUrl")) map["termsUrl"] else "") as String, + contacts = (if (map.containsKey("contacts")) map["contacts"] else listOf()) as List, + tagline = (if (map.containsKey("tagline")) map["tagline"] else "") as String, + tags = (if (map.containsKey("tags")) map["tags"] else listOf()) as List, + labels = (if (map.containsKey("labels")) map["labels"] else listOf()) as List, + images = (if (map.containsKey("images")) map["images"] else listOf()) as List, + supportUrl = (if (map.containsKey("supportUrl")) map["supportUrl"] else "") as String, + dataDeletionUrl = (if (map.containsKey("dataDeletionUrl")) map["dataDeletionUrl"] else "") as String, + redirectUris = (if (map.containsKey("redirectUris")) map["redirectUris"] else listOf()) as List, + postLogoutRedirectUris = (if (map.containsKey("postLogoutRedirectUris")) map["postLogoutRedirectUris"] else listOf()) as List, + enabled = (if (map.containsKey("enabled")) map["enabled"] else true) as Boolean, + type = (if (map.containsKey("type")) map["type"] else "confidential") as String, + deviceFlow = (if (map.containsKey("deviceFlow")) map["deviceFlow"] else false) as Boolean, + teamId = (if (map.containsKey("teamId")) map["teamId"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + installationScopes = (if (map.containsKey("installationScopes")) map["installationScopes"] else listOf()) as List, + installationRedirectUrl = (if (map.containsKey("installationRedirectUrl")) map["installationRedirectUrl"] else "") as String, + secrets = ((if (map.containsKey("secrets")) map["secrets"] else listOf()) as List>).map { AppSecret.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/AppInstallation.kt b/library/src/main/java/io/appwrite/models/AppInstallation.kt index 46bf9075..3c6d1f68 100644 --- a/library/src/main/java/io/appwrite/models/AppInstallation.kt +++ b/library/src/main/java/io/appwrite/models/AppInstallation.kt @@ -86,15 +86,15 @@ data class AppInstallation( fun from( map: Map, ) = AppInstallation( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - appId = map["appId"] as String, - teamId = map["teamId"] as String, - scopes = map["scopes"] as List, - authorizationDetails = map["authorizationDetails"] as List>, - createdById = map["createdById"] as String, - createdByName = map["createdByName"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + teamId = (if (map.containsKey("teamId")) map["teamId"] else "") as String, + scopes = (if (map.containsKey("scopes")) map["scopes"] else listOf()) as List, + authorizationDetails = (if (map.containsKey("authorizationDetails")) map["authorizationDetails"] else listOf()) as List>, + createdById = (if (map.containsKey("createdById")) map["createdById"] else "") as String, + createdByName = (if (map.containsKey("createdByName")) map["createdByName"] else "") as String, lastAccessedAt = map["lastAccessedAt"] as? String, ) } diff --git a/library/src/main/java/io/appwrite/models/AppInstallationList.kt b/library/src/main/java/io/appwrite/models/AppInstallationList.kt index f7434cf2..8006d98c 100644 --- a/library/src/main/java/io/appwrite/models/AppInstallationList.kt +++ b/library/src/main/java/io/appwrite/models/AppInstallationList.kt @@ -30,8 +30,8 @@ data class AppInstallationList( fun from( map: Map, ) = AppInstallationList( - total = (map["total"] as Number).toLong(), - installations = (map["installations"] as List>).map { AppInstallation.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + installations = ((if (map.containsKey("installations")) map["installations"] else listOf()) as List>).map { AppInstallation.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/AppKey.kt b/library/src/main/java/io/appwrite/models/AppKey.kt index f59e8d50..7348cd3f 100644 --- a/library/src/main/java/io/appwrite/models/AppKey.kt +++ b/library/src/main/java/io/appwrite/models/AppKey.kt @@ -79,14 +79,14 @@ data class AppKey( fun from( map: Map, ) = AppKey( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - appId = map["appId"] as String, - secret = map["secret"] as String, - hint = map["hint"] as String, - createdById = map["createdById"] as String, - createdByName = map["createdByName"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + hint = (if (map.containsKey("hint")) map["hint"] else "") as String, + createdById = (if (map.containsKey("createdById")) map["createdById"] else "") as String, + createdByName = (if (map.containsKey("createdByName")) map["createdByName"] else "") as String, lastAccessedAt = map["lastAccessedAt"] as? String, ) } diff --git a/library/src/main/java/io/appwrite/models/AppKeyList.kt b/library/src/main/java/io/appwrite/models/AppKeyList.kt index ea4b1cca..854af040 100644 --- a/library/src/main/java/io/appwrite/models/AppKeyList.kt +++ b/library/src/main/java/io/appwrite/models/AppKeyList.kt @@ -30,8 +30,8 @@ data class AppKeyList( fun from( map: Map, ) = AppKeyList( - total = (map["total"] as Number).toLong(), - keys = (map["keys"] as List>).map { AppKey.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + keys = ((if (map.containsKey("keys")) map["keys"] else listOf()) as List>).map { AppKey.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/AppScope.kt b/library/src/main/java/io/appwrite/models/AppScope.kt index 4cc7fe95..dbacafc7 100644 --- a/library/src/main/java/io/appwrite/models/AppScope.kt +++ b/library/src/main/java/io/appwrite/models/AppScope.kt @@ -51,11 +51,11 @@ data class AppScope( fun from( map: Map, ) = AppScope( - value = map["value"] as String, - description = map["description"] as String, - type = map["type"] as String, - category = map["category"] as String, - deprecated = map["deprecated"] as Boolean, + value = (if (map.containsKey("value")) map["value"] else "") as String, + description = (if (map.containsKey("description")) map["description"] else "") as String, + type = (if (map.containsKey("type")) map["type"] else "") as String, + category = (if (map.containsKey("category")) map["category"] else "") as String, + deprecated = (if (map.containsKey("deprecated")) map["deprecated"] else false) as Boolean, ) } } diff --git a/library/src/main/java/io/appwrite/models/AppScopeList.kt b/library/src/main/java/io/appwrite/models/AppScopeList.kt index cbcf91fb..76bc7369 100644 --- a/library/src/main/java/io/appwrite/models/AppScopeList.kt +++ b/library/src/main/java/io/appwrite/models/AppScopeList.kt @@ -30,8 +30,8 @@ data class AppScopeList( fun from( map: Map, ) = AppScopeList( - total = (map["total"] as Number).toLong(), - scopes = (map["scopes"] as List>).map { AppScope.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + scopes = ((if (map.containsKey("scopes")) map["scopes"] else listOf()) as List>).map { AppScope.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/AppSecret.kt b/library/src/main/java/io/appwrite/models/AppSecret.kt index 0349ffbd..b7d70922 100644 --- a/library/src/main/java/io/appwrite/models/AppSecret.kt +++ b/library/src/main/java/io/appwrite/models/AppSecret.kt @@ -79,14 +79,14 @@ data class AppSecret( fun from( map: Map, ) = AppSecret( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - appId = map["appId"] as String, - secret = map["secret"] as String, - hint = map["hint"] as String, - createdById = map["createdById"] as String, - createdByName = map["createdByName"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + hint = (if (map.containsKey("hint")) map["hint"] else "") as String, + createdById = (if (map.containsKey("createdById")) map["createdById"] else "") as String, + createdByName = (if (map.containsKey("createdByName")) map["createdByName"] else "") as String, lastAccessedAt = map["lastAccessedAt"] as? String, ) } diff --git a/library/src/main/java/io/appwrite/models/AppSecretList.kt b/library/src/main/java/io/appwrite/models/AppSecretList.kt index 04d2ebb7..1f2946c4 100644 --- a/library/src/main/java/io/appwrite/models/AppSecretList.kt +++ b/library/src/main/java/io/appwrite/models/AppSecretList.kt @@ -30,8 +30,8 @@ data class AppSecretList( fun from( map: Map, ) = AppSecretList( - total = (map["total"] as Number).toLong(), - secrets = (map["secrets"] as List>).map { AppSecret.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + secrets = ((if (map.containsKey("secrets")) map["secrets"] else listOf()) as List>).map { AppSecret.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/AppSecretPlaintext.kt b/library/src/main/java/io/appwrite/models/AppSecretPlaintext.kt index 098b7f19..7464598c 100644 --- a/library/src/main/java/io/appwrite/models/AppSecretPlaintext.kt +++ b/library/src/main/java/io/appwrite/models/AppSecretPlaintext.kt @@ -79,14 +79,14 @@ data class AppSecretPlaintext( fun from( map: Map, ) = AppSecretPlaintext( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - appId = map["appId"] as String, - secret = map["secret"] as String, - hint = map["hint"] as String, - createdById = map["createdById"] as String, - createdByName = map["createdByName"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + hint = (if (map.containsKey("hint")) map["hint"] else "") as String, + createdById = (if (map.containsKey("createdById")) map["createdById"] else "") as String, + createdByName = (if (map.containsKey("createdByName")) map["createdByName"] else "") as String, lastAccessedAt = map["lastAccessedAt"] as? String, ) } diff --git a/library/src/main/java/io/appwrite/models/AppsList.kt b/library/src/main/java/io/appwrite/models/AppsList.kt index 5dcff821..0ad546f1 100644 --- a/library/src/main/java/io/appwrite/models/AppsList.kt +++ b/library/src/main/java/io/appwrite/models/AppsList.kt @@ -30,8 +30,8 @@ data class AppsList( fun from( map: Map, ) = AppsList( - total = (map["total"] as Number).toLong(), - apps = (map["apps"] as List>).map { App.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + apps = ((if (map.containsKey("apps")) map["apps"] else listOf()) as List>).map { App.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Continent.kt b/library/src/main/java/io/appwrite/models/Continent.kt index 0adbb5fa..e8599a03 100644 --- a/library/src/main/java/io/appwrite/models/Continent.kt +++ b/library/src/main/java/io/appwrite/models/Continent.kt @@ -30,8 +30,8 @@ data class Continent( fun from( map: Map, ) = Continent( - name = map["name"] as String, - code = map["code"] as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + code = (if (map.containsKey("code")) map["code"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/ContinentList.kt b/library/src/main/java/io/appwrite/models/ContinentList.kt index 53f95f2a..0a1f4f34 100644 --- a/library/src/main/java/io/appwrite/models/ContinentList.kt +++ b/library/src/main/java/io/appwrite/models/ContinentList.kt @@ -30,8 +30,8 @@ data class ContinentList( fun from( map: Map, ) = ContinentList( - total = (map["total"] as Number).toLong(), - continents = (map["continents"] as List>).map { Continent.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + continents = ((if (map.containsKey("continents")) map["continents"] else listOf()) as List>).map { Continent.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Country.kt b/library/src/main/java/io/appwrite/models/Country.kt index 7b104f0b..1c3065ea 100644 --- a/library/src/main/java/io/appwrite/models/Country.kt +++ b/library/src/main/java/io/appwrite/models/Country.kt @@ -30,8 +30,8 @@ data class Country( fun from( map: Map, ) = Country( - name = map["name"] as String, - code = map["code"] as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + code = (if (map.containsKey("code")) map["code"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/CountryList.kt b/library/src/main/java/io/appwrite/models/CountryList.kt index 322cdaf8..629bae23 100644 --- a/library/src/main/java/io/appwrite/models/CountryList.kt +++ b/library/src/main/java/io/appwrite/models/CountryList.kt @@ -30,8 +30,8 @@ data class CountryList( fun from( map: Map, ) = CountryList( - total = (map["total"] as Number).toLong(), - countries = (map["countries"] as List>).map { Country.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + countries = ((if (map.containsKey("countries")) map["countries"] else listOf()) as List>).map { Country.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Currency.kt b/library/src/main/java/io/appwrite/models/Currency.kt index 95ff13ac..d78a90ca 100644 --- a/library/src/main/java/io/appwrite/models/Currency.kt +++ b/library/src/main/java/io/appwrite/models/Currency.kt @@ -65,13 +65,13 @@ data class Currency( fun from( map: Map, ) = Currency( - symbol = map["symbol"] as String, - name = map["name"] as String, - symbolNative = map["symbolNative"] as String, - decimalDigits = (map["decimalDigits"] as Number).toLong(), - rounding = (map["rounding"] as Number).toDouble(), - code = map["code"] as String, - namePlural = map["namePlural"] as String, + symbol = (if (map.containsKey("symbol")) map["symbol"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + symbolNative = (if (map.containsKey("symbolNative")) map["symbolNative"] else "") as String, + decimalDigits = ((if (map.containsKey("decimalDigits")) map["decimalDigits"] else 0) as Number).toLong(), + rounding = ((if (map.containsKey("rounding")) map["rounding"] else 0.0) as Number).toDouble(), + code = (if (map.containsKey("code")) map["code"] else "") as String, + namePlural = (if (map.containsKey("namePlural")) map["namePlural"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/CurrencyList.kt b/library/src/main/java/io/appwrite/models/CurrencyList.kt index 8082a2f1..5928c9a1 100644 --- a/library/src/main/java/io/appwrite/models/CurrencyList.kt +++ b/library/src/main/java/io/appwrite/models/CurrencyList.kt @@ -30,8 +30,8 @@ data class CurrencyList( fun from( map: Map, ) = CurrencyList( - total = (map["total"] as Number).toLong(), - currencies = (map["currencies"] as List>).map { Currency.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + currencies = ((if (map.containsKey("currencies")) map["currencies"] else listOf()) as List>).map { Currency.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Document.kt b/library/src/main/java/io/appwrite/models/Document.kt index 20e803e1..a19dd7fa 100644 --- a/library/src/main/java/io/appwrite/models/Document.kt +++ b/library/src/main/java/io/appwrite/models/Document.kt @@ -92,13 +92,13 @@ data class Document( map: Map, nestedType: Class ) = Document( - id = map["\$id"] as String, - sequence = map["\$sequence"] as String, - collectionId = map["\$collectionId"] as String, - databaseId = map["\$databaseId"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - permissions = map["\$permissions"] as List, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + sequence = (if (map.containsKey("\$sequence")) map["\$sequence"] else "") as String, + collectionId = (if (map.containsKey("\$collectionId")) map["\$collectionId"] else "") as String, + databaseId = (if (map.containsKey("\$databaseId")) map["\$databaseId"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + permissions = (if (map.containsKey("\$permissions")) map["\$permissions"] else listOf()) as List, data = map["data"]?.jsonCast(to = nestedType) ?: map.jsonCast(to = nestedType) ) } diff --git a/library/src/main/java/io/appwrite/models/DocumentList.kt b/library/src/main/java/io/appwrite/models/DocumentList.kt index d8741eab..59203167 100644 --- a/library/src/main/java/io/appwrite/models/DocumentList.kt +++ b/library/src/main/java/io/appwrite/models/DocumentList.kt @@ -39,8 +39,8 @@ data class DocumentList( map: Map, nestedType: Class ) = DocumentList( - total = (map["total"] as Number).toLong(), - documents = (map["documents"] as List>).map { Document.from(map = it, nestedType) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + documents = ((if (map.containsKey("documents")) map["documents"] else listOf()) as List>).map { Document.from(map = it, nestedType) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Execution.kt b/library/src/main/java/io/appwrite/models/Execution.kt index c698d9c6..c7cada63 100644 --- a/library/src/main/java/io/appwrite/models/Execution.kt +++ b/library/src/main/java/io/appwrite/models/Execution.kt @@ -152,24 +152,24 @@ data class Execution( fun from( map: Map, ) = Execution( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - permissions = map["\$permissions"] as List, - resourceId = map["resourceId"] as String, - resourceType = ExecutionResourceType.values().find { it.value == map["resourceType"] as String }!!, - deploymentId = map["deploymentId"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + permissions = (if (map.containsKey("\$permissions")) map["\$permissions"] else listOf()) as List, + resourceId = (if (map.containsKey("resourceId")) map["resourceId"] else "") as String, + resourceType = ExecutionResourceType.values().find { it.value == (if (map.containsKey("resourceType")) map["resourceType"] else "functions") as String }!!, + deploymentId = (if (map.containsKey("deploymentId")) map["deploymentId"] else "") as String, trigger = ExecutionTrigger.values().find { it.value == map["trigger"] as String }!!, status = ExecutionStatus.values().find { it.value == map["status"] as String }!!, - requestMethod = map["requestMethod"] as String, - requestPath = map["requestPath"] as String, - requestHeaders = (map["requestHeaders"] as List>).map { Headers.from(map = it) }, - responseStatusCode = (map["responseStatusCode"] as Number).toLong(), - responseBody = map["responseBody"] as String, - responseHeaders = (map["responseHeaders"] as List>).map { Headers.from(map = it) }, - logs = map["logs"] as String, - errors = map["errors"] as String, - duration = (map["duration"] as Number).toDouble(), + requestMethod = (if (map.containsKey("requestMethod")) map["requestMethod"] else "") as String, + requestPath = (if (map.containsKey("requestPath")) map["requestPath"] else "") as String, + requestHeaders = ((if (map.containsKey("requestHeaders")) map["requestHeaders"] else listOf()) as List>).map { Headers.from(map = it) }, + responseStatusCode = ((if (map.containsKey("responseStatusCode")) map["responseStatusCode"] else 0) as Number).toLong(), + responseBody = (if (map.containsKey("responseBody")) map["responseBody"] else "") as String, + responseHeaders = ((if (map.containsKey("responseHeaders")) map["responseHeaders"] else listOf()) as List>).map { Headers.from(map = it) }, + logs = (if (map.containsKey("logs")) map["logs"] else "") as String, + errors = (if (map.containsKey("errors")) map["errors"] else "") as String, + duration = ((if (map.containsKey("duration")) map["duration"] else 0.0) as Number).toDouble(), scheduledAt = map["scheduledAt"] as? String, ) } diff --git a/library/src/main/java/io/appwrite/models/ExecutionList.kt b/library/src/main/java/io/appwrite/models/ExecutionList.kt index 4eac1d89..0eba2816 100644 --- a/library/src/main/java/io/appwrite/models/ExecutionList.kt +++ b/library/src/main/java/io/appwrite/models/ExecutionList.kt @@ -30,8 +30,8 @@ data class ExecutionList( fun from( map: Map, ) = ExecutionList( - total = (map["total"] as Number).toLong(), - executions = (map["executions"] as List>).map { Execution.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + executions = ((if (map.containsKey("executions")) map["executions"] else listOf()) as List>).map { Execution.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/File.kt b/library/src/main/java/io/appwrite/models/File.kt index d15a55b1..049bbae7 100644 --- a/library/src/main/java/io/appwrite/models/File.kt +++ b/library/src/main/java/io/appwrite/models/File.kt @@ -128,22 +128,22 @@ data class File( fun from( map: Map, ) = File( - id = map["\$id"] as String, - bucketId = map["bucketId"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - permissions = map["\$permissions"] as List, - name = map["name"] as String, - folder = map["folder"] as String, - key = map["key"] as String, - signature = map["signature"] as String, - mimeType = map["mimeType"] as String, - sizeOriginal = (map["sizeOriginal"] as Number).toLong(), - sizeActual = (map["sizeActual"] as Number).toLong(), - chunksTotal = (map["chunksTotal"] as Number).toLong(), - chunksUploaded = (map["chunksUploaded"] as Number).toLong(), - encryption = map["encryption"] as Boolean, - compression = map["compression"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + bucketId = (if (map.containsKey("bucketId")) map["bucketId"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + permissions = (if (map.containsKey("\$permissions")) map["\$permissions"] else listOf()) as List, + name = (if (map.containsKey("name")) map["name"] else "") as String, + folder = (if (map.containsKey("folder")) map["folder"] else "") as String, + key = (if (map.containsKey("key")) map["key"] else "") as String, + signature = (if (map.containsKey("signature")) map["signature"] else "") as String, + mimeType = (if (map.containsKey("mimeType")) map["mimeType"] else "") as String, + sizeOriginal = ((if (map.containsKey("sizeOriginal")) map["sizeOriginal"] else 0) as Number).toLong(), + sizeActual = ((if (map.containsKey("sizeActual")) map["sizeActual"] else 0) as Number).toLong(), + chunksTotal = ((if (map.containsKey("chunksTotal")) map["chunksTotal"] else 0) as Number).toLong(), + chunksUploaded = ((if (map.containsKey("chunksUploaded")) map["chunksUploaded"] else 0) as Number).toLong(), + encryption = (if (map.containsKey("encryption")) map["encryption"] else false) as Boolean, + compression = (if (map.containsKey("compression")) map["compression"] else "none") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/FileList.kt b/library/src/main/java/io/appwrite/models/FileList.kt index 7a462dc7..1a84a460 100644 --- a/library/src/main/java/io/appwrite/models/FileList.kt +++ b/library/src/main/java/io/appwrite/models/FileList.kt @@ -30,8 +30,8 @@ data class FileList( fun from( map: Map, ) = FileList( - total = (map["total"] as Number).toLong(), - files = (map["files"] as List>).map { File.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + files = ((if (map.containsKey("files")) map["files"] else listOf()) as List>).map { File.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Headers.kt b/library/src/main/java/io/appwrite/models/Headers.kt index 4fece1e4..d2c27770 100644 --- a/library/src/main/java/io/appwrite/models/Headers.kt +++ b/library/src/main/java/io/appwrite/models/Headers.kt @@ -30,8 +30,8 @@ data class Headers( fun from( map: Map, ) = Headers( - name = map["name"] as String, - value = map["value"] as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + value = (if (map.containsKey("value")) map["value"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Identity.kt b/library/src/main/java/io/appwrite/models/Identity.kt index 011c32e4..0269400f 100644 --- a/library/src/main/java/io/appwrite/models/Identity.kt +++ b/library/src/main/java/io/appwrite/models/Identity.kt @@ -93,17 +93,17 @@ data class Identity( fun from( map: Map, ) = Identity( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - userId = map["userId"] as String, - provider = map["provider"] as String, - providerUid = map["providerUid"] as String, - providerEmail = map["providerEmail"] as String, - providerAccessToken = map["providerAccessToken"] as String, - providerAccessTokenExpiry = map["providerAccessTokenExpiry"] as String, - providerRefreshToken = map["providerRefreshToken"] as String, - providerIdToken = map["providerIdToken"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + provider = (if (map.containsKey("provider")) map["provider"] else "") as String, + providerUid = (if (map.containsKey("providerUid")) map["providerUid"] else "") as String, + providerEmail = (if (map.containsKey("providerEmail")) map["providerEmail"] else "") as String, + providerAccessToken = (if (map.containsKey("providerAccessToken")) map["providerAccessToken"] else "") as String, + providerAccessTokenExpiry = (if (map.containsKey("providerAccessTokenExpiry")) map["providerAccessTokenExpiry"] else "") as String, + providerRefreshToken = (if (map.containsKey("providerRefreshToken")) map["providerRefreshToken"] else "") as String, + providerIdToken = (if (map.containsKey("providerIdToken")) map["providerIdToken"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/IdentityList.kt b/library/src/main/java/io/appwrite/models/IdentityList.kt index b92f4e6a..31e5b0ea 100644 --- a/library/src/main/java/io/appwrite/models/IdentityList.kt +++ b/library/src/main/java/io/appwrite/models/IdentityList.kt @@ -30,8 +30,8 @@ data class IdentityList( fun from( map: Map, ) = IdentityList( - total = (map["total"] as Number).toLong(), - identities = (map["identities"] as List>).map { Identity.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + identities = ((if (map.containsKey("identities")) map["identities"] else listOf()) as List>).map { Identity.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Jwt.kt b/library/src/main/java/io/appwrite/models/Jwt.kt index c188a853..7b27865e 100644 --- a/library/src/main/java/io/appwrite/models/Jwt.kt +++ b/library/src/main/java/io/appwrite/models/Jwt.kt @@ -23,7 +23,7 @@ data class Jwt( fun from( map: Map, ) = Jwt( - jwt = map["jwt"] as String, + jwt = (if (map.containsKey("jwt")) map["jwt"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Language.kt b/library/src/main/java/io/appwrite/models/Language.kt index be1f30e8..01eb45bd 100644 --- a/library/src/main/java/io/appwrite/models/Language.kt +++ b/library/src/main/java/io/appwrite/models/Language.kt @@ -37,9 +37,9 @@ data class Language( fun from( map: Map, ) = Language( - name = map["name"] as String, - code = map["code"] as String, - nativeName = map["nativeName"] as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + code = (if (map.containsKey("code")) map["code"] else "") as String, + nativeName = (if (map.containsKey("nativeName")) map["nativeName"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/LanguageList.kt b/library/src/main/java/io/appwrite/models/LanguageList.kt index e1b0e6e7..86f6aed4 100644 --- a/library/src/main/java/io/appwrite/models/LanguageList.kt +++ b/library/src/main/java/io/appwrite/models/LanguageList.kt @@ -30,8 +30,8 @@ data class LanguageList( fun from( map: Map, ) = LanguageList( - total = (map["total"] as Number).toLong(), - languages = (map["languages"] as List>).map { Language.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + languages = ((if (map.containsKey("languages")) map["languages"] else listOf()) as List>).map { Language.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Locale.kt b/library/src/main/java/io/appwrite/models/Locale.kt index 689de89c..dc6a1ad8 100644 --- a/library/src/main/java/io/appwrite/models/Locale.kt +++ b/library/src/main/java/io/appwrite/models/Locale.kt @@ -142,13 +142,13 @@ data class Locale( fun from( map: Map, ) = Locale( - ip = map["ip"] as String, - countryCode = map["countryCode"] as String, - country = map["country"] as String, - continentCode = map["continentCode"] as String, - continent = map["continent"] as String, - eu = map["eu"] as Boolean, - currency = map["currency"] as String, + ip = (if (map.containsKey("ip")) map["ip"] else "") as String, + countryCode = (if (map.containsKey("countryCode")) map["countryCode"] else "") as String, + country = (if (map.containsKey("country")) map["country"] else "") as String, + continentCode = (if (map.containsKey("continentCode")) map["continentCode"] else "") as String, + continent = (if (map.containsKey("continent")) map["continent"] else "") as String, + eu = (if (map.containsKey("eu")) map["eu"] else false) as Boolean, + currency = (if (map.containsKey("currency")) map["currency"] else "") as String, city = map["city"] as? String, timeZone = map["timeZone"] as? String, postalCode = map["postalCode"] as? String, diff --git a/library/src/main/java/io/appwrite/models/LocaleCode.kt b/library/src/main/java/io/appwrite/models/LocaleCode.kt index 602d9ed9..3bee5288 100644 --- a/library/src/main/java/io/appwrite/models/LocaleCode.kt +++ b/library/src/main/java/io/appwrite/models/LocaleCode.kt @@ -30,8 +30,8 @@ data class LocaleCode( fun from( map: Map, ) = LocaleCode( - code = map["code"] as String, - name = map["name"] as String, + code = (if (map.containsKey("code")) map["code"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/LocaleCodeList.kt b/library/src/main/java/io/appwrite/models/LocaleCodeList.kt index e0c568a7..86cb808f 100644 --- a/library/src/main/java/io/appwrite/models/LocaleCodeList.kt +++ b/library/src/main/java/io/appwrite/models/LocaleCodeList.kt @@ -30,8 +30,8 @@ data class LocaleCodeList( fun from( map: Map, ) = LocaleCodeList( - total = (map["total"] as Number).toLong(), - localeCodes = (map["localeCodes"] as List>).map { LocaleCode.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + localeCodes = ((if (map.containsKey("localeCodes")) map["localeCodes"] else listOf()) as List>).map { LocaleCode.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Membership.kt b/library/src/main/java/io/appwrite/models/Membership.kt index 7d5b963b..3d0c37b9 100644 --- a/library/src/main/java/io/appwrite/models/Membership.kt +++ b/library/src/main/java/io/appwrite/models/Membership.kt @@ -121,21 +121,21 @@ data class Membership( fun from( map: Map, ) = Membership( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - userId = map["userId"] as String, - userName = map["userName"] as String, - userEmail = map["userEmail"] as String, - userPhone = map["userPhone"] as String, - teamId = map["teamId"] as String, - teamName = map["teamName"] as String, - invited = map["invited"] as String, - joined = map["joined"] as String, - confirm = map["confirm"] as Boolean, - mfa = map["mfa"] as Boolean, - userAccessedAt = map["userAccessedAt"] as String, - roles = map["roles"] as List, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + userName = (if (map.containsKey("userName")) map["userName"] else "") as String, + userEmail = (if (map.containsKey("userEmail")) map["userEmail"] else "") as String, + userPhone = (if (map.containsKey("userPhone")) map["userPhone"] else "") as String, + teamId = (if (map.containsKey("teamId")) map["teamId"] else "") as String, + teamName = (if (map.containsKey("teamName")) map["teamName"] else "") as String, + invited = (if (map.containsKey("invited")) map["invited"] else "") as String, + joined = (if (map.containsKey("joined")) map["joined"] else "") as String, + confirm = (if (map.containsKey("confirm")) map["confirm"] else false) as Boolean, + mfa = (if (map.containsKey("mfa")) map["mfa"] else false) as Boolean, + userAccessedAt = (if (map.containsKey("userAccessedAt")) map["userAccessedAt"] else "") as String, + roles = (if (map.containsKey("roles")) map["roles"] else listOf()) as List, ) } } diff --git a/library/src/main/java/io/appwrite/models/MembershipList.kt b/library/src/main/java/io/appwrite/models/MembershipList.kt index 8376bc96..f6f4afe7 100644 --- a/library/src/main/java/io/appwrite/models/MembershipList.kt +++ b/library/src/main/java/io/appwrite/models/MembershipList.kt @@ -30,8 +30,8 @@ data class MembershipList( fun from( map: Map, ) = MembershipList( - total = (map["total"] as Number).toLong(), - memberships = (map["memberships"] as List>).map { Membership.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + memberships = ((if (map.containsKey("memberships")) map["memberships"] else listOf()) as List>).map { Membership.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/MfaChallenge.kt b/library/src/main/java/io/appwrite/models/MfaChallenge.kt index b3e9f6bd..c17e792d 100644 --- a/library/src/main/java/io/appwrite/models/MfaChallenge.kt +++ b/library/src/main/java/io/appwrite/models/MfaChallenge.kt @@ -44,10 +44,10 @@ data class MfaChallenge( fun from( map: Map, ) = MfaChallenge( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - userId = map["userId"] as String, - expire = map["expire"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/MfaFactors.kt b/library/src/main/java/io/appwrite/models/MfaFactors.kt index 20f7bb36..42872fa2 100644 --- a/library/src/main/java/io/appwrite/models/MfaFactors.kt +++ b/library/src/main/java/io/appwrite/models/MfaFactors.kt @@ -51,11 +51,11 @@ data class MfaFactors( fun from( map: Map, ) = MfaFactors( - totp = map["totp"] as Boolean, - phone = map["phone"] as Boolean, - email = map["email"] as Boolean, - recoveryCode = map["recoveryCode"] as Boolean, - custom = map["custom"] as Boolean, + totp = (if (map.containsKey("totp")) map["totp"] else false) as Boolean, + phone = (if (map.containsKey("phone")) map["phone"] else false) as Boolean, + email = (if (map.containsKey("email")) map["email"] else false) as Boolean, + recoveryCode = (if (map.containsKey("recoveryCode")) map["recoveryCode"] else false) as Boolean, + custom = (if (map.containsKey("custom")) map["custom"] else false) as Boolean, ) } } diff --git a/library/src/main/java/io/appwrite/models/MfaRecoveryCodes.kt b/library/src/main/java/io/appwrite/models/MfaRecoveryCodes.kt index a8967496..25e783cd 100644 --- a/library/src/main/java/io/appwrite/models/MfaRecoveryCodes.kt +++ b/library/src/main/java/io/appwrite/models/MfaRecoveryCodes.kt @@ -23,7 +23,7 @@ data class MfaRecoveryCodes( fun from( map: Map, ) = MfaRecoveryCodes( - recoveryCodes = map["recoveryCodes"] as List, + recoveryCodes = (if (map.containsKey("recoveryCodes")) map["recoveryCodes"] else listOf()) as List, ) } } diff --git a/library/src/main/java/io/appwrite/models/MfaType.kt b/library/src/main/java/io/appwrite/models/MfaType.kt index e20b7f66..5b7b9710 100644 --- a/library/src/main/java/io/appwrite/models/MfaType.kt +++ b/library/src/main/java/io/appwrite/models/MfaType.kt @@ -30,8 +30,8 @@ data class MfaType( fun from( map: Map, ) = MfaType( - secret = map["secret"] as String, - uri = map["uri"] as String, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + uri = (if (map.containsKey("uri")) map["uri"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Approve.kt b/library/src/main/java/io/appwrite/models/Oauth2Approve.kt index e09ab9ea..0aabf4de 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Approve.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Approve.kt @@ -23,7 +23,7 @@ data class Oauth2Approve( fun from( map: Map, ) = Oauth2Approve( - redirectUrl = map["redirectUrl"] as String, + redirectUrl = (if (map.containsKey("redirectUrl")) map["redirectUrl"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Authorize.kt b/library/src/main/java/io/appwrite/models/Oauth2Authorize.kt index e0812f7e..fe878e3a 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Authorize.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Authorize.kt @@ -30,8 +30,8 @@ data class Oauth2Authorize( fun from( map: Map, ) = Oauth2Authorize( - grantId = map["grantId"] as String, - redirectUrl = map["redirectUrl"] as String, + grantId = (if (map.containsKey("grantId")) map["grantId"] else "") as String, + redirectUrl = (if (map.containsKey("redirectUrl")) map["redirectUrl"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Consent.kt b/library/src/main/java/io/appwrite/models/Oauth2Consent.kt index 7769e9b1..28cde915 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Consent.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Consent.kt @@ -86,16 +86,16 @@ data class Oauth2Consent( fun from( map: Map, ) = Oauth2Consent( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - userId = map["userId"] as String, - appId = map["appId"] as String, - cimdUrl = map["cimdUrl"] as String, - scopes = map["scopes"] as List, - resources = map["resources"] as List, - authorizationDetails = map["authorizationDetails"] as String, - expire = map["expire"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + cimdUrl = (if (map.containsKey("cimdUrl")) map["cimdUrl"] else "") as String, + scopes = (if (map.containsKey("scopes")) map["scopes"] else listOf()) as List, + resources = (if (map.containsKey("resources")) map["resources"] else listOf()) as List, + authorizationDetails = (if (map.containsKey("authorizationDetails")) map["authorizationDetails"] else "[]") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2ConsentList.kt b/library/src/main/java/io/appwrite/models/Oauth2ConsentList.kt index da6acd38..2a6337bd 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2ConsentList.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2ConsentList.kt @@ -30,8 +30,8 @@ data class Oauth2ConsentList( fun from( map: Map, ) = Oauth2ConsentList( - total = (map["total"] as Number).toLong(), - consents = (map["consents"] as List>).map { Oauth2Consent.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + consents = ((if (map.containsKey("consents")) map["consents"] else listOf()) as List>).map { Oauth2Consent.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2ConsentToken.kt b/library/src/main/java/io/appwrite/models/Oauth2ConsentToken.kt index 31f6249b..1f5c1dfc 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2ConsentToken.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2ConsentToken.kt @@ -93,17 +93,17 @@ data class Oauth2ConsentToken( fun from( map: Map, ) = Oauth2ConsentToken( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - consentId = map["consentId"] as String, - userId = map["userId"] as String, - appId = map["appId"] as String, - cimdUrl = map["cimdUrl"] as String, - scopes = map["scopes"] as List, - resources = map["resources"] as List, - authorizationDetails = map["authorizationDetails"] as String, - expire = map["expire"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + consentId = (if (map.containsKey("consentId")) map["consentId"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + cimdUrl = (if (map.containsKey("cimdUrl")) map["cimdUrl"] else "") as String, + scopes = (if (map.containsKey("scopes")) map["scopes"] else listOf()) as List, + resources = (if (map.containsKey("resources")) map["resources"] else listOf()) as List, + authorizationDetails = (if (map.containsKey("authorizationDetails")) map["authorizationDetails"] else "[]") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2ConsentTokenList.kt b/library/src/main/java/io/appwrite/models/Oauth2ConsentTokenList.kt index 09278f45..b5d4c524 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2ConsentTokenList.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2ConsentTokenList.kt @@ -30,8 +30,8 @@ data class Oauth2ConsentTokenList( fun from( map: Map, ) = Oauth2ConsentTokenList( - total = (map["total"] as Number).toLong(), - tokens = (map["tokens"] as List>).map { Oauth2ConsentToken.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + tokens = ((if (map.containsKey("tokens")) map["tokens"] else listOf()) as List>).map { Oauth2ConsentToken.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2DeviceAuthorization.kt b/library/src/main/java/io/appwrite/models/Oauth2DeviceAuthorization.kt index 58c7e5ee..f4ca6e64 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2DeviceAuthorization.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2DeviceAuthorization.kt @@ -58,12 +58,12 @@ data class Oauth2DeviceAuthorization( fun from( map: Map, ) = Oauth2DeviceAuthorization( - device_code = map["device_code"] as String, - user_code = map["user_code"] as String, - verification_uri = map["verification_uri"] as String, - verification_uri_complete = map["verification_uri_complete"] as String, - expires_in = (map["expires_in"] as Number).toLong(), - interval = (map["interval"] as Number).toLong(), + device_code = (if (map.containsKey("device_code")) map["device_code"] else "") as String, + user_code = (if (map.containsKey("user_code")) map["user_code"] else "") as String, + verification_uri = (if (map.containsKey("verification_uri")) map["verification_uri"] else "") as String, + verification_uri_complete = (if (map.containsKey("verification_uri_complete")) map["verification_uri_complete"] else "") as String, + expires_in = ((if (map.containsKey("expires_in")) map["expires_in"] else 0) as Number).toLong(), + interval = ((if (map.containsKey("interval")) map["interval"] else 0) as Number).toLong(), ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Grant.kt b/library/src/main/java/io/appwrite/models/Oauth2Grant.kt index ef1a7c39..914b6f33 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Grant.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Grant.kt @@ -100,18 +100,18 @@ data class Oauth2Grant( fun from( map: Map, ) = Oauth2Grant( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - userId = map["userId"] as String, - appId = map["appId"] as String, - scopes = map["scopes"] as List, - resources = map["resources"] as List, - authorizationDetails = map["authorizationDetails"] as String, - prompt = map["prompt"] as String, - redirectUri = map["redirectUri"] as String, - authTime = (map["authTime"] as Number).toLong(), - expire = map["expire"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + scopes = (if (map.containsKey("scopes")) map["scopes"] else listOf()) as List, + resources = (if (map.containsKey("resources")) map["resources"] else listOf()) as List, + authorizationDetails = (if (map.containsKey("authorizationDetails")) map["authorizationDetails"] else "[]") as String, + prompt = (if (map.containsKey("prompt")) map["prompt"] else "") as String, + redirectUri = (if (map.containsKey("redirectUri")) map["redirectUri"] else "") as String, + authTime = ((if (map.containsKey("authTime")) map["authTime"] else 0) as Number).toLong(), + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Organization.kt b/library/src/main/java/io/appwrite/models/Oauth2Organization.kt index 605916a0..47b6a8ff 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Organization.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Organization.kt @@ -23,7 +23,7 @@ data class Oauth2Organization( fun from( map: Map, ) = Oauth2Organization( - id = map["\$id"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2OrganizationList.kt b/library/src/main/java/io/appwrite/models/Oauth2OrganizationList.kt index b90ee636..48f45063 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2OrganizationList.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2OrganizationList.kt @@ -30,8 +30,8 @@ data class Oauth2OrganizationList( fun from( map: Map, ) = Oauth2OrganizationList( - total = (map["total"] as Number).toLong(), - organizations = (map["organizations"] as List>).map { Oauth2Organization.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + organizations = ((if (map.containsKey("organizations")) map["organizations"] else listOf()) as List>).map { Oauth2Organization.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2PAR.kt b/library/src/main/java/io/appwrite/models/Oauth2PAR.kt index 94d175ee..1091bdd5 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2PAR.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2PAR.kt @@ -30,8 +30,8 @@ data class Oauth2PAR( fun from( map: Map, ) = Oauth2PAR( - request_uri = map["request_uri"] as String, - expires_in = (map["expires_in"] as Number).toLong(), + request_uri = (if (map.containsKey("request_uri")) map["request_uri"] else "") as String, + expires_in = ((if (map.containsKey("expires_in")) map["expires_in"] else 0) as Number).toLong(), ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Project.kt b/library/src/main/java/io/appwrite/models/Oauth2Project.kt index eb085dce..95c79e0b 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Project.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Project.kt @@ -37,9 +37,9 @@ data class Oauth2Project( fun from( map: Map, ) = Oauth2Project( - id = map["\$id"] as String, - region = map["region"] as String, - endpoint = map["endpoint"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + region = (if (map.containsKey("region")) map["region"] else "") as String, + endpoint = (if (map.containsKey("endpoint")) map["endpoint"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2ProjectList.kt b/library/src/main/java/io/appwrite/models/Oauth2ProjectList.kt index 551b9ee4..5eb46ed5 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2ProjectList.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2ProjectList.kt @@ -30,8 +30,8 @@ data class Oauth2ProjectList( fun from( map: Map, ) = Oauth2ProjectList( - total = (map["total"] as Number).toLong(), - projects = (map["projects"] as List>).map { Oauth2Project.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + projects = ((if (map.containsKey("projects")) map["projects"] else listOf()) as List>).map { Oauth2Project.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Reject.kt b/library/src/main/java/io/appwrite/models/Oauth2Reject.kt index b23f308c..42b3a3a3 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Reject.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Reject.kt @@ -23,7 +23,7 @@ data class Oauth2Reject( fun from( map: Map, ) = Oauth2Reject( - redirectUrl = map["redirectUrl"] as String, + redirectUrl = (if (map.containsKey("redirectUrl")) map["redirectUrl"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Token.kt b/library/src/main/java/io/appwrite/models/Oauth2Token.kt index f36f7768..53547c23 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Token.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Token.kt @@ -65,11 +65,11 @@ data class Oauth2Token( fun from( map: Map, ) = Oauth2Token( - access_token = map["access_token"] as String, - token_type = map["token_type"] as String, - expires_in = (map["expires_in"] as Number).toLong(), - refresh_token = map["refresh_token"] as String, - scope = map["scope"] as String, + access_token = (if (map.containsKey("access_token")) map["access_token"] else "") as String, + token_type = (if (map.containsKey("token_type")) map["token_type"] else "Bearer") as String, + expires_in = ((if (map.containsKey("expires_in")) map["expires_in"] else 0) as Number).toLong(), + refresh_token = (if (map.containsKey("refresh_token")) map["refresh_token"] else "") as String, + scope = (if (map.containsKey("scope")) map["scope"] else "") as String, authorization_details = map["authorization_details"] as? String, id_token = map["id_token"] as? String, ) diff --git a/library/src/main/java/io/appwrite/models/Passkey.kt b/library/src/main/java/io/appwrite/models/Passkey.kt new file mode 100644 index 00000000..90f0e497 --- /dev/null +++ b/library/src/main/java/io/appwrite/models/Passkey.kt @@ -0,0 +1,61 @@ +package io.appwrite.models + +import com.google.gson.annotations.SerializedName +import io.appwrite.extensions.jsonCast + +/** + * Passkey + */ +data class Passkey( + /** + * Passkey ID. + */ + @SerializedName("\$id") + val id: String, + + /** + * Passkey creation date in ISO 8601 format. + */ + @SerializedName("\$createdAt") + val createdAt: String, + + /** + * Passkey update date in ISO 8601 format. + */ + @SerializedName("\$updatedAt") + val updatedAt: String, + + /** + * Passkey name. + */ + @SerializedName("name") + val name: String, + + /** + * Most recent sign-in with this passkey in ISO 8601 format. Empty until the passkey is used to sign in. + */ + @SerializedName("accessedAt") + val accessedAt: String, + +) { + fun toMap(): Map = mapOf( + "\$id" to id as Any, + "\$createdAt" to createdAt as Any, + "\$updatedAt" to updatedAt as Any, + "name" to name as Any, + "accessedAt" to accessedAt as Any, + ) + + companion object { + @Suppress("UNCHECKED_CAST") + fun from( + map: Map, + ) = Passkey( + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + accessedAt = (if (map.containsKey("accessedAt")) map["accessedAt"] else "") as String, + ) + } +} diff --git a/library/src/main/java/io/appwrite/models/PasskeyChallenge.kt b/library/src/main/java/io/appwrite/models/PasskeyChallenge.kt new file mode 100644 index 00000000..dcdcb771 --- /dev/null +++ b/library/src/main/java/io/appwrite/models/PasskeyChallenge.kt @@ -0,0 +1,61 @@ +package io.appwrite.models + +import com.google.gson.annotations.SerializedName +import io.appwrite.extensions.jsonCast + +/** + * Passkey Challenge + */ +data class PasskeyChallenge( + /** + * Challenge ID. When signing in, pass it back with the credential to receive a token. + */ + @SerializedName("\$id") + val id: String, + + /** + * Challenge creation date in ISO 8601 format. + */ + @SerializedName("\$createdAt") + val createdAt: String, + + /** + * ID of the passkey being registered. Empty for sign-in challenges. + */ + @SerializedName("passkeyId") + val passkeyId: String, + + /** + * Challenge expiration date in ISO 8601 format. + */ + @SerializedName("expire") + val expire: String, + + /** + * WebAuthn options in JSON form. Pass them to `PublicKeyCredential.parseCreationOptionsFromJSON()` when registering, or `PublicKeyCredential.parseRequestOptionsFromJSON()` when signing in. + */ + @SerializedName("publicKey") + val publicKey: Map, + +) { + fun toMap(): Map = mapOf( + "\$id" to id as Any, + "\$createdAt" to createdAt as Any, + "passkeyId" to passkeyId as Any, + "expire" to expire as Any, + "publicKey" to publicKey as Any, + ) + + companion object { + @Suppress("UNCHECKED_CAST") + fun from( + map: Map, + ) = PasskeyChallenge( + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + passkeyId = (if (map.containsKey("passkeyId")) map["passkeyId"] else "") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, + publicKey = (if (map.containsKey("publicKey")) map["publicKey"] else mapOf()) as Map, + ) + } +} diff --git a/library/src/main/java/io/appwrite/models/PasskeyList.kt b/library/src/main/java/io/appwrite/models/PasskeyList.kt new file mode 100644 index 00000000..05692293 --- /dev/null +++ b/library/src/main/java/io/appwrite/models/PasskeyList.kt @@ -0,0 +1,37 @@ +package io.appwrite.models + +import com.google.gson.annotations.SerializedName +import io.appwrite.extensions.jsonCast + +/** + * Passkeys List + */ +data class PasskeyList( + /** + * Total number of passkeys that matched your query. + */ + @SerializedName("total") + val total: Long, + + /** + * List of passkeys. + */ + @SerializedName("passkeys") + val passkeys: List, + +) { + fun toMap(): Map = mapOf( + "total" to total as Any, + "passkeys" to passkeys.map { it.toMap() } as Any, + ) + + companion object { + @Suppress("UNCHECKED_CAST") + fun from( + map: Map, + ) = PasskeyList( + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + passkeys = ((if (map.containsKey("passkeys")) map["passkeys"] else listOf()) as List>).map { Passkey.from(map = it) }, + ) + } +} diff --git a/library/src/main/java/io/appwrite/models/Phone.kt b/library/src/main/java/io/appwrite/models/Phone.kt index 74bfa518..12c8eb63 100644 --- a/library/src/main/java/io/appwrite/models/Phone.kt +++ b/library/src/main/java/io/appwrite/models/Phone.kt @@ -37,9 +37,9 @@ data class Phone( fun from( map: Map, ) = Phone( - code = map["code"] as String, - countryCode = map["countryCode"] as String, - countryName = map["countryName"] as String, + code = (if (map.containsKey("code")) map["code"] else "") as String, + countryCode = (if (map.containsKey("countryCode")) map["countryCode"] else "") as String, + countryName = (if (map.containsKey("countryName")) map["countryName"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/PhoneList.kt b/library/src/main/java/io/appwrite/models/PhoneList.kt index fc0ffa3c..2c3f9cd1 100644 --- a/library/src/main/java/io/appwrite/models/PhoneList.kt +++ b/library/src/main/java/io/appwrite/models/PhoneList.kt @@ -30,8 +30,8 @@ data class PhoneList( fun from( map: Map, ) = PhoneList( - total = (map["total"] as Number).toLong(), - phones = (map["phones"] as List>).map { Phone.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + phones = ((if (map.containsKey("phones")) map["phones"] else listOf()) as List>).map { Phone.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Presence.kt b/library/src/main/java/io/appwrite/models/Presence.kt index c7f60731..b8e27fc0 100644 --- a/library/src/main/java/io/appwrite/models/Presence.kt +++ b/library/src/main/java/io/appwrite/models/Presence.kt @@ -79,13 +79,13 @@ data class Presence( fun from( map: Map, ) = Presence( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - permissions = map["\$permissions"] as List, - userId = map["userId"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + permissions = (if (map.containsKey("\$permissions")) map["\$permissions"] else listOf()) as List, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, status = map["status"] as? String, - source = map["source"] as String, + source = (if (map.containsKey("source")) map["source"] else "") as String, expiresAt = map["expiresAt"] as? String, metadata = map["metadata"] as? Map, ) diff --git a/library/src/main/java/io/appwrite/models/PresenceList.kt b/library/src/main/java/io/appwrite/models/PresenceList.kt index b69d3e13..01a20264 100644 --- a/library/src/main/java/io/appwrite/models/PresenceList.kt +++ b/library/src/main/java/io/appwrite/models/PresenceList.kt @@ -30,8 +30,8 @@ data class PresenceList( fun from( map: Map, ) = PresenceList( - total = (map["total"] as Number).toLong(), - presences = (map["presences"] as List>).map { Presence.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + presences = ((if (map.containsKey("presences")) map["presences"] else listOf()) as List>).map { Presence.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Row.kt b/library/src/main/java/io/appwrite/models/Row.kt index 1f190104..d0968469 100644 --- a/library/src/main/java/io/appwrite/models/Row.kt +++ b/library/src/main/java/io/appwrite/models/Row.kt @@ -92,13 +92,13 @@ data class Row( map: Map, nestedType: Class ) = Row( - id = map["\$id"] as String, - sequence = map["\$sequence"] as String, - tableId = map["\$tableId"] as String, - databaseId = map["\$databaseId"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - permissions = map["\$permissions"] as List, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + sequence = (if (map.containsKey("\$sequence")) map["\$sequence"] else "") as String, + tableId = (if (map.containsKey("\$tableId")) map["\$tableId"] else "") as String, + databaseId = (if (map.containsKey("\$databaseId")) map["\$databaseId"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + permissions = (if (map.containsKey("\$permissions")) map["\$permissions"] else listOf()) as List, data = map["data"]?.jsonCast(to = nestedType) ?: map.jsonCast(to = nestedType) ) } diff --git a/library/src/main/java/io/appwrite/models/RowList.kt b/library/src/main/java/io/appwrite/models/RowList.kt index f9c8f61e..0d52602c 100644 --- a/library/src/main/java/io/appwrite/models/RowList.kt +++ b/library/src/main/java/io/appwrite/models/RowList.kt @@ -39,8 +39,8 @@ data class RowList( map: Map, nestedType: Class ) = RowList( - total = (map["total"] as Number).toLong(), - rows = (map["rows"] as List>).map { Row.from(map = it, nestedType) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + rows = ((if (map.containsKey("rows")) map["rows"] else listOf()) as List>).map { Row.from(map = it, nestedType) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Session.kt b/library/src/main/java/io/appwrite/models/Session.kt index c9c39509..884d5814 100644 --- a/library/src/main/java/io/appwrite/models/Session.kt +++ b/library/src/main/java/io/appwrite/models/Session.kt @@ -219,35 +219,35 @@ data class Session( fun from( map: Map, ) = Session( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - userId = map["userId"] as String, - expire = map["expire"] as String, - provider = map["provider"] as String, - providerUid = map["providerUid"] as String, - providerAccessToken = map["providerAccessToken"] as String, - providerAccessTokenExpiry = map["providerAccessTokenExpiry"] as String, - providerRefreshToken = map["providerRefreshToken"] as String, - ip = map["ip"] as String, - osCode = map["osCode"] as String, - osName = map["osName"] as String, - osVersion = map["osVersion"] as String, - clientType = map["clientType"] as String, - clientCode = map["clientCode"] as String, - clientName = map["clientName"] as String, - clientVersion = map["clientVersion"] as String, - clientEngine = map["clientEngine"] as String, - clientEngineVersion = map["clientEngineVersion"] as String, - deviceName = map["deviceName"] as String, - deviceBrand = map["deviceBrand"] as String, - deviceModel = map["deviceModel"] as String, - countryCode = map["countryCode"] as String, - countryName = map["countryName"] as String, - current = map["current"] as Boolean, - factors = map["factors"] as List, - secret = map["secret"] as String, - mfaUpdatedAt = map["mfaUpdatedAt"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, + provider = (if (map.containsKey("provider")) map["provider"] else "") as String, + providerUid = (if (map.containsKey("providerUid")) map["providerUid"] else "") as String, + providerAccessToken = (if (map.containsKey("providerAccessToken")) map["providerAccessToken"] else "") as String, + providerAccessTokenExpiry = (if (map.containsKey("providerAccessTokenExpiry")) map["providerAccessTokenExpiry"] else "") as String, + providerRefreshToken = (if (map.containsKey("providerRefreshToken")) map["providerRefreshToken"] else "") as String, + ip = (if (map.containsKey("ip")) map["ip"] else "") as String, + osCode = (if (map.containsKey("osCode")) map["osCode"] else "") as String, + osName = (if (map.containsKey("osName")) map["osName"] else "") as String, + osVersion = (if (map.containsKey("osVersion")) map["osVersion"] else "") as String, + clientType = (if (map.containsKey("clientType")) map["clientType"] else "") as String, + clientCode = (if (map.containsKey("clientCode")) map["clientCode"] else "") as String, + clientName = (if (map.containsKey("clientName")) map["clientName"] else "") as String, + clientVersion = (if (map.containsKey("clientVersion")) map["clientVersion"] else "") as String, + clientEngine = (if (map.containsKey("clientEngine")) map["clientEngine"] else "") as String, + clientEngineVersion = (if (map.containsKey("clientEngineVersion")) map["clientEngineVersion"] else "") as String, + deviceName = (if (map.containsKey("deviceName")) map["deviceName"] else "") as String, + deviceBrand = (if (map.containsKey("deviceBrand")) map["deviceBrand"] else "") as String, + deviceModel = (if (map.containsKey("deviceModel")) map["deviceModel"] else "") as String, + countryCode = (if (map.containsKey("countryCode")) map["countryCode"] else "") as String, + countryName = (if (map.containsKey("countryName")) map["countryName"] else "") as String, + current = (if (map.containsKey("current")) map["current"] else false) as Boolean, + factors = (if (map.containsKey("factors")) map["factors"] else listOf()) as List, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + mfaUpdatedAt = (if (map.containsKey("mfaUpdatedAt")) map["mfaUpdatedAt"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/SessionList.kt b/library/src/main/java/io/appwrite/models/SessionList.kt index bda39fca..9be215ba 100644 --- a/library/src/main/java/io/appwrite/models/SessionList.kt +++ b/library/src/main/java/io/appwrite/models/SessionList.kt @@ -30,8 +30,8 @@ data class SessionList( fun from( map: Map, ) = SessionList( - total = (map["total"] as Number).toLong(), - sessions = (map["sessions"] as List>).map { Session.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + sessions = ((if (map.containsKey("sessions")) map["sessions"] else listOf()) as List>).map { Session.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Subscriber.kt b/library/src/main/java/io/appwrite/models/Subscriber.kt index 931b65db..adcd8c53 100644 --- a/library/src/main/java/io/appwrite/models/Subscriber.kt +++ b/library/src/main/java/io/appwrite/models/Subscriber.kt @@ -79,15 +79,15 @@ data class Subscriber( fun from( map: Map, ) = Subscriber( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - targetId = map["targetId"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + targetId = (if (map.containsKey("targetId")) map["targetId"] else "") as String, target = Target.from(map = map["target"] as Map), - userId = map["userId"] as String, - userName = map["userName"] as String, - topicId = map["topicId"] as String, - providerType = map["providerType"] as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + userName = (if (map.containsKey("userName")) map["userName"] else "") as String, + topicId = (if (map.containsKey("topicId")) map["topicId"] else "") as String, + providerType = (if (map.containsKey("providerType")) map["providerType"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Target.kt b/library/src/main/java/io/appwrite/models/Target.kt index 3e89e08d..d67a716c 100644 --- a/library/src/main/java/io/appwrite/models/Target.kt +++ b/library/src/main/java/io/appwrite/models/Target.kt @@ -79,15 +79,15 @@ data class Target( fun from( map: Map, ) = Target( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - name = map["name"] as String, - userId = map["userId"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, providerId = map["providerId"] as? String, - providerType = map["providerType"] as String, - identifier = map["identifier"] as String, - expired = map["expired"] as Boolean, + providerType = (if (map.containsKey("providerType")) map["providerType"] else "") as String, + identifier = (if (map.containsKey("identifier")) map["identifier"] else "") as String, + expired = (if (map.containsKey("expired")) map["expired"] else false) as Boolean, ) } } diff --git a/library/src/main/java/io/appwrite/models/Team.kt b/library/src/main/java/io/appwrite/models/Team.kt index 6cd7dbc9..dc780c8b 100644 --- a/library/src/main/java/io/appwrite/models/Team.kt +++ b/library/src/main/java/io/appwrite/models/Team.kt @@ -75,11 +75,11 @@ data class Team( map: Map, nestedType: Class ) = Team( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - name = map["name"] as String, - total = (map["total"] as Number).toLong(), + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), prefs = Preferences.from(map = map["prefs"] as Map, nestedType), ) } diff --git a/library/src/main/java/io/appwrite/models/TeamList.kt b/library/src/main/java/io/appwrite/models/TeamList.kt index 8e490f51..db4e1eb2 100644 --- a/library/src/main/java/io/appwrite/models/TeamList.kt +++ b/library/src/main/java/io/appwrite/models/TeamList.kt @@ -39,8 +39,8 @@ data class TeamList( map: Map, nestedType: Class ) = TeamList( - total = (map["total"] as Number).toLong(), - teams = (map["teams"] as List>).map { Team.from(map = it, nestedType) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + teams = ((if (map.containsKey("teams")) map["teams"] else listOf()) as List>).map { Team.from(map = it, nestedType) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Token.kt b/library/src/main/java/io/appwrite/models/Token.kt index 625e3085..c3762397 100644 --- a/library/src/main/java/io/appwrite/models/Token.kt +++ b/library/src/main/java/io/appwrite/models/Token.kt @@ -58,12 +58,12 @@ data class Token( fun from( map: Map, ) = Token( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - userId = map["userId"] as String, - secret = map["secret"] as String, - expire = map["expire"] as String, - phrase = map["phrase"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, + phrase = (if (map.containsKey("phrase")) map["phrase"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/TokenSecret.kt b/library/src/main/java/io/appwrite/models/TokenSecret.kt new file mode 100644 index 00000000..fe2dd92c --- /dev/null +++ b/library/src/main/java/io/appwrite/models/TokenSecret.kt @@ -0,0 +1,69 @@ +package io.appwrite.models + +import com.google.gson.annotations.SerializedName +import io.appwrite.extensions.jsonCast + +/** + * Token Secret + */ +data class TokenSecret( + /** + * Token ID. + */ + @SerializedName("\$id") + val id: String, + + /** + * Token creation date in ISO 8601 format. + */ + @SerializedName("\$createdAt") + val createdAt: String, + + /** + * User ID. + */ + @SerializedName("userId") + val userId: String, + + /** + * Token secret key. Exchange it for a session with the create session endpoint. + */ + @SerializedName("secret") + val secret: String, + + /** + * Token expiration date in ISO 8601 format. + */ + @SerializedName("expire") + val expire: String, + + /** + * Security phrase of a token. Empty if security phrase was not requested when creating a token. It includes randomly generated phrase which is also sent in the external resource such as email. + */ + @SerializedName("phrase") + val phrase: String, + +) { + fun toMap(): Map = mapOf( + "\$id" to id as Any, + "\$createdAt" to createdAt as Any, + "userId" to userId as Any, + "secret" to secret as Any, + "expire" to expire as Any, + "phrase" to phrase as Any, + ) + + companion object { + @Suppress("UNCHECKED_CAST") + fun from( + map: Map, + ) = TokenSecret( + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, + phrase = (if (map.containsKey("phrase")) map["phrase"] else "") as String, + ) + } +} diff --git a/library/src/main/java/io/appwrite/models/Transaction.kt b/library/src/main/java/io/appwrite/models/Transaction.kt index 8f09860e..bb5f39b3 100644 --- a/library/src/main/java/io/appwrite/models/Transaction.kt +++ b/library/src/main/java/io/appwrite/models/Transaction.kt @@ -58,12 +58,12 @@ data class Transaction( fun from( map: Map, ) = Transaction( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - status = map["status"] as String, - operations = (map["operations"] as Number).toLong(), - expiresAt = map["expiresAt"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + status = (if (map.containsKey("status")) map["status"] else "pending") as String, + operations = ((if (map.containsKey("operations")) map["operations"] else 0) as Number).toLong(), + expiresAt = (if (map.containsKey("expiresAt")) map["expiresAt"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/TransactionList.kt b/library/src/main/java/io/appwrite/models/TransactionList.kt index c55a7317..963f3732 100644 --- a/library/src/main/java/io/appwrite/models/TransactionList.kt +++ b/library/src/main/java/io/appwrite/models/TransactionList.kt @@ -30,8 +30,8 @@ data class TransactionList( fun from( map: Map, ) = TransactionList( - total = (map["total"] as Number).toLong(), - transactions = (map["transactions"] as List>).map { Transaction.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + transactions = ((if (map.containsKey("transactions")) map["transactions"] else listOf()) as List>).map { Transaction.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/User.kt b/library/src/main/java/io/appwrite/models/User.kt index 0eea3b00..22b4aeb5 100644 --- a/library/src/main/java/io/appwrite/models/User.kt +++ b/library/src/main/java/io/appwrite/models/User.kt @@ -264,31 +264,31 @@ data class User( map: Map, nestedType: Class ) = User( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - name = map["name"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, password = map["password"] as? String, hash = map["hash"] as? String, hashOptions = map["hashOptions"] as? Map, - registration = map["registration"] as String, - status = map["status"] as Boolean, - labels = map["labels"] as List, - passwordUpdate = map["passwordUpdate"] as String, - email = map["email"] as String, - phone = map["phone"] as String, - emailVerification = map["emailVerification"] as Boolean, + registration = (if (map.containsKey("registration")) map["registration"] else "") as String, + status = (if (map.containsKey("status")) map["status"] else true) as Boolean, + labels = (if (map.containsKey("labels")) map["labels"] else listOf()) as List, + passwordUpdate = (if (map.containsKey("passwordUpdate")) map["passwordUpdate"] else "") as String, + email = (if (map.containsKey("email")) map["email"] else "") as String, + phone = (if (map.containsKey("phone")) map["phone"] else "") as String, + emailVerification = (if (map.containsKey("emailVerification")) map["emailVerification"] else false) as Boolean, emailCanonical = map["emailCanonical"] as? String, emailIsFree = map["emailIsFree"] as? Boolean, emailIsDisposable = map["emailIsDisposable"] as? Boolean, emailIsCorporate = map["emailIsCorporate"] as? Boolean, emailIsCanonical = map["emailIsCanonical"] as? Boolean, passwordPwned = map["passwordPwned"] as? Boolean, - phoneVerification = map["phoneVerification"] as Boolean, - mfa = map["mfa"] as Boolean, + phoneVerification = (if (map.containsKey("phoneVerification")) map["phoneVerification"] else false) as Boolean, + mfa = (if (map.containsKey("mfa")) map["mfa"] else false) as Boolean, prefs = Preferences.from(map = map["prefs"] as Map, nestedType), - targets = (map["targets"] as List>).map { Target.from(map = it) }, - accessedAt = map["accessedAt"] as String, + targets = ((if (map.containsKey("targets")) map["targets"] else listOf()) as List>).map { Target.from(map = it) }, + accessedAt = (if (map.containsKey("accessedAt")) map["accessedAt"] else "") as String, impersonator = map["impersonator"] as? Boolean, impersonatorUserId = map["impersonatorUserId"] as? String, ) diff --git a/library/src/main/java/io/appwrite/services/Account.kt b/library/src/main/java/io/appwrite/services/Account.kt index 8df38221..e6201c46 100644 --- a/library/src/main/java/io/appwrite/services/Account.kt +++ b/library/src/main/java/io/appwrite/services/Account.kt @@ -1200,6 +1200,219 @@ class Account(client: Client) : Service(client) { nestedType = classOf(), ) + /** + * Get the list of verified passkeys registered by the currently logged in user. + * + * @param queries Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter and order on the following attributes: $id, $createdAt, $updatedAt, name, accessedAt + * @param total When set to false, the total count returned will be 0 and will not be calculated. + * @return [io.appwrite.models.PasskeyList] + */ + @JvmOverloads + suspend fun listPasskeys( + queries: List? = null, + total: Boolean? = null, + ): io.appwrite.models.PasskeyList { + val apiPath = "/account/passkeys" + val apiParams = mutableMapOf( + "queries" to queries, + "total" to total, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.PasskeyList = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.PasskeyList.from(map = it as Map) + } + return client.call( + "GET", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.PasskeyList::class.java, + converter, + ) + } + + /** + * Start registering a passkey for the currently logged in user. The session must have signed in or completed an MFA challenge within the last 10 minutes. Pass the returned `publicKey` options to `navigator.credentials.create()`, then complete the registration with [Update passkey verification](/docs/references/cloud/client-web/account#updatePasskeyVerification). The passkey stays pending until verified, and the challenge expires after 5 minutes. + * + * @param passkeyId Passkey ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars. + * @param name Passkey name, shown when listing passkeys. Max length: 128 chars. + * @return [io.appwrite.models.PasskeyChallenge] + */ + @JvmOverloads + suspend fun createPasskey( + passkeyId: String? = null, + name: String? = null, + ): io.appwrite.models.PasskeyChallenge { + val apiPath = "/account/passkeys" + val apiParams = mutableMapOf( + "passkeyId" to passkeyId, + "name" to name, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.PasskeyChallenge = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.PasskeyChallenge.from(map = it as Map) + } + return client.call( + "POST", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.PasskeyChallenge::class.java, + converter, + ) + } + + /** + * Get a verified passkey of the currently logged in user by its unique ID. + * + * @param passkeyId Passkey ID. + * @return [io.appwrite.models.Passkey] + */ + suspend fun getPasskey( + passkeyId: String, + ): io.appwrite.models.Passkey { + if (passkeyId.isEmpty()) { + throw AppwriteException("Missing required parameter: \"passkeyId\"") + } + val apiPath = ("/account/passkeys/{passkeyId}" + .replace("{passkeyId}", passkeyId) + ) + val apiParams = mutableMapOf() + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.Passkey = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.Passkey.from(map = it as Map) + } + return client.call( + "GET", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.Passkey::class.java, + converter, + ) + } + + /** + * Rename a passkey of the currently logged in user. + * + * @param passkeyId Passkey ID. + * @param name Passkey name. Max length: 128 chars. + * @return [io.appwrite.models.Passkey] + */ + suspend fun updatePasskey( + passkeyId: String, + name: String, + ): io.appwrite.models.Passkey { + if (passkeyId.isEmpty()) { + throw AppwriteException("Missing required parameter: \"passkeyId\"") + } + val apiPath = ("/account/passkeys/{passkeyId}" + .replace("{passkeyId}", passkeyId) + ) + val apiParams = mutableMapOf( + "name" to name, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.Passkey = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.Passkey.from(map = it as Map) + } + return client.call( + "PATCH", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.Passkey::class.java, + converter, + ) + } + + /** + * Delete a passkey from the currently logged in user. The passkey can no longer be used to sign in, although it may remain stored on the user's device. The session must have signed in or completed an MFA challenge within the last 10 minutes. + * + * @param passkeyId Passkey ID. + * @return [Any] + */ + suspend fun deletePasskey( + passkeyId: String, + ): Any { + if (passkeyId.isEmpty()) { + throw AppwriteException("Missing required parameter: \"passkeyId\"") + } + val apiPath = ("/account/passkeys/{passkeyId}" + .replace("{passkeyId}", passkeyId) + ) + val apiParams = mutableMapOf() + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + return client.call( + "DELETE", + apiPath, + apiHeaders, + apiParams, + responseType = Any::class.java, + ) + } + + /** + * Complete a passkey registration started with [Create passkey](/docs/references/cloud/client-web/account#createPasskey). Pass the JSON form of the credential returned by `navigator.credentials.create()`, for example `credential.toJSON()`. Each registration can be verified once; a failed attempt requires starting it again. + * + * @param passkeyId Passkey ID. + * @param credential Registration credential returned by the authenticator, in the JSON form produced by `PublicKeyCredential.toJSON()`. + * @return [io.appwrite.models.Passkey] + */ + suspend fun updatePasskeyVerification( + passkeyId: String, + credential: Map, + ): io.appwrite.models.Passkey { + if (passkeyId.isEmpty()) { + throw AppwriteException("Missing required parameter: \"passkeyId\"") + } + val apiPath = ("/account/passkeys/{passkeyId}/verification" + .replace("{passkeyId}", passkeyId) + ) + val apiParams = mutableMapOf( + "credential" to credential, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.Passkey = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.Passkey.from(map = it as Map) + } + return client.call( + "PUT", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.Passkey::class.java, + converter, + ) + } + /** * Update currently logged in user password. For validation, user is required to pass in the new password, and the old password. For users created with OAuth, Team Invites and Magic URL, oldPassword is optional. * @@ -1784,6 +1997,7 @@ class Account(client: Client) : Service(client) { * @param success URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param failure URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param scopes A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of 100 scopes are allowed, each 4096 characters long. + * @param state An opaque value your app generates and keeps, for example in a cookie. It is returned unchanged as the `state` query parameter on the success and failure URLs, so your app can check that the sign-in it receives is one it started. Printable ASCII only (RFC 6749 Appendix A.5). Max length: 256 chars. */ @JvmOverloads suspend fun createOAuth2Session( @@ -1792,6 +2006,7 @@ class Account(client: Client) : Service(client) { success: String? = null, failure: String? = null, scopes: List? = null, + state: String? = null, ) { val apiPath = ("/account/sessions/oauth2/{provider}" .replace("{provider}", provider.value) @@ -1800,6 +2015,7 @@ class Account(client: Client) : Service(client) { "success" to success, "failure" to failure, "scopes" to scopes, + "state" to state, "project" to client.config["project"], ) val apiQuery = mutableListOf() @@ -1810,11 +2026,11 @@ class Account(client: Client) : Service(client) { } is List<*> -> { (it.value as List<*>).forEach { v -> - apiQuery.add("${it.key}[]=${v.toString()}") + apiQuery.add("${Uri.encode(it.key)}[]=${Uri.encode(v.toString())}") } } else -> { - apiQuery.add("${it.key}=${it.value.toString()}") + apiQuery.add("${Uri.encode(it.key)}=${Uri.encode(it.value.toString())}") } } } @@ -2268,6 +2484,7 @@ class Account(client: Client) : Service(client) { * @param success URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param failure URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param scopes A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of 100 scopes are allowed, each 4096 characters long. + * @param state An opaque value your app generates and keeps, for example in a cookie. It is returned unchanged as the `state` query parameter on the success and failure URLs, so your app can check that the sign-in it receives is one it started. Printable ASCII only (RFC 6749 Appendix A.5). Max length: 256 chars. */ @JvmOverloads suspend fun createOAuth2Token( @@ -2276,6 +2493,7 @@ class Account(client: Client) : Service(client) { success: String? = null, failure: String? = null, scopes: List? = null, + state: String? = null, ) { val apiPath = ("/account/tokens/oauth2/{provider}" .replace("{provider}", provider.value) @@ -2284,6 +2502,7 @@ class Account(client: Client) : Service(client) { "success" to success, "failure" to failure, "scopes" to scopes, + "state" to state, "project" to client.config["project"], ) val apiQuery = mutableListOf() @@ -2294,11 +2513,11 @@ class Account(client: Client) : Service(client) { } is List<*> -> { (it.value as List<*>).forEach { v -> - apiQuery.add("${it.key}[]=${v.toString()}") + apiQuery.add("${Uri.encode(it.key)}[]=${Uri.encode(v.toString())}") } } else -> { - apiQuery.add("${it.key}=${it.value.toString()}") + apiQuery.add("${Uri.encode(it.key)}=${Uri.encode(it.value.toString())}") } } } @@ -2332,6 +2551,68 @@ class Account(client: Client) : Service(client) { } } + /** + * Start signing in with a passkey. No email or user ID is needed: pass the returned `publicKey` options to `navigator.credentials.get()` and the user picks one of their passkeys. Then call [Update passkey token](/docs/references/cloud/client-web/account#updatePasskeyToken) with the credential to receive a token, and exchange it with [Create session](/docs/references/cloud/client-web/account#createSession). The challenge expires after 5 minutes. + * + * @return [io.appwrite.models.PasskeyChallenge] + */ + suspend fun createPasskeyToken(): io.appwrite.models.PasskeyChallenge { + val apiPath = "/account/tokens/passkey" + val apiParams = mutableMapOf() + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.PasskeyChallenge = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.PasskeyChallenge.from(map = it as Map) + } + return client.call( + "POST", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.PasskeyChallenge::class.java, + converter, + ) + } + + /** + * Complete a passkey sign-in started with [Create passkey token](/docs/references/cloud/client-web/account#createPasskeyToken). Pass the challenge ID and the JSON form of the credential returned by `navigator.credentials.get()`, for example `credential.toJSON()`. The returned token includes its secret and expires after 1 minute: exchange it for a session with [Create session](/docs/references/cloud/client-web/account#createSession). Passkeys require user verification, so the session also satisfies MFA. Each challenge can only be used once. + * + * @param challengeId Challenge ID returned by createPasskeyToken. + * @param credential Authentication credential returned by the authenticator, in the JSON form produced by `PublicKeyCredential.toJSON()`. + * @return [io.appwrite.models.TokenSecret] + */ + suspend fun updatePasskeyToken( + challengeId: String, + credential: Map, + ): io.appwrite.models.TokenSecret { + val apiPath = "/account/tokens/passkey" + val apiParams = mutableMapOf( + "challengeId" to challengeId, + "credential" to credential, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.TokenSecret = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.TokenSecret.from(map = it as Map) + } + return client.call( + "PUT", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.TokenSecret::class.java, + converter, + ) + } + /** * Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST /v1/account/sessions/token](https://appwrite.io/docs/references/cloud/client-web/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes. * diff --git a/library/src/main/java/io/appwrite/services/Analytics.kt b/library/src/main/java/io/appwrite/services/Analytics.kt new file mode 100644 index 00000000..1949f76b --- /dev/null +++ b/library/src/main/java/io/appwrite/services/Analytics.kt @@ -0,0 +1,83 @@ +package io.appwrite.services + +import android.net.Uri +import io.appwrite.Client +import io.appwrite.Service +import io.appwrite.exceptions.AppwriteException +import io.appwrite.extensions.classOf +import io.appwrite.models.* +import okhttp3.Cookie +import java.io.File + +/** + * + */ +class Analytics(client: Client) : Service(client) { + /** + * Send a tracking event from a browser, native app, or server-side SDK. + * + * @param propertyId Analytics property ID. + * @param name Event name. "pageview" is just a conventional event name; events are not modeled specially. + * @param url Full page URL or screen identifier. + * @param domain Hostname (e.g. example.com). + * @param referrer Referrer URL. + * @param screenWidth Viewport width in CSS pixels. + * @param sessionHash Optional session hash (provided by SDK). + * @param scrollDepth Scroll depth percentage 0-100. + * @param engagementTime Engagement time in seconds, 0-4294967295. + * @param props Custom string properties as a flat key=value list (max 32 entries, alternating key,value). + * @param userId Override user ID. Requires API-key auth with analytics.write scope. + * @param ip Override IP address. Requires API-key auth with analytics.write scope. + * @param userAgent Override user agent. Requires API-key auth with analytics.write scope. + * @return [Any] + */ + @JvmOverloads + suspend fun createEvent( + propertyId: String, + name: String, + url: String, + domain: String? = null, + referrer: String? = null, + screenWidth: Long? = null, + sessionHash: String? = null, + scrollDepth: Long? = null, + engagementTime: Long? = null, + props: List? = null, + userId: String? = null, + ip: String? = null, + userAgent: String? = null, + ): Any { + if (propertyId.isEmpty()) { + throw AppwriteException("Missing required parameter: \"propertyId\"") + } + val apiPath = ("/analytics/properties/{propertyId}/events" + .replace("{propertyId}", propertyId) + ) + val apiParams = mutableMapOf( + "name" to name, + "url" to url, + "domain" to domain, + "referrer" to referrer, + "screenWidth" to screenWidth, + "sessionHash" to sessionHash, + "scrollDepth" to scrollDepth, + "engagementTime" to engagementTime, + "props" to props, + "userId" to userId, + "ip" to ip, + "userAgent" to userAgent, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + return client.call( + "POST", + apiPath, + apiHeaders, + apiParams, + responseType = Any::class.java, + ) + } +} diff --git a/library/src/main/java/io/appwrite/services/Oauth2.kt b/library/src/main/java/io/appwrite/services/Oauth2.kt index 030f4cef..f5cbc3cb 100644 --- a/library/src/main/java/io/appwrite/services/Oauth2.kt +++ b/library/src/main/java/io/appwrite/services/Oauth2.kt @@ -85,7 +85,7 @@ class Oauth2(client: Client) : Service(client) { prompt: String? = null, maxAge: Long? = null, authorizationDetails: String? = null, - resource: String? = null, + resource: List? = null, audience: String? = null, requestUri: String? = null, ): io.appwrite.models.Oauth2Authorize { @@ -157,7 +157,7 @@ class Oauth2(client: Client) : Service(client) { prompt: String? = null, maxAge: Long? = null, authorizationDetails: String? = null, - resource: String? = null, + resource: List? = null, audience: String? = null, requestUri: String? = null, ): io.appwrite.models.Oauth2Authorize { @@ -213,7 +213,7 @@ class Oauth2(client: Client) : Service(client) { clientId: String? = null, scope: String? = null, authorizationDetails: String? = null, - resource: String? = null, + resource: List? = null, audience: String? = null, ): io.appwrite.models.Oauth2DeviceAuthorization { val apiPath = ("/oauth2/{project_id}/device_authorization" @@ -468,7 +468,7 @@ class Oauth2(client: Client) : Service(client) { prompt: String? = null, maxAge: Long? = null, authorizationDetails: String? = null, - resource: String? = null, + resource: List? = null, audience: String? = null, ): io.appwrite.models.Oauth2PAR { val apiPath = ("/oauth2/{project_id}/par" @@ -642,7 +642,7 @@ class Oauth2(client: Client) : Service(client) { clientSecret: String? = null, codeVerifier: String? = null, redirectUri: String? = null, - resource: String? = null, + resource: List? = null, audience: String? = null, ): io.appwrite.models.Oauth2Token { val apiPath = ("/oauth2/{project_id}/token" diff --git a/library/src/main/java/io/appwrite/services/Push.kt b/library/src/main/java/io/appwrite/services/Push.kt index 48488c06..fc913847 100644 --- a/library/src/main/java/io/appwrite/services/Push.kt +++ b/library/src/main/java/io/appwrite/services/Push.kt @@ -828,10 +828,14 @@ class Push( const val EXTRA_PAYLOAD = PushBackground.EXTRA_PAYLOAD /** - * What background delivery can rely on: exact alarms, battery-optimisation exemption and - * foreground mode. When [PushBackgroundStatus.bestEffort] is true, wake-ups are inexact and - * messages can arrive late while the app is closed; explain why before asking with - * [requestExactAlarms] or [requestIgnoreBatteryOptimizations]. + * What background delivery can rely on: exact alarms, battery-optimisation exemption, + * foreground mode and restricted background usage. When [PushBackgroundStatus.bestEffort] + * is true, messages can arrive late while the app is closed; explain why before asking with + * [requestExactAlarms] or [requestIgnoreBatteryOptimizations]. When + * [PushBackgroundStatus.backgroundRestricted] is true, neither exact alarms nor foreground + * mode lift it: ask the user to choose Unrestricted in the app's battery settings + * (Settings > Apps > your app > Battery), or for the battery-optimisation exemption, which + * also lets the SDK's alarms and jobs run. */ @JvmStatic fun backgroundStatus(context: Context): PushBackgroundStatus = PushBackground.backgroundStatus(context) diff --git a/library/src/main/java/io/appwrite/services/PushBackground.kt b/library/src/main/java/io/appwrite/services/PushBackground.kt index e06dfabe..73103d5d 100644 --- a/library/src/main/java/io/appwrite/services/PushBackground.kt +++ b/library/src/main/java/io/appwrite/services/PushBackground.kt @@ -52,8 +52,15 @@ import java.util.concurrent.atomic.AtomicInteger private const val LOG_TAG = "AppwritePush" -/** What a message's `notification` block asks a background notification to show; [present] is false without one. */ -internal data class PushNotificationContent(val present: Boolean, val title: String?, val body: String?, val image: String?) +/** + * What a message asks a background notification to show. A [raw] payload, one that is not a JSON + * object, is shown as the body under the subscription's title; an envelope is [visible] only with a + * title or body in its `notification`. + */ +internal data class PushNotificationContent(val raw: Boolean, val title: String?, val body: String?, val image: String?) { + val visible: Boolean + get() = raw || title != null || body != null +} /** A background subscription saved across restarts: its filter, QoS choice and notification title. */ internal data class PushEntry( @@ -887,7 +894,7 @@ internal object PushBackground { // Deliver a message to the matching live callbacks; with none, to the declared PushReceiver // and/or a notification per saved subscription that matches it. [deferAck] hands a listener // that acknowledges messages itself its acknowledgement. - private fun deliverMessage(context: Context, message: PushMessage, deferAck: () -> () -> Unit) { + internal fun deliverMessage(context: Context, message: PushMessage, deferAck: () -> () -> Unit) { val matching = listeners.filter { listener -> listener.topics.any { matchesTopic(it, message.topic) } } // Saved background subscriptions no live one has taken over; they deliver even when a live // foreground-only subscription on an overlapping filter received the message too. @@ -952,16 +959,21 @@ internal object PushBackground { /** * Post a notification for [message] that opens the app, with the message in its extras. It shows - * the server's title, body and image from [content], falling back to [title] and the raw payload. + * the server's title, body and image from [content], falling back to [title], and to the payload + * as the body when it is not a JSON object. An envelope with neither a title nor a body, such as + * a data-only push, posts nothing. */ fun notify(context: Context, message: PushMessage, title: String, content: PushNotificationContent = notificationContent(message)) { + if (!content.visible) { + return + } val manager = NotificationManagerCompat.from(context) if (!manager.areNotificationsEnabled()) { return } createChannels(context) val id = 31 * message.hashCode() + title.hashCode() - val body = content.body ?: message.data.takeIf { !content.present } + val body = content.body ?: message.data.takeIf { content.raw } val builder = NotificationCompat.Builder(context, CHANNEL_ID) .setContentTitle(content.title ?: title) .setSmallIcon(notificationIcon(context)) @@ -992,12 +1004,13 @@ internal object PushBackground { .also { ActivityManager.getMyMemoryState(it) } .importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND - /** The server's `notification` block in [message]: nulls when the payload has none or is not JSON. */ + /** The server's `notification` block in [message], [PushNotificationContent.raw] when the payload is not a JSON object. */ fun notificationContent(message: PushMessage): PushNotificationContent { - val notification = runCatching { JSONObject(message.data) }.getOrNull()?.optJSONObject("notification") - ?: return PushNotificationContent(false, null, null, null) - fun field(name: String) = (notification.opt(name) as? String)?.takeIf { it.isNotEmpty() } - return PushNotificationContent(true, field("title"), field("body"), field("image")) + val envelope = runCatching { JSONObject(message.data) }.getOrNull() + ?: return PushNotificationContent(true, null, null, null) + val notification = envelope.optJSONObject("notification") + fun field(name: String) = (notification?.opt(name) as? String)?.takeIf { it.isNotEmpty() } + return PushNotificationContent(false, field("title"), field("body"), field("image")) } // Downloads notification images, so a slow one is abandoned without holding up delivery. @@ -1201,6 +1214,8 @@ internal object PushBackground { app.getSystemService(AlarmManager::class.java)?.canScheduleExactAlarms() == true, ignoringBatteryOptimizations = app.getSystemService(PowerManager::class.java)?.isIgnoringBatteryOptimizations(app.packageName) == true, foregroundService = PushStore.foreground(app), + backgroundRestricted = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && + app.getSystemService(ActivityManager::class.java)?.isBackgroundRestricted == true, ) } diff --git a/library/src/main/java/io/appwrite/services/PushCore.kt b/library/src/main/java/io/appwrite/services/PushCore.kt index 15f86b6a..bc151295 100644 --- a/library/src/main/java/io/appwrite/services/PushCore.kt +++ b/library/src/main/java/io/appwrite/services/PushCore.kt @@ -54,19 +54,36 @@ data class PushMessage( /** * What background delivery can rely on. Without exact alarms, the scheduled wake-ups are inexact * and Doze can defer them, so delivery is [bestEffort] unless foreground mode keeps the - * connection open. + * connection open. When the user restricted background usage, delivery is [bestEffort] unless the + * app is also exempt from battery optimisation, and foreground mode no longer counts: Android stops + * a restricted app's foreground service once it leaves the screen, exemption or not. */ -data class PushBackgroundStatus( +data class PushBackgroundStatus @JvmOverloads constructor( /** The app may schedule exact alarms (`SCHEDULE_EXACT_ALARM`, granted). */ val exactAlarms: Boolean, /** The app is exempt from battery optimisation. */ val ignoringBatteryOptimizations: Boolean, /** Foreground mode (`setForeground(true)`) keeps the connection open in a service. */ val foregroundService: Boolean, + /** + * The user restricted the app's background usage in its battery settings (Android 9+). Android + * may then hold its alarms, jobs and foreground service while it is closed, so messages can + * wait until the app is opened. The battery-optimisation exemption lets the alarms and jobs + * run again, but foreground mode still stops once the app leaves the screen. + */ + val backgroundRestricted: Boolean = false, ) { - /** Wake-ups may be deferred by Doze, so messages can arrive late while the app is closed. */ + /** + * Messages can arrive late, or not until the app is opened, while the app is closed: background + * usage is restricted without the battery-optimisation exemption, or wake-ups may be deferred + * by Doze. + */ val bestEffort: Boolean - get() = !exactAlarms && !foregroundService + get() { + val held = backgroundRestricted && !ignoringBatteryOptimizations + val foreground = foregroundService && !backgroundRestricted + return held || (!exactAlarms && !foreground) + } } /** Connection parameters shared between the in-process client and the foreground service. */ diff --git a/library/src/main/java/io/appwrite/services/Teams.kt b/library/src/main/java/io/appwrite/services/Teams.kt index 7227eba6..8a4bb87d 100644 --- a/library/src/main/java/io/appwrite/services/Teams.kt +++ b/library/src/main/java/io/appwrite/services/Teams.kt @@ -539,6 +539,7 @@ class Teams(client: Client) : Service(client) { * @param phone Phone number. Format this number with a leading '+' and a country code, e.g., +16175551212. * @param url URL to redirect the user back to your app from the invitation email. This parameter is not required when an API key is supplied. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param name Name of the new team member. Max length: 128 chars. + * @param membershipId Membership ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars. * @return [io.appwrite.models.Membership] */ @JvmOverloads @@ -550,6 +551,7 @@ class Teams(client: Client) : Service(client) { phone: String? = null, url: String? = null, name: String? = null, + membershipId: String? = null, ): io.appwrite.models.Membership { if (teamId.isEmpty()) { throw AppwriteException("Missing required parameter: \"teamId\"") @@ -564,6 +566,7 @@ class Teams(client: Client) : Service(client) { "roles" to roles, "url" to url, "name" to name, + "membershipId" to membershipId, ) val apiHeaders = mutableMapOf( "X-Appwrite-Project" to client.config["project"].orEmpty(), From 967730dfc5d67a66e058cf906b26112fbe6d60e0 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 15 Oct 2026 11:12:19 +0100 Subject: [PATCH 3/4] feat: SDK update for the 2.5.x majors --- CHANGELOG.md | 52 +- README.md | 67 +- .../java/account/create-o-auth-2-session.md | 1 + .../java/account/create-o-auth-2-token.md | 1 + .../java/account/create-passkey-token.md | 22 + docs/examples/java/account/create-passkey.md | 27 + docs/examples/java/account/delete-passkey.md | 26 + docs/examples/java/account/get-passkey.md | 26 + docs/examples/java/account/list-passkeys.md | 27 + .../java/account/update-passkey-token.md | 27 + .../account/update-passkey-verification.md | 27 + docs/examples/java/account/update-passkey.md | 27 + docs/examples/java/analytics/create-event.md | 38 ++ docs/examples/java/oauth2/authorize-post.md | 2 +- docs/examples/java/oauth2/authorize.md | 2 +- .../oauth2/create-device-authorization.md | 2 +- docs/examples/java/oauth2/create-par.md | 2 +- docs/examples/java/oauth2/create-token.md | 2 +- docs/examples/java/teams/create-membership.md | 1 + .../kotlin/account/create-o-auth-2-session.md | 1 + .../kotlin/account/create-o-auth-2-token.md | 1 + .../kotlin/account/create-passkey-token.md | 13 + .../examples/kotlin/account/create-passkey.md | 16 + .../examples/kotlin/account/delete-passkey.md | 15 + docs/examples/kotlin/account/get-passkey.md | 15 + docs/examples/kotlin/account/list-passkeys.md | 16 + .../kotlin/account/update-passkey-token.md | 16 + .../account/update-passkey-verification.md | 16 + .../examples/kotlin/account/update-passkey.md | 16 + .../examples/kotlin/analytics/create-event.md | 27 + docs/examples/kotlin/oauth2/authorize-post.md | 2 +- docs/examples/kotlin/oauth2/authorize.md | 2 +- .../oauth2/create-device-authorization.md | 2 +- docs/examples/kotlin/oauth2/create-par.md | 2 +- docs/examples/kotlin/oauth2/create-token.md | 2 +- .../kotlin/teams/create-membership.md | 1 + library/src/main/java/io/appwrite/Client.kt | 51 +- library/src/main/java/io/appwrite/Query.kt | 574 ++++++++++++++++++ library/src/main/java/io/appwrite/Topic.kt | 34 +- .../io/appwrite/enums/ExecutionTrigger.kt | 4 +- .../main/java/io/appwrite/models/Account.kt | 30 +- .../java/io/appwrite/models/AlgoArgon2.kt | 8 +- .../java/io/appwrite/models/AlgoBcrypt.kt | 2 +- .../main/java/io/appwrite/models/AlgoMd5.kt | 2 +- .../java/io/appwrite/models/AlgoPhpass.kt | 2 +- .../java/io/appwrite/models/AlgoScrypt.kt | 10 +- .../io/appwrite/models/AlgoScryptModified.kt | 8 +- .../main/java/io/appwrite/models/AlgoSha.kt | 2 +- .../src/main/java/io/appwrite/models/App.kt | 52 +- .../io/appwrite/models/AppInstallation.kt | 18 +- .../io/appwrite/models/AppInstallationList.kt | 4 +- .../main/java/io/appwrite/models/AppKey.kt | 16 +- .../java/io/appwrite/models/AppKeyList.kt | 4 +- .../main/java/io/appwrite/models/AppScope.kt | 10 +- .../java/io/appwrite/models/AppScopeList.kt | 4 +- .../main/java/io/appwrite/models/AppSecret.kt | 16 +- .../java/io/appwrite/models/AppSecretList.kt | 4 +- .../io/appwrite/models/AppSecretPlaintext.kt | 16 +- .../main/java/io/appwrite/models/AppsList.kt | 4 +- .../main/java/io/appwrite/models/Continent.kt | 4 +- .../java/io/appwrite/models/ContinentList.kt | 4 +- .../main/java/io/appwrite/models/Country.kt | 4 +- .../java/io/appwrite/models/CountryList.kt | 4 +- .../main/java/io/appwrite/models/Currency.kt | 14 +- .../java/io/appwrite/models/CurrencyList.kt | 4 +- .../main/java/io/appwrite/models/Document.kt | 14 +- .../java/io/appwrite/models/DocumentList.kt | 4 +- .../main/java/io/appwrite/models/Execution.kt | 34 +- .../java/io/appwrite/models/ExecutionList.kt | 4 +- .../src/main/java/io/appwrite/models/File.kt | 32 +- .../main/java/io/appwrite/models/FileList.kt | 4 +- .../main/java/io/appwrite/models/Headers.kt | 4 +- .../main/java/io/appwrite/models/Identity.kt | 22 +- .../java/io/appwrite/models/IdentityList.kt | 4 +- .../src/main/java/io/appwrite/models/Jwt.kt | 2 +- .../main/java/io/appwrite/models/Language.kt | 6 +- .../java/io/appwrite/models/LanguageList.kt | 4 +- .../main/java/io/appwrite/models/Locale.kt | 14 +- .../java/io/appwrite/models/LocaleCode.kt | 4 +- .../java/io/appwrite/models/LocaleCodeList.kt | 4 +- .../java/io/appwrite/models/Membership.kt | 30 +- .../java/io/appwrite/models/MembershipList.kt | 4 +- .../java/io/appwrite/models/MfaChallenge.kt | 8 +- .../java/io/appwrite/models/MfaFactors.kt | 10 +- .../io/appwrite/models/MfaRecoveryCodes.kt | 2 +- .../main/java/io/appwrite/models/MfaType.kt | 4 +- .../java/io/appwrite/models/Oauth2Approve.kt | 2 +- .../io/appwrite/models/Oauth2Authorize.kt | 4 +- .../java/io/appwrite/models/Oauth2Consent.kt | 20 +- .../io/appwrite/models/Oauth2ConsentList.kt | 4 +- .../io/appwrite/models/Oauth2ConsentToken.kt | 22 +- .../appwrite/models/Oauth2ConsentTokenList.kt | 4 +- .../models/Oauth2DeviceAuthorization.kt | 12 +- .../java/io/appwrite/models/Oauth2Grant.kt | 24 +- .../io/appwrite/models/Oauth2Organization.kt | 2 +- .../appwrite/models/Oauth2OrganizationList.kt | 4 +- .../main/java/io/appwrite/models/Oauth2PAR.kt | 4 +- .../java/io/appwrite/models/Oauth2Project.kt | 6 +- .../io/appwrite/models/Oauth2ProjectList.kt | 4 +- .../java/io/appwrite/models/Oauth2Reject.kt | 2 +- .../java/io/appwrite/models/Oauth2Token.kt | 10 +- .../main/java/io/appwrite/models/Passkey.kt | 61 ++ .../io/appwrite/models/PasskeyChallenge.kt | 61 ++ .../java/io/appwrite/models/PasskeyList.kt | 37 ++ .../src/main/java/io/appwrite/models/Phone.kt | 6 +- .../main/java/io/appwrite/models/PhoneList.kt | 4 +- .../main/java/io/appwrite/models/Presence.kt | 12 +- .../java/io/appwrite/models/PresenceList.kt | 4 +- .../src/main/java/io/appwrite/models/Row.kt | 14 +- .../main/java/io/appwrite/models/RowList.kt | 4 +- .../main/java/io/appwrite/models/Session.kt | 58 +- .../java/io/appwrite/models/SessionList.kt | 4 +- .../java/io/appwrite/models/Subscriber.kt | 16 +- .../main/java/io/appwrite/models/Target.kt | 16 +- .../src/main/java/io/appwrite/models/Team.kt | 10 +- .../main/java/io/appwrite/models/TeamList.kt | 4 +- .../src/main/java/io/appwrite/models/Token.kt | 12 +- .../java/io/appwrite/models/TokenSecret.kt | 69 +++ .../java/io/appwrite/models/Transaction.kt | 12 +- .../io/appwrite/models/TransactionList.kt | 4 +- .../src/main/java/io/appwrite/models/User.kt | 30 +- .../main/java/io/appwrite/services/Account.kt | 289 ++++++++- .../java/io/appwrite/services/Analytics.kt | 83 +++ .../main/java/io/appwrite/services/Oauth2.kt | 10 +- .../main/java/io/appwrite/services/Push.kt | 47 +- .../io/appwrite/services/PushBackground.kt | 35 +- .../java/io/appwrite/services/PushCore.kt | 25 +- .../main/java/io/appwrite/services/Teams.kt | 3 + .../src/test/java/io/appwrite/QueryTest.kt | 166 +++++ .../src/test/java/io/appwrite/TopicTest.kt | 19 +- 130 files changed, 2341 insertions(+), 563 deletions(-) create mode 100644 docs/examples/java/account/create-passkey-token.md create mode 100644 docs/examples/java/account/create-passkey.md create mode 100644 docs/examples/java/account/delete-passkey.md create mode 100644 docs/examples/java/account/get-passkey.md create mode 100644 docs/examples/java/account/list-passkeys.md create mode 100644 docs/examples/java/account/update-passkey-token.md create mode 100644 docs/examples/java/account/update-passkey-verification.md create mode 100644 docs/examples/java/account/update-passkey.md create mode 100644 docs/examples/java/analytics/create-event.md create mode 100644 docs/examples/kotlin/account/create-passkey-token.md create mode 100644 docs/examples/kotlin/account/create-passkey.md create mode 100644 docs/examples/kotlin/account/delete-passkey.md create mode 100644 docs/examples/kotlin/account/get-passkey.md create mode 100644 docs/examples/kotlin/account/list-passkeys.md create mode 100644 docs/examples/kotlin/account/update-passkey-token.md create mode 100644 docs/examples/kotlin/account/update-passkey-verification.md create mode 100644 docs/examples/kotlin/account/update-passkey.md create mode 100644 docs/examples/kotlin/analytics/create-event.md create mode 100644 library/src/main/java/io/appwrite/models/Passkey.kt create mode 100644 library/src/main/java/io/appwrite/models/PasskeyChallenge.kt create mode 100644 library/src/main/java/io/appwrite/models/PasskeyList.kt create mode 100644 library/src/main/java/io/appwrite/models/TokenSecret.kt create mode 100644 library/src/main/java/io/appwrite/services/Analytics.kt create mode 100644 library/src/test/java/io/appwrite/QueryTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 21f1b0c0..3197c210 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,29 +1,33 @@ # Change Log -## 29.1.0-rc.3 - -* Added: first background subscription auto-requests `POST_NOTIFICATIONS` (Android 13+) from the visible activity -* Fixed: `onOpen`/`onClose` callbacks fire correctly when joining or leaving the background host -* Fixed: notification taps are recorded after launching the app, so tap-opened screens land on top - -## 29.1.0-rc.2 - -* Added: `getInitialNotification()`, `onNotificationOpened()`, and the `PushNotificationOpened` data class (topic + data) -* Added: `onOpen`/`onClose` callbacks now fire for background push connections -* Added: push auth falls back to the client's cookie-store session -* Fixed: a foreground message with no live callback now still posts a notification -* Updated: notification taps route through an internal `PushOpenActivity` - -## 29.1.0-rc.1 - -* Updated: regenerated with sdk-generator 5.5.0; no SDK code changes - -## 29.1.0-rc.0 - -* Added: optional `method` param on `Client.chunkedUpload()` to set the upload HTTP method -* Added: background push notifications render the server `notification` title, body, and image -* Updated: `Avatars.updatePhoto()` uploads via `PUT` -* Fixed: an exactly-chunk-sized file uploads in a single request +## 30.0.0 + +* Breaking: requests send `x-appwrite-response-format: 2.5.0`; responses follow the 2.5 shapes +* Breaking: missing response fields map to their documented defaults +* Breaking: `oauth2` `resource` params now take `List` on the authorize, PAR, device and token methods +* Breaking: `Push.subscribe` takes `notifyInForeground` before the callback; Java callers must pass it +* Breaking: removed the `Topic.any()` and `Topic.all()` wildcard builders and `ResolvedTopic`; `Push.subscribe` takes strings or `Topic` +* Added: `notifyInForeground` option on `Push.subscribe` +* Added: `PushOpenActivity` for opening notifications +* Added: `Query` aggregates (`count`, `countDistinct`, `sum`, `avg`, `min`, `max` and more), `groupBy`, `having`, `distinct`, joins, `relationship`, `covers`, `spatialEquals` and `page` +* Added: `Query.builder()` to chain queries; `build()` returns the query list +* Added: `ExecutionTrigger.EMAIL` +* Updated: `Topic.path` rejects levels containing a NUL character +* Fixed: a file exactly `CHUNK_SIZE` bytes long uploads in a single request + +## 29.2.0 + +* Added: `analytics.createEvent` in a new `Analytics` service to send tracking events +* Added: optional `state` on `account.createOAuth2Session` and `account.createOAuth2Token`, returned unchanged on the success and failure URLs +* Added: optional `membershipId` on `teams.createMembership` +* Fixed: `avatars.updatePhoto` sends `PUT`, so uploading a profile photo no longer returns 404 +* Fixed: OAuth2 query parameters (`success`, `failure`, `scopes`, `state`) are URL-encoded, so values containing `&`, `+` or `#` survive the redirect + +## 29.1.0 + +* Added: `account` passkey methods `listPasskeys`, `createPasskey`, `getPasskey`, `updatePasskey`, `deletePasskey` and `updatePasskeyVerification` +* Added: `account.createPasskeyToken` and `account.updatePasskeyToken` for passkey sign-in +* Added: `Passkey`, `PasskeyList`, `PasskeyChallenge` and `TokenSecret` models ## 29.0.0 diff --git a/README.md b/README.md index afcc18e1..d045d7a4 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ ![Maven Central](https://img.shields.io/maven-central/v/io.appwrite/sdk-for-android.svg?color=green&style=flat-square) ![License](https://img.shields.io/github/license/appwrite/sdk-for-android.svg?style=flat-square) -![Version](https://img.shields.io/badge/api%20version-2.3.0-blue.svg?style=flat-square) +![Version](https://img.shields.io/badge/api%20version-2.5.0-blue.svg?style=flat-square) [![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator) [![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite) [![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord) -**This SDK targets Appwrite server version 2.3.x as shipped on Appwrite Cloud.** Self-hosted releases can lag behind Cloud — if you run an older self-hosted build, use a matching older SDK from [previous releases](https://github.com/appwrite/sdk-for-android/releases) when APIs differ. +**This SDK targets Appwrite server version 2.5.x as shipped on Appwrite Cloud.** Self-hosted releases can lag behind Cloud — if you run an older self-hosted build, use a matching older SDK from [previous releases](https://github.com/appwrite/sdk-for-android/releases) when APIs differ. Appwrite is an open-source backend as a service server that abstracts and simplifies complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the Android SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs) @@ -38,7 +38,7 @@ repositories { Next, add the dependency to your project's `build.gradle(.kts)` file: ```groovy -implementation("io.appwrite:sdk-for-android:29.1.0-rc.3") +implementation("io.appwrite:sdk-for-android:30.0.0") ``` ### Maven @@ -49,7 +49,7 @@ Add this to your project's `pom.xml` file: io.appwrite sdk-for-android - 29.1.0-rc.3 + 30.0.0 ``` @@ -79,8 +79,10 @@ app every 15 to 60 seconds to reconnect, and the broker replays what was sent in While the app is not on screen, each message is also posted as a notification that opens your launch activity with `Push.EXTRA_TOPIC` and `Push.EXTRA_PAYLOAD` in its extras. It shows the -title, body and image sent with `createPush`, falling back to the subscription's `title` and the -raw payload for other messages. On Android 13 and later it needs the `POST_NOTIFICATIONS` +title, body and image sent with `createPush`, falling back to the subscription's `title`, and to +the raw payload as the body for messages that are not a JSON object. A push with neither a title +nor a body, such as a data-only push, posts no notification; it still reaches your callback or +`PushReceiver`. On Android 13 and later it needs the `POST_NOTIFICATIONS` permission: the first background subscription asks for it from the visible activity (the one `Push` is created with, else the next one to resume), or call `Push.requestNotificationPermission(activity)` to ask yourself. Set its @@ -119,6 +121,13 @@ The SDK uses exact alarms on its own whenever the app may schedule them. To allo nothing to ask, including when the permission is not declared. The SDK never opens these screens on its own. +If `backgroundRestricted` is true, the user restricted the app's background usage and messages can +wait until the app is opened. Exact alarms don't lift it, and neither does foreground mode: Android +stops a restricted app's foreground service once it leaves the screen. Ask the user to choose +Unrestricted in the app's battery settings (Settings > Apps > your app > Battery); granting the +battery optimisation exemption also lets the SDK's alarms and jobs run, and `bestEffort` then +reflects only the exact-alarm state. + If the user force-stops the app (Settings > Force stop, and on some devices swiping it away from recents), Android cancels its alarms and jobs: nothing is delivered until the app is opened again, and the broker then replays what was sent meanwhile. @@ -232,7 +241,7 @@ val user = account.create( ### Type Safety with Models -The Appwrite Android SDK provides type safety when working with database documents through generic methods. Methods like `listDocuments`, `getDocument`, and others accept a `nestedType` parameter that allows you to specify your custom model type for full type safety. +The Appwrite Android SDK provides type safety when working with table rows through generic methods. Methods like `listRows`, `getRow`, and others accept a `nestedType` parameter that allows you to specify your custom model type for full type safety. **Kotlin:** ```kotlin @@ -245,16 +254,17 @@ data class Book( val isCheckedOut: Boolean ) -val databases = Databases(client) +val tablesDB = TablesDB(client) try { - val documents = databases.listDocuments( + val result = tablesDB.listRows( databaseId = "your-database-id", - collectionId = "your-collection-id", + tableId = "your-table-id", nestedType = Book::class.java // Pass in your custom model type ) - for (book in documents.documents) { + for (row in result.rows) { + val book = row.data Log.d("Appwrite", "Book: ${book.name} by ${book.author}") // Now you have full type safety } } catch (e: AppwriteException) { @@ -299,21 +309,24 @@ public class Book { public void setCheckedOut(boolean checkedOut) { isCheckedOut = checkedOut; } } -Databases databases = new Databases(client); - -try { - DocumentList documents = databases.listDocuments( - "your-database-id", - "your-collection-id", - Book.class // Pass in your custom model type - ); - - for (Book book : documents.getDocuments()) { - Log.d("Appwrite", "Book: " + book.getName() + " by " + book.getAuthor()); // Now you have full type safety - } -} catch (AppwriteException e) { - Log.e("Appwrite", e.getMessage() != null ? e.getMessage() : "Unknown error"); -} +TablesDB tablesDB = new TablesDB(client); + +tablesDB.listRows( + "your-database-id", + "your-table-id", + Book.class, // Pass in your custom model type + new CoroutineCallback<>((result, error) -> { + if (error != null) { + Log.e("Appwrite", error.getMessage() != null ? error.getMessage() : "Unknown error"); + return; + } + + for (Row row : result.getRows()) { + Book book = row.getData(); + Log.d("Appwrite", "Book: " + book.getName() + " by " + book.getAuthor()); // Now you have full type safety + } + }) +); ``` **Tip**: You can use the `appwrite types` command to automatically generate model definitions based on your Appwrite database schema. Learn more about [type generation](https://appwrite.io/docs/products/databases/type-generation). @@ -363,7 +376,7 @@ The Appwrite Android SDK raises an `AppwriteException` object with `message`, `c ```kotlin try { var user = account.create(ID.unique(),"email@example.com","password","Walter O'Brien") - Log.d("Appwrite user", user.toMap()) + Log.d("Appwrite user", user.toMap().toString()) } catch(e : AppwriteException) { e.printStackTrace() } diff --git a/docs/examples/java/account/create-o-auth-2-session.md b/docs/examples/java/account/create-o-auth-2-session.md index da547421..f2a07eff 100644 --- a/docs/examples/java/account/create-o-auth-2-session.md +++ b/docs/examples/java/account/create-o-auth-2-session.md @@ -17,6 +17,7 @@ account.createOAuth2Session( "https://example.com", // success (optional) "https://example.com", // failure (optional) List.of(), // scopes (optional) + "", // state (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/java/account/create-o-auth-2-token.md b/docs/examples/java/account/create-o-auth-2-token.md index ae685850..885c066f 100644 --- a/docs/examples/java/account/create-o-auth-2-token.md +++ b/docs/examples/java/account/create-o-auth-2-token.md @@ -17,6 +17,7 @@ account.createOAuth2Token( "https://example.com", // success (optional) "https://example.com", // failure (optional) List.of(), // scopes (optional) + "", // state (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/java/account/create-passkey-token.md b/docs/examples/java/account/create-passkey-token.md new file mode 100644 index 00000000..12db5fe5 --- /dev/null +++ b/docs/examples/java/account/create-passkey-token.md @@ -0,0 +1,22 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.createPasskeyToken(new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); +})); +``` diff --git a/docs/examples/java/account/create-passkey.md b/docs/examples/java/account/create-passkey.md new file mode 100644 index 00000000..39a9a29d --- /dev/null +++ b/docs/examples/java/account/create-passkey.md @@ -0,0 +1,27 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.createPasskey( + "", // passkeyId (optional) + "", // name (optional) + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/delete-passkey.md b/docs/examples/java/account/delete-passkey.md new file mode 100644 index 00000000..a59d3848 --- /dev/null +++ b/docs/examples/java/account/delete-passkey.md @@ -0,0 +1,26 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.deletePasskey( + "", // passkeyId + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/get-passkey.md b/docs/examples/java/account/get-passkey.md new file mode 100644 index 00000000..5d988e86 --- /dev/null +++ b/docs/examples/java/account/get-passkey.md @@ -0,0 +1,26 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.getPasskey( + "", // passkeyId + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/list-passkeys.md b/docs/examples/java/account/list-passkeys.md new file mode 100644 index 00000000..6b04b1fb --- /dev/null +++ b/docs/examples/java/account/list-passkeys.md @@ -0,0 +1,27 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.listPasskeys( + List.of(), // queries (optional) + false, // total (optional) + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/update-passkey-token.md b/docs/examples/java/account/update-passkey-token.md new file mode 100644 index 00000000..6370aaaf --- /dev/null +++ b/docs/examples/java/account/update-passkey-token.md @@ -0,0 +1,27 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.updatePasskeyToken( + "", // challengeId + Map.of("a", "b"), // credential + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/update-passkey-verification.md b/docs/examples/java/account/update-passkey-verification.md new file mode 100644 index 00000000..53eed3a1 --- /dev/null +++ b/docs/examples/java/account/update-passkey-verification.md @@ -0,0 +1,27 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.updatePasskeyVerification( + "", // passkeyId + Map.of("a", "b"), // credential + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/update-passkey.md b/docs/examples/java/account/update-passkey.md new file mode 100644 index 00000000..3cf1078a --- /dev/null +++ b/docs/examples/java/account/update-passkey.md @@ -0,0 +1,27 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.updatePasskey( + "", // passkeyId + "", // name + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/analytics/create-event.md b/docs/examples/java/analytics/create-event.md new file mode 100644 index 00000000..e2b8dbe0 --- /dev/null +++ b/docs/examples/java/analytics/create-event.md @@ -0,0 +1,38 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Analytics; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Analytics analytics = new Analytics(client); + +analytics.createEvent( + "", // propertyId + "", // name + "https://example.com", // url + "", // domain (optional) + "", // referrer (optional) + 0L, // screenWidth (optional) + "", // sessionHash (optional) + 0L, // scrollDepth (optional) + 0L, // engagementTime (optional) + List.of(), // props (optional) + "", // userId (optional) + "", // ip (optional) + "", // userAgent (optional) + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/oauth2/authorize-post.md b/docs/examples/java/oauth2/authorize-post.md index 99ed6b8e..077546fd 100644 --- a/docs/examples/java/oauth2/authorize-post.md +++ b/docs/examples/java/oauth2/authorize-post.md @@ -23,7 +23,7 @@ oauth2.authorizePost( "", // prompt (optional) 0L, // max_age (optional) "", // authorization_details (optional) - "", // resource (optional) + List.of(), // resource (optional) "", // audience (optional) "", // request_uri (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/java/oauth2/authorize.md b/docs/examples/java/oauth2/authorize.md index 62a452c8..d5b72cf1 100644 --- a/docs/examples/java/oauth2/authorize.md +++ b/docs/examples/java/oauth2/authorize.md @@ -23,7 +23,7 @@ oauth2.authorize( "", // prompt (optional) 0L, // max_age (optional) "", // authorization_details (optional) - "", // resource (optional) + List.of(), // resource (optional) "", // audience (optional) "", // request_uri (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/java/oauth2/create-device-authorization.md b/docs/examples/java/oauth2/create-device-authorization.md index 0b65a1ab..c2fc4adb 100644 --- a/docs/examples/java/oauth2/create-device-authorization.md +++ b/docs/examples/java/oauth2/create-device-authorization.md @@ -15,7 +15,7 @@ oauth2.createDeviceAuthorization( "", // client_id (optional) "", // scope (optional) "", // authorization_details (optional) - "", // resource (optional) + List.of(), // resource (optional) "", // audience (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/java/oauth2/create-par.md b/docs/examples/java/oauth2/create-par.md index e65bdb54..e1f14de9 100644 --- a/docs/examples/java/oauth2/create-par.md +++ b/docs/examples/java/oauth2/create-par.md @@ -23,7 +23,7 @@ oauth2.createPAR( "", // prompt (optional) 0L, // max_age (optional) "", // authorization_details (optional) - "", // resource (optional) + List.of(), // resource (optional) "", // audience (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/java/oauth2/create-token.md b/docs/examples/java/oauth2/create-token.md index 39b6fa25..5c808df3 100644 --- a/docs/examples/java/oauth2/create-token.md +++ b/docs/examples/java/oauth2/create-token.md @@ -20,7 +20,7 @@ oauth2.createToken( "", // client_secret (optional) "", // code_verifier (optional) "https://example.com", // redirect_uri (optional) - "", // resource (optional) + List.of(), // resource (optional) "", // audience (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/java/teams/create-membership.md b/docs/examples/java/teams/create-membership.md index d7fba7c1..b6d23a6a 100644 --- a/docs/examples/java/teams/create-membership.md +++ b/docs/examples/java/teams/create-membership.md @@ -19,6 +19,7 @@ teams.createMembership( "+12065550100", // phone (optional) "https://example.com", // url (optional) "", // name (optional) + "", // membershipId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/kotlin/account/create-o-auth-2-session.md b/docs/examples/kotlin/account/create-o-auth-2-session.md index cde941c7..c9b3a92a 100644 --- a/docs/examples/kotlin/account/create-o-auth-2-session.md +++ b/docs/examples/kotlin/account/create-o-auth-2-session.md @@ -15,5 +15,6 @@ account.createOAuth2Session( success = "https://example.com", // (optional) failure = "https://example.com", // (optional) scopes = listOf(), // (optional) + state = "", // (optional) ) ``` diff --git a/docs/examples/kotlin/account/create-o-auth-2-token.md b/docs/examples/kotlin/account/create-o-auth-2-token.md index 5e905649..3d5b0d77 100644 --- a/docs/examples/kotlin/account/create-o-auth-2-token.md +++ b/docs/examples/kotlin/account/create-o-auth-2-token.md @@ -15,5 +15,6 @@ account.createOAuth2Token( success = "https://example.com", // (optional) failure = "https://example.com", // (optional) scopes = listOf(), // (optional) + state = "", // (optional) ) ``` diff --git a/docs/examples/kotlin/account/create-passkey-token.md b/docs/examples/kotlin/account/create-passkey-token.md new file mode 100644 index 00000000..52b6bf3d --- /dev/null +++ b/docs/examples/kotlin/account/create-passkey-token.md @@ -0,0 +1,13 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.createPasskeyToken() +``` diff --git a/docs/examples/kotlin/account/create-passkey.md b/docs/examples/kotlin/account/create-passkey.md new file mode 100644 index 00000000..69a06515 --- /dev/null +++ b/docs/examples/kotlin/account/create-passkey.md @@ -0,0 +1,16 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.createPasskey( + passkeyId = "", // (optional) + name = "", // (optional) +) +``` diff --git a/docs/examples/kotlin/account/delete-passkey.md b/docs/examples/kotlin/account/delete-passkey.md new file mode 100644 index 00000000..08bf8fd6 --- /dev/null +++ b/docs/examples/kotlin/account/delete-passkey.md @@ -0,0 +1,15 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.deletePasskey( + passkeyId = "", +) +``` diff --git a/docs/examples/kotlin/account/get-passkey.md b/docs/examples/kotlin/account/get-passkey.md new file mode 100644 index 00000000..ddf9e833 --- /dev/null +++ b/docs/examples/kotlin/account/get-passkey.md @@ -0,0 +1,15 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.getPasskey( + passkeyId = "", +) +``` diff --git a/docs/examples/kotlin/account/list-passkeys.md b/docs/examples/kotlin/account/list-passkeys.md new file mode 100644 index 00000000..74c8add8 --- /dev/null +++ b/docs/examples/kotlin/account/list-passkeys.md @@ -0,0 +1,16 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.listPasskeys( + queries = listOf(), // (optional) + total = false, // (optional) +) +``` diff --git a/docs/examples/kotlin/account/update-passkey-token.md b/docs/examples/kotlin/account/update-passkey-token.md new file mode 100644 index 00000000..f48e29f9 --- /dev/null +++ b/docs/examples/kotlin/account/update-passkey-token.md @@ -0,0 +1,16 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.updatePasskeyToken( + challengeId = "", + credential = mapOf( "a" to "b" ), +) +``` diff --git a/docs/examples/kotlin/account/update-passkey-verification.md b/docs/examples/kotlin/account/update-passkey-verification.md new file mode 100644 index 00000000..41c016a8 --- /dev/null +++ b/docs/examples/kotlin/account/update-passkey-verification.md @@ -0,0 +1,16 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.updatePasskeyVerification( + passkeyId = "", + credential = mapOf( "a" to "b" ), +) +``` diff --git a/docs/examples/kotlin/account/update-passkey.md b/docs/examples/kotlin/account/update-passkey.md new file mode 100644 index 00000000..c8c2bc34 --- /dev/null +++ b/docs/examples/kotlin/account/update-passkey.md @@ -0,0 +1,16 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.updatePasskey( + passkeyId = "", + name = "", +) +``` diff --git a/docs/examples/kotlin/analytics/create-event.md b/docs/examples/kotlin/analytics/create-event.md new file mode 100644 index 00000000..820e0bac --- /dev/null +++ b/docs/examples/kotlin/analytics/create-event.md @@ -0,0 +1,27 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Analytics + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val analytics = Analytics(client) + +val result = analytics.createEvent( + propertyId = "", + name = "", + url = "https://example.com", + domain = "", // (optional) + referrer = "", // (optional) + screenWidth = 0, // (optional) + sessionHash = "", // (optional) + scrollDepth = 0, // (optional) + engagementTime = 0, // (optional) + props = listOf(), // (optional) + userId = "", // (optional) + ip = "", // (optional) + userAgent = "", // (optional) +) +``` diff --git a/docs/examples/kotlin/oauth2/authorize-post.md b/docs/examples/kotlin/oauth2/authorize-post.md index 88bc4ee0..ea490994 100644 --- a/docs/examples/kotlin/oauth2/authorize-post.md +++ b/docs/examples/kotlin/oauth2/authorize-post.md @@ -21,7 +21,7 @@ val result = oauth2.authorizePost( prompt = "", // (optional) maxAge = 0, // (optional) authorizationDetails = "", // (optional) - resource = "", // (optional) + resource = listOf(), // (optional) audience = "", // (optional) requestUri = "", // (optional) ) diff --git a/docs/examples/kotlin/oauth2/authorize.md b/docs/examples/kotlin/oauth2/authorize.md index 4d307e8d..6c598343 100644 --- a/docs/examples/kotlin/oauth2/authorize.md +++ b/docs/examples/kotlin/oauth2/authorize.md @@ -21,7 +21,7 @@ val result = oauth2.authorize( prompt = "", // (optional) maxAge = 0, // (optional) authorizationDetails = "", // (optional) - resource = "", // (optional) + resource = listOf(), // (optional) audience = "", // (optional) requestUri = "", // (optional) ) diff --git a/docs/examples/kotlin/oauth2/create-device-authorization.md b/docs/examples/kotlin/oauth2/create-device-authorization.md index ab307c37..d4fb9b02 100644 --- a/docs/examples/kotlin/oauth2/create-device-authorization.md +++ b/docs/examples/kotlin/oauth2/create-device-authorization.md @@ -13,7 +13,7 @@ val result = oauth2.createDeviceAuthorization( clientId = "", // (optional) scope = "", // (optional) authorizationDetails = "", // (optional) - resource = "", // (optional) + resource = listOf(), // (optional) audience = "", // (optional) ) ``` diff --git a/docs/examples/kotlin/oauth2/create-par.md b/docs/examples/kotlin/oauth2/create-par.md index 752c64eb..213096c1 100644 --- a/docs/examples/kotlin/oauth2/create-par.md +++ b/docs/examples/kotlin/oauth2/create-par.md @@ -21,7 +21,7 @@ val result = oauth2.createPAR( prompt = "", // (optional) maxAge = 0, // (optional) authorizationDetails = "", // (optional) - resource = "", // (optional) + resource = listOf(), // (optional) audience = "", // (optional) ) ``` diff --git a/docs/examples/kotlin/oauth2/create-token.md b/docs/examples/kotlin/oauth2/create-token.md index 5e23344f..a0e4f20e 100644 --- a/docs/examples/kotlin/oauth2/create-token.md +++ b/docs/examples/kotlin/oauth2/create-token.md @@ -18,7 +18,7 @@ val result = oauth2.createToken( clientSecret = "", // (optional) codeVerifier = "", // (optional) redirectUri = "https://example.com", // (optional) - resource = "", // (optional) + resource = listOf(), // (optional) audience = "", // (optional) ) ``` diff --git a/docs/examples/kotlin/teams/create-membership.md b/docs/examples/kotlin/teams/create-membership.md index 219119af..2f73c039 100644 --- a/docs/examples/kotlin/teams/create-membership.md +++ b/docs/examples/kotlin/teams/create-membership.md @@ -17,5 +17,6 @@ val result = teams.createMembership( phone = "+12065550100", // (optional) url = "https://example.com", // (optional) name = "", // (optional) + membershipId = "", // (optional) ) ``` diff --git a/library/src/main/java/io/appwrite/Client.kt b/library/src/main/java/io/appwrite/Client.kt index 3df991a3..332e548b 100644 --- a/library/src/main/java/io/appwrite/Client.kt +++ b/library/src/main/java/io/appwrite/Client.kt @@ -101,8 +101,8 @@ class Client @JvmOverloads constructor( "x-sdk-name" to "Android", "x-sdk-platform" to "client", "x-sdk-language" to "android", - "x-sdk-version" to "29.1.0-rc.3", - "x-appwrite-response-format" to "2.3.0" + "x-sdk-version" to "30.0.0", + "x-appwrite-response-format" to "2.5.0" ) config = mutableMapOf() @@ -434,7 +434,10 @@ class Client @JvmOverloads constructor( headers: Map = mapOf(), params: Map = mapOf(), ): Request { - val filteredParams = params.filterValues { it != null } + // flatten() resolves Query/Query.Builder values before the null filter, + // so a builder that renders to null is dropped like any other null. + val flattenedParams = params.mapValues { flatten(it.value) } + val filteredParams = flattenedParams.filterValues { it != null } val requestHeaders = this.headers.toHeaders().newBuilder() .addAll(headers.toHeaders()) @@ -497,7 +500,7 @@ class Client @JvmOverloads constructor( } builder.build() } else { - params + flattenedParams .toJsonRequestBody() .toRequestBody("application/json".toMediaType()) } @@ -841,4 +844,44 @@ class Client @JvmOverloads constructor( } }) } + + fun flatten(value: Any?): Any? { + if (value is Query.Builder) { + return value.build() + } + if (value is Query) { + return value.toString() + } + + val items = when (value) { + is Array<*> -> value.asList() + is Collection<*> -> value + else -> return value + } + + val out = ArrayList(items.size) + var changed = false + for (item in items) { + val flat = flatten(item) + if (item is Query.Builder || item is Query) { + if (flat is Collection<*>) { + out.addAll(flat) + } else { + out.add(flat) + } + changed = true + continue + } + if ((item is Array<*> || item is Collection<*>) && flat is Collection<*> && flat.all { it is String }) { + out.addAll(flat) + changed = true + continue + } + if (flat !== item) { + changed = true + } + out.add(flat) + } + return if (changed) out else value + } } diff --git a/library/src/main/java/io/appwrite/Query.kt b/library/src/main/java/io/appwrite/Query.kt index 23f100df..1c0faba7 100644 --- a/library/src/main/java/io/appwrite/Query.kt +++ b/library/src/main/java/io/appwrite/Query.kt @@ -377,6 +377,15 @@ class Query( */ fun elemMatch(attribute: String, queries: List) = Query("elemMatch", attribute, queries.map { it.fromJson>() }).toJson() + /** + * Constrain populated related documents on a relationship attribute. + * + * @param attribute The relationship attribute to query. + * @param queries Filter, order, select, and pagination queries applied to related documents. + * @returns The query string. + */ + fun relationship(attribute: String, queries: List) = Query("relationship", attribute, queries.map { it.fromJson>() }).toJson() + /** * Filter resources where attribute is at a specific distance from the given coordinates. * @@ -520,6 +529,377 @@ class Query( */ fun notTouches(attribute: String, values: List) = Query("notTouches", attribute, listOf(values)).toJson() + /** + * Count rows of [attribute], optionally aliased. + * + * @param attribute The attribute to count. Defaults to `*`. + * @param alias Optional result alias. + * @returns The query string. + */ + fun count(attribute: String = "*", alias: String = "") = Query("count", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Count distinct values of [attribute], optionally aliased. + * + * @param attribute The attribute to count. + * @param alias Optional result alias. + * @returns The query string. + */ + fun countDistinct(attribute: String, alias: String = "") = Query("countDistinct", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Sum values of [attribute], optionally aliased. + * + * @param attribute The attribute to sum. + * @param alias Optional result alias. + * @returns The query string. + */ + fun sum(attribute: String, alias: String = "") = Query("sum", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Average values of [attribute], optionally aliased. + * + * @param attribute The attribute to average. + * @param alias Optional result alias. + * @returns The query string. + */ + fun avg(attribute: String, alias: String = "") = Query("avg", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Minimum value of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ + fun min(attribute: String, alias: String = "") = Query("min", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Maximum value of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ + fun max(attribute: String, alias: String = "") = Query("max", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Standard deviation of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ + fun stddev(attribute: String, alias: String = "") = Query("stddev", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Population standard deviation of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ + fun stddevPop(attribute: String, alias: String = "") = Query("stddevPop", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Sample standard deviation of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ + fun stddevSamp(attribute: String, alias: String = "") = Query("stddevSamp", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Variance of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ + fun variance(attribute: String, alias: String = "") = Query("variance", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Population variance of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ + fun varPop(attribute: String, alias: String = "") = Query("varPop", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Sample variance of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ + fun varSamp(attribute: String, alias: String = "") = Query("varSamp", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Bitwise AND of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ + fun bitAnd(attribute: String, alias: String = "") = Query("bitAnd", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Bitwise OR of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ + fun bitOr(attribute: String, alias: String = "") = Query("bitOr", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Bitwise XOR of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ + fun bitXor(attribute: String, alias: String = "") = Query("bitXor", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Group results by [attributes]. + * + * @param attributes Attributes to group by. + * @returns The query string. + */ + fun groupBy(attributes: List) = Query("groupBy", null, attributes).toJson() + + /** + * Filter grouped results. [queries] are nested like `and`/`or`. + * + * @param queries Nested query strings. + * @returns The query string. + */ + fun having(queries: List) = Query("having", null, queries.map { it.fromJson>() }).toJson() + + /** + * Select distinct rows. + * + * @returns The query string. + */ + fun distinct() = Query("distinct").toJson() + + /** + * Column-to-column join ON condition. + * + * @param left Left-hand column. + * @param right Right-hand column. + * @param operator Comparison operator. Defaults to `=`. + * @returns The query string. + */ + fun on(left: String, right: String, operator: String = "=") = Query("on", values = listOf(left, operator, right)).toJson() + + /** + * Inner join. [table] is the joined collection/table ID. + * + * @param table Joined table ID. + * @param left Left-hand join column. + * @param right Right-hand join column. + * @param operator Comparison operator. Defaults to `=`. + * @param alias Optional table alias. + * @returns The query string. + */ + fun join(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("join", table, left, right, operator, alias) + + /** + * Inner join with nested ON queries and no alias. + * + * @param table Joined table ID. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ + fun join(table: String, on: List) = nestedJoin("join", table, "", on) + + /** + * Inner join with nested ON queries and an alias. + * + * @param table Joined table ID. + * @param alias Table alias. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ + fun join(table: String, alias: String, on: List) = nestedJoin("join", table, alias, on) + + /** + * Left join. [table] is the joined collection/table ID. + * + * @param table Joined table ID. + * @param left Left-hand join column. + * @param right Right-hand join column. + * @param operator Comparison operator. Defaults to `=`. + * @param alias Optional table alias. + * @returns The query string. + */ + fun leftJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("leftJoin", table, left, right, operator, alias) + + /** + * Left join with nested ON queries and no alias. + * + * @param table Joined table ID. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ + fun leftJoin(table: String, on: List) = nestedJoin("leftJoin", table, "", on) + + /** + * Left join with nested ON queries and an alias. + * + * @param table Joined table ID. + * @param alias Table alias. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ + fun leftJoin(table: String, alias: String, on: List) = nestedJoin("leftJoin", table, alias, on) + + /** + * Right join. [table] is the joined collection/table ID. + * + * @param table Joined table ID. + * @param left Left-hand join column. + * @param right Right-hand join column. + * @param operator Comparison operator. Defaults to `=`. + * @param alias Optional table alias. + * @returns The query string. + */ + fun rightJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("rightJoin", table, left, right, operator, alias) + + /** + * Right join with nested ON queries and no alias. + * + * @param table Joined table ID. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ + fun rightJoin(table: String, on: List) = nestedJoin("rightJoin", table, "", on) + + /** + * Right join with nested ON queries and an alias. + * + * @param table Joined table ID. + * @param alias Table alias. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ + fun rightJoin(table: String, alias: String, on: List) = nestedJoin("rightJoin", table, alias, on) + + /** + * Full outer join. [table] is the joined collection/table ID. + * + * @param table Joined table ID. + * @param left Left-hand join column. + * @param right Right-hand join column. + * @param operator Comparison operator. Defaults to `=`. + * @param alias Optional table alias. + * @returns The query string. + */ + fun fullOuterJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("fullOuterJoin", table, left, right, operator, alias) + + /** + * Full outer join with nested ON queries and no alias. + * + * @param table Joined table ID. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ + fun fullOuterJoin(table: String, on: List) = nestedJoin("fullOuterJoin", table, "", on) + + /** + * Full outer join with nested ON queries and an alias. + * + * @param table Joined table ID. + * @param alias Table alias. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ + fun fullOuterJoin(table: String, alias: String, on: List) = nestedJoin("fullOuterJoin", table, alias, on) + + /** + * Cross join. [table] is the joined collection/table ID. + * + * @param table Joined table ID. + * @param alias Optional table alias. + * @returns The query string. + */ + fun crossJoin(table: String, alias: String = "") = Query("crossJoin", table, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Filter resources where [attribute] covers the given geometry. + * + * @param attribute The attribute to filter on. + * @param values The coordinate values. + * @returns The query string. + */ + fun covers(attribute: String, values: List) = Query("covers", attribute, listOf(values)).toJson() + + /** + * Filter resources where [attribute] does not cover the given geometry. + * + * @param attribute The attribute to filter on. + * @param values The coordinate values. + * @returns The query string. + */ + fun notCovers(attribute: String, values: List) = Query("notCovers", attribute, listOf(values)).toJson() + + /** + * Filter resources where [attribute] is spatially equal to the given geometry. + * + * @param attribute The attribute to filter on. + * @param values The coordinate values. + * @returns The query string. + */ + fun spatialEquals(attribute: String, values: List) = Query("spatialEquals", attribute, listOf(values)).toJson() + + /** + * Filter resources where [attribute] is not spatially equal to the given geometry. + * + * @param attribute The attribute to filter on. + * @param values The coordinate values. + * @returns The query string. + */ + fun notSpatialEquals(attribute: String, values: List) = Query("notSpatialEquals", attribute, listOf(values)).toJson() + + /** + * Return limit and offset queries for 1-based page pagination. + * + * @param page 1-based page number. + * @param perPage Rows per page. Defaults to 25. + * @returns Limit and offset query strings. + */ + fun page(page: Int, perPage: Int = 25): List { + require(page >= 1) { "Page must be >= 1, got $page" } + require(perPage >= 1) { "Per page must be >= 1, got $perPage" } + return listOf(limit(perPage), offset((page - 1) * perPage)) + } + + /** + * Start a fluent query builder. + * + * @returns A new query builder. + */ + fun builder() = Builder() + + private fun joinQuery(method: String, table: String, left: String, right: String, operator: String, alias: String): String { + val values = if (alias.isNotEmpty()) listOf(left, operator, right, alias) else listOf(left, operator, right) + return Query(method, table, values).toJson() + } + + private fun nestedJoin(method: String, table: String, alias: String, on: List): String { + val values = mutableListOf() + if (alias.isNotEmpty()) { + values.add(alias) + } + on.forEach { values.add(it.fromJson>()) } + return Query(method, table, values).toJson() + } + /** * Parse the value to a list of values. * @@ -533,4 +913,198 @@ class Query( } } } + + class Builder { + private val queries = mutableListOf() + + fun build(): List = queries.toList() + + fun equal(attribute: String, value: Any) = push(Query.equal(attribute, value)) + + fun notEqual(attribute: String, value: Any) = push(Query.notEqual(attribute, value)) + + fun lessThan(attribute: String, value: Any) = push(Query.lessThan(attribute, value)) + + fun lessThanEqual(attribute: String, value: Any) = push(Query.lessThanEqual(attribute, value)) + + fun greaterThan(attribute: String, value: Any) = push(Query.greaterThan(attribute, value)) + + fun greaterThanEqual(attribute: String, value: Any) = push(Query.greaterThanEqual(attribute, value)) + + fun search(attribute: String, value: String) = push(Query.search(attribute, value)) + + fun isNull(attribute: String) = push(Query.isNull(attribute)) + + fun isNotNull(attribute: String) = push(Query.isNotNull(attribute)) + + fun between(attribute: String, start: Any, end: Any) = push(Query.between(attribute, start, end)) + + fun startsWith(attribute: String, value: String) = push(Query.startsWith(attribute, value)) + + fun endsWith(attribute: String, value: String) = push(Query.endsWith(attribute, value)) + + fun select(attributes: List) = push(Query.select(attributes)) + + fun cursorAfter(documentId: String) = push(Query.cursorAfter(documentId)) + + fun cursorBefore(documentId: String) = push(Query.cursorBefore(documentId)) + + fun orderAsc(attribute: String) = push(Query.orderAsc(attribute)) + + fun orderDesc(attribute: String) = push(Query.orderDesc(attribute)) + + fun orderRandom() = push(Query.orderRandom()) + + fun limit(limit: Int) = push(Query.limit(limit)) + + fun offset(offset: Int) = push(Query.offset(offset)) + + fun contains(attribute: String, value: Any) = push(Query.contains(attribute, value)) + + fun containsAny(attribute: String, value: List) = push(Query.containsAny(attribute, value)) + + fun containsAll(attribute: String, value: List) = push(Query.containsAll(attribute, value)) + + fun notContains(attribute: String, value: Any) = push(Query.notContains(attribute, value)) + + fun notSearch(attribute: String, value: String) = push(Query.notSearch(attribute, value)) + + fun notBetween(attribute: String, start: Any, end: Any) = push(Query.notBetween(attribute, start, end)) + + fun notStartsWith(attribute: String, value: String) = push(Query.notStartsWith(attribute, value)) + + fun notEndsWith(attribute: String, value: String) = push(Query.notEndsWith(attribute, value)) + + fun regex(attribute: String, pattern: String) = push(Query.regex(attribute, pattern)) + + fun exists(attributes: List) = push(Query.exists(attributes)) + + fun notExists(attributes: List) = push(Query.notExists(attributes)) + + fun createdBefore(value: String) = push(Query.createdBefore(value)) + + fun createdAfter(value: String) = push(Query.createdAfter(value)) + + fun createdBetween(start: String, end: String) = push(Query.createdBetween(start, end)) + + fun updatedBefore(value: String) = push(Query.updatedBefore(value)) + + fun updatedAfter(value: String) = push(Query.updatedAfter(value)) + + fun updatedBetween(start: String, end: String) = push(Query.updatedBetween(start, end)) + + fun or(queries: List) = push(Query.or(queries)) + + fun and(queries: List) = push(Query.and(queries)) + + fun elemMatch(attribute: String, queries: List) = push(Query.elemMatch(attribute, queries)) + + fun relationship(attribute: String, queries: List) = push(Query.relationship(attribute, queries)) + + fun distanceEqual(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceEqual(attribute, values, distance, meters)) + + fun distanceNotEqual(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceNotEqual(attribute, values, distance, meters)) + + fun distanceGreaterThan(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceGreaterThan(attribute, values, distance, meters)) + + fun distanceLessThan(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceLessThan(attribute, values, distance, meters)) + + fun intersects(attribute: String, values: List) = push(Query.intersects(attribute, values)) + + fun notIntersects(attribute: String, values: List) = push(Query.notIntersects(attribute, values)) + + fun crosses(attribute: String, values: List) = push(Query.crosses(attribute, values)) + + fun notCrosses(attribute: String, values: List) = push(Query.notCrosses(attribute, values)) + + fun overlaps(attribute: String, values: List) = push(Query.overlaps(attribute, values)) + + fun notOverlaps(attribute: String, values: List) = push(Query.notOverlaps(attribute, values)) + + fun touches(attribute: String, values: List) = push(Query.touches(attribute, values)) + + fun notTouches(attribute: String, values: List) = push(Query.notTouches(attribute, values)) + + fun count(attribute: String = "*", alias: String = "") = push(Query.count(attribute, alias)) + + fun countDistinct(attribute: String, alias: String = "") = push(Query.countDistinct(attribute, alias)) + + fun sum(attribute: String, alias: String = "") = push(Query.sum(attribute, alias)) + + fun avg(attribute: String, alias: String = "") = push(Query.avg(attribute, alias)) + + fun min(attribute: String, alias: String = "") = push(Query.min(attribute, alias)) + + fun max(attribute: String, alias: String = "") = push(Query.max(attribute, alias)) + + fun stddev(attribute: String, alias: String = "") = push(Query.stddev(attribute, alias)) + + fun stddevPop(attribute: String, alias: String = "") = push(Query.stddevPop(attribute, alias)) + + fun stddevSamp(attribute: String, alias: String = "") = push(Query.stddevSamp(attribute, alias)) + + fun variance(attribute: String, alias: String = "") = push(Query.variance(attribute, alias)) + + fun varPop(attribute: String, alias: String = "") = push(Query.varPop(attribute, alias)) + + fun varSamp(attribute: String, alias: String = "") = push(Query.varSamp(attribute, alias)) + + fun bitAnd(attribute: String, alias: String = "") = push(Query.bitAnd(attribute, alias)) + + fun bitOr(attribute: String, alias: String = "") = push(Query.bitOr(attribute, alias)) + + fun bitXor(attribute: String, alias: String = "") = push(Query.bitXor(attribute, alias)) + + fun groupBy(attributes: List) = push(Query.groupBy(attributes)) + + fun having(queries: List) = push(Query.having(queries)) + + fun distinct() = push(Query.distinct()) + + fun join(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.join(table, left, right, operator, alias)) + + fun join(table: String, on: List) = push(Query.join(table, on)) + + fun join(table: String, alias: String, on: List) = push(Query.join(table, alias, on)) + + fun leftJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.leftJoin(table, left, right, operator, alias)) + + fun leftJoin(table: String, on: List) = push(Query.leftJoin(table, on)) + + fun leftJoin(table: String, alias: String, on: List) = push(Query.leftJoin(table, alias, on)) + + fun rightJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.rightJoin(table, left, right, operator, alias)) + + fun rightJoin(table: String, on: List) = push(Query.rightJoin(table, on)) + + fun rightJoin(table: String, alias: String, on: List) = push(Query.rightJoin(table, alias, on)) + + fun fullOuterJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.fullOuterJoin(table, left, right, operator, alias)) + + fun fullOuterJoin(table: String, on: List) = push(Query.fullOuterJoin(table, on)) + + fun fullOuterJoin(table: String, alias: String, on: List) = push(Query.fullOuterJoin(table, alias, on)) + + fun crossJoin(table: String, alias: String = "") = push(Query.crossJoin(table, alias)) + + fun vectorDot(attribute: String, vector: List) = push(Query.vectorDot(attribute, vector)) + + fun vectorCosine(attribute: String, vector: List) = push(Query.vectorCosine(attribute, vector)) + + fun vectorEuclidean(attribute: String, vector: List) = push(Query.vectorEuclidean(attribute, vector)) + + fun covers(attribute: String, values: List) = push(Query.covers(attribute, values)) + + fun notCovers(attribute: String, values: List) = push(Query.notCovers(attribute, values)) + + fun spatialEquals(attribute: String, values: List) = push(Query.spatialEquals(attribute, values)) + + fun notSpatialEquals(attribute: String, values: List) = push(Query.notSpatialEquals(attribute, values)) + + fun page(page: Int, perPage: Int = 25) = push(Query.page(page, perPage)) + + private fun push(query: String) = apply { queries.add(query) } + + private fun push(query: List) = apply { queries.addAll(query) } + } } diff --git a/library/src/main/java/io/appwrite/Topic.kt b/library/src/main/java/io/appwrite/Topic.kt index 8945da0e..614dca39 100644 --- a/library/src/main/java/io/appwrite/Topic.kt +++ b/library/src/main/java/io/appwrite/Topic.kt @@ -2,7 +2,7 @@ package io.appwrite /** * Validate the levels passed to `path()`: a non-empty list of non-empty strings, none holding - * the level separator `/` or the wildcards `+` and `#` (use `any()` and `all()` for those). + * the level separator `/` or the MQTT wildcards `+` and `#`. */ private fun levelsOf(levels: List): List { require(levels.isNotEmpty()) { "path() needs at least one level" } @@ -12,10 +12,9 @@ private fun levelsOf(levels: List): List { val parts = level.split("/").joinToString(", ") { "'$it'" } "\"$level\" contains \"/\", split it: path([$parts])" } - require(level != "+") { "\"+\" is reserved, use any()" } - require(!level.contains("+")) { "\"$level\" contains \"+\", use any() for wildcards" } - require(level != "#") { "\"#\" is reserved, use all()" } - require(!level.contains("#")) { "\"$level\" contains \"#\", use all() for wildcards" } + require(!level.contains("+")) { "\"$level\" contains \"+\", not allowed in a topic" } + require(!level.contains("#")) { "\"$level\" contains \"#\", not allowed in a topic" } + require(!level.contains('\u0000')) { "level at index $index contains a NUL character" } } return levels.toList() } @@ -25,13 +24,9 @@ private fun levelsOf(levels: List): List { * * ``` * Topic.path(listOf("user", userId, "notification")) // "user/123/notification" - * Topic.path(listOf("user")).any().path(listOf("notification")) // "user/+/notification" - * Topic.path(listOf("org")).any().path(listOf("logs")).all() // "org/+/logs/#" + * Topic.path(listOf("org", orgId)).path(listOf("logs")) // "org/42/logs" * ``` * - * `any()` matches exactly one level and `all()` everything below, at any depth (including the - * parent itself). `all()` ends the topic, so it returns a [ResolvedTopic]. - * * @throws IllegalArgumentException from `path()` when a level is empty or holds `/`, `+` or `#`. */ class Topic private constructor( @@ -40,29 +35,10 @@ class Topic private constructor( /** Append the given levels. */ fun path(levels: List): Topic = Topic(this.levels + levelsOf(levels)) - /** Append a single-level wildcard (`+`): matches exactly one level. */ - fun any(): Topic = Topic(levels + "+") - - /** Append the multi-level wildcard (`#`) and end the topic. */ - fun all(): ResolvedTopic = ResolvedTopic(levels + "#") - override fun toString(): String = levels.joinToString("/") companion object { /** Start a topic with the given levels, e.g. `Topic.path(listOf("user", userId))`. */ fun path(levels: List): Topic = Topic(levelsOf(levels)) - - /** Start a topic with a single-level wildcard (`+`). */ - fun any(): Topic = Topic(listOf("+")) - - /** The multi-level wildcard alone (`#`): matches every topic. */ - fun all(): ResolvedTopic = ResolvedTopic(listOf("#")) } } - -/** A topic ended by `all()`: nothing can be appended after the `#` wildcard. */ -class ResolvedTopic internal constructor( - private val levels: List, -) { - override fun toString(): String = levels.joinToString("/") -} diff --git a/library/src/main/java/io/appwrite/enums/ExecutionTrigger.kt b/library/src/main/java/io/appwrite/enums/ExecutionTrigger.kt index 3ba84bb5..5cb01125 100644 --- a/library/src/main/java/io/appwrite/enums/ExecutionTrigger.kt +++ b/library/src/main/java/io/appwrite/enums/ExecutionTrigger.kt @@ -8,7 +8,9 @@ enum class ExecutionTrigger(val value: String) { @SerializedName("schedule") SCHEDULE("schedule"), @SerializedName("event") - EVENT("event"); + EVENT("event"), + @SerializedName("email") + EMAIL("email"); override fun toString() = value } diff --git a/library/src/main/java/io/appwrite/models/Account.kt b/library/src/main/java/io/appwrite/models/Account.kt index 38865ff8..2d461144 100644 --- a/library/src/main/java/io/appwrite/models/Account.kt +++ b/library/src/main/java/io/appwrite/models/Account.kt @@ -237,28 +237,28 @@ data class Account( map: Map, nestedType: Class ) = Account( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - name = map["name"] as String, - registration = map["registration"] as String, - status = map["status"] as Boolean, - labels = map["labels"] as List, - passwordUpdate = map["passwordUpdate"] as String, - email = map["email"] as String, - phone = map["phone"] as String, - emailVerification = map["emailVerification"] as Boolean, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + registration = (if (map.containsKey("registration")) map["registration"] else "") as String, + status = (if (map.containsKey("status")) map["status"] else true) as Boolean, + labels = (if (map.containsKey("labels")) map["labels"] else listOf()) as List, + passwordUpdate = (if (map.containsKey("passwordUpdate")) map["passwordUpdate"] else "") as String, + email = (if (map.containsKey("email")) map["email"] else "") as String, + phone = (if (map.containsKey("phone")) map["phone"] else "") as String, + emailVerification = (if (map.containsKey("emailVerification")) map["emailVerification"] else false) as Boolean, emailCanonical = map["emailCanonical"] as? String, emailIsFree = map["emailIsFree"] as? Boolean, emailIsDisposable = map["emailIsDisposable"] as? Boolean, emailIsCorporate = map["emailIsCorporate"] as? Boolean, emailIsCanonical = map["emailIsCanonical"] as? Boolean, passwordPwned = map["passwordPwned"] as? Boolean, - phoneVerification = map["phoneVerification"] as Boolean, - mfa = map["mfa"] as Boolean, + phoneVerification = (if (map.containsKey("phoneVerification")) map["phoneVerification"] else false) as Boolean, + mfa = (if (map.containsKey("mfa")) map["mfa"] else false) as Boolean, prefs = Preferences.from(map = map["prefs"] as Map, nestedType), - targets = (map["targets"] as List>).map { Target.from(map = it) }, - accessedAt = map["accessedAt"] as String, + targets = ((if (map.containsKey("targets")) map["targets"] else listOf()) as List>).map { Target.from(map = it) }, + accessedAt = (if (map.containsKey("accessedAt")) map["accessedAt"] else "") as String, impersonator = map["impersonator"] as? Boolean, impersonatorUserId = map["impersonatorUserId"] as? String, ) diff --git a/library/src/main/java/io/appwrite/models/AlgoArgon2.kt b/library/src/main/java/io/appwrite/models/AlgoArgon2.kt index 39773328..1d26dfd3 100644 --- a/library/src/main/java/io/appwrite/models/AlgoArgon2.kt +++ b/library/src/main/java/io/appwrite/models/AlgoArgon2.kt @@ -44,10 +44,10 @@ data class AlgoArgon2( fun from( map: Map, ) = AlgoArgon2( - type = map["type"] as String, - memoryCost = (map["memoryCost"] as Number).toLong(), - timeCost = (map["timeCost"] as Number).toLong(), - threads = (map["threads"] as Number).toLong(), + type = (if (map.containsKey("type")) map["type"] else "argon2") as String, + memoryCost = ((if (map.containsKey("memoryCost")) map["memoryCost"] else 0) as Number).toLong(), + timeCost = ((if (map.containsKey("timeCost")) map["timeCost"] else 0) as Number).toLong(), + threads = ((if (map.containsKey("threads")) map["threads"] else 0) as Number).toLong(), ) } } diff --git a/library/src/main/java/io/appwrite/models/AlgoBcrypt.kt b/library/src/main/java/io/appwrite/models/AlgoBcrypt.kt index 38317807..2a99dad7 100644 --- a/library/src/main/java/io/appwrite/models/AlgoBcrypt.kt +++ b/library/src/main/java/io/appwrite/models/AlgoBcrypt.kt @@ -23,7 +23,7 @@ data class AlgoBcrypt( fun from( map: Map, ) = AlgoBcrypt( - type = map["type"] as String, + type = (if (map.containsKey("type")) map["type"] else "bcrypt") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/AlgoMd5.kt b/library/src/main/java/io/appwrite/models/AlgoMd5.kt index e46eb99b..d683ea70 100644 --- a/library/src/main/java/io/appwrite/models/AlgoMd5.kt +++ b/library/src/main/java/io/appwrite/models/AlgoMd5.kt @@ -23,7 +23,7 @@ data class AlgoMd5( fun from( map: Map, ) = AlgoMd5( - type = map["type"] as String, + type = (if (map.containsKey("type")) map["type"] else "md5") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/AlgoPhpass.kt b/library/src/main/java/io/appwrite/models/AlgoPhpass.kt index d457a81d..3eba6cad 100644 --- a/library/src/main/java/io/appwrite/models/AlgoPhpass.kt +++ b/library/src/main/java/io/appwrite/models/AlgoPhpass.kt @@ -23,7 +23,7 @@ data class AlgoPhpass( fun from( map: Map, ) = AlgoPhpass( - type = map["type"] as String, + type = (if (map.containsKey("type")) map["type"] else "phpass") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/AlgoScrypt.kt b/library/src/main/java/io/appwrite/models/AlgoScrypt.kt index 92147046..789061fe 100644 --- a/library/src/main/java/io/appwrite/models/AlgoScrypt.kt +++ b/library/src/main/java/io/appwrite/models/AlgoScrypt.kt @@ -51,11 +51,11 @@ data class AlgoScrypt( fun from( map: Map, ) = AlgoScrypt( - type = map["type"] as String, - costCpu = (map["costCpu"] as Number).toLong(), - costMemory = (map["costMemory"] as Number).toLong(), - costParallel = (map["costParallel"] as Number).toLong(), - length = (map["length"] as Number).toLong(), + type = (if (map.containsKey("type")) map["type"] else "scrypt") as String, + costCpu = ((if (map.containsKey("costCpu")) map["costCpu"] else 8) as Number).toLong(), + costMemory = ((if (map.containsKey("costMemory")) map["costMemory"] else 14) as Number).toLong(), + costParallel = ((if (map.containsKey("costParallel")) map["costParallel"] else 1) as Number).toLong(), + length = ((if (map.containsKey("length")) map["length"] else 64) as Number).toLong(), ) } } diff --git a/library/src/main/java/io/appwrite/models/AlgoScryptModified.kt b/library/src/main/java/io/appwrite/models/AlgoScryptModified.kt index 539828ac..ff306fa4 100644 --- a/library/src/main/java/io/appwrite/models/AlgoScryptModified.kt +++ b/library/src/main/java/io/appwrite/models/AlgoScryptModified.kt @@ -44,10 +44,10 @@ data class AlgoScryptModified( fun from( map: Map, ) = AlgoScryptModified( - type = map["type"] as String, - salt = map["salt"] as String, - saltSeparator = map["saltSeparator"] as String, - signerKey = map["signerKey"] as String, + type = (if (map.containsKey("type")) map["type"] else "scryptMod") as String, + salt = (if (map.containsKey("salt")) map["salt"] else "") as String, + saltSeparator = (if (map.containsKey("saltSeparator")) map["saltSeparator"] else "") as String, + signerKey = (if (map.containsKey("signerKey")) map["signerKey"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/AlgoSha.kt b/library/src/main/java/io/appwrite/models/AlgoSha.kt index 45fb0104..5c52f087 100644 --- a/library/src/main/java/io/appwrite/models/AlgoSha.kt +++ b/library/src/main/java/io/appwrite/models/AlgoSha.kt @@ -23,7 +23,7 @@ data class AlgoSha( fun from( map: Map, ) = AlgoSha( - type = map["type"] as String, + type = (if (map.containsKey("type")) map["type"] else "sha") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/App.kt b/library/src/main/java/io/appwrite/models/App.kt index f4a715c8..9125cdcf 100644 --- a/library/src/main/java/io/appwrite/models/App.kt +++ b/library/src/main/java/io/appwrite/models/App.kt @@ -198,32 +198,32 @@ data class App( fun from( map: Map, ) = App( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - name = map["name"] as String, - description = map["description"] as String, - clientUri = map["clientUri"] as String, - logoUri = map["logoUri"] as String, - privacyPolicyUrl = map["privacyPolicyUrl"] as String, - termsUrl = map["termsUrl"] as String, - contacts = map["contacts"] as List, - tagline = map["tagline"] as String, - tags = map["tags"] as List, - labels = map["labels"] as List, - images = map["images"] as List, - supportUrl = map["supportUrl"] as String, - dataDeletionUrl = map["dataDeletionUrl"] as String, - redirectUris = map["redirectUris"] as List, - postLogoutRedirectUris = map["postLogoutRedirectUris"] as List, - enabled = map["enabled"] as Boolean, - type = map["type"] as String, - deviceFlow = map["deviceFlow"] as Boolean, - teamId = map["teamId"] as String, - userId = map["userId"] as String, - installationScopes = map["installationScopes"] as List, - installationRedirectUrl = map["installationRedirectUrl"] as String, - secrets = (map["secrets"] as List>).map { AppSecret.from(map = it) }, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + description = (if (map.containsKey("description")) map["description"] else "") as String, + clientUri = (if (map.containsKey("clientUri")) map["clientUri"] else "") as String, + logoUri = (if (map.containsKey("logoUri")) map["logoUri"] else "") as String, + privacyPolicyUrl = (if (map.containsKey("privacyPolicyUrl")) map["privacyPolicyUrl"] else "") as String, + termsUrl = (if (map.containsKey("termsUrl")) map["termsUrl"] else "") as String, + contacts = (if (map.containsKey("contacts")) map["contacts"] else listOf()) as List, + tagline = (if (map.containsKey("tagline")) map["tagline"] else "") as String, + tags = (if (map.containsKey("tags")) map["tags"] else listOf()) as List, + labels = (if (map.containsKey("labels")) map["labels"] else listOf()) as List, + images = (if (map.containsKey("images")) map["images"] else listOf()) as List, + supportUrl = (if (map.containsKey("supportUrl")) map["supportUrl"] else "") as String, + dataDeletionUrl = (if (map.containsKey("dataDeletionUrl")) map["dataDeletionUrl"] else "") as String, + redirectUris = (if (map.containsKey("redirectUris")) map["redirectUris"] else listOf()) as List, + postLogoutRedirectUris = (if (map.containsKey("postLogoutRedirectUris")) map["postLogoutRedirectUris"] else listOf()) as List, + enabled = (if (map.containsKey("enabled")) map["enabled"] else true) as Boolean, + type = (if (map.containsKey("type")) map["type"] else "confidential") as String, + deviceFlow = (if (map.containsKey("deviceFlow")) map["deviceFlow"] else false) as Boolean, + teamId = (if (map.containsKey("teamId")) map["teamId"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + installationScopes = (if (map.containsKey("installationScopes")) map["installationScopes"] else listOf()) as List, + installationRedirectUrl = (if (map.containsKey("installationRedirectUrl")) map["installationRedirectUrl"] else "") as String, + secrets = ((if (map.containsKey("secrets")) map["secrets"] else listOf()) as List>).map { AppSecret.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/AppInstallation.kt b/library/src/main/java/io/appwrite/models/AppInstallation.kt index 46bf9075..3c6d1f68 100644 --- a/library/src/main/java/io/appwrite/models/AppInstallation.kt +++ b/library/src/main/java/io/appwrite/models/AppInstallation.kt @@ -86,15 +86,15 @@ data class AppInstallation( fun from( map: Map, ) = AppInstallation( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - appId = map["appId"] as String, - teamId = map["teamId"] as String, - scopes = map["scopes"] as List, - authorizationDetails = map["authorizationDetails"] as List>, - createdById = map["createdById"] as String, - createdByName = map["createdByName"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + teamId = (if (map.containsKey("teamId")) map["teamId"] else "") as String, + scopes = (if (map.containsKey("scopes")) map["scopes"] else listOf()) as List, + authorizationDetails = (if (map.containsKey("authorizationDetails")) map["authorizationDetails"] else listOf()) as List>, + createdById = (if (map.containsKey("createdById")) map["createdById"] else "") as String, + createdByName = (if (map.containsKey("createdByName")) map["createdByName"] else "") as String, lastAccessedAt = map["lastAccessedAt"] as? String, ) } diff --git a/library/src/main/java/io/appwrite/models/AppInstallationList.kt b/library/src/main/java/io/appwrite/models/AppInstallationList.kt index f7434cf2..8006d98c 100644 --- a/library/src/main/java/io/appwrite/models/AppInstallationList.kt +++ b/library/src/main/java/io/appwrite/models/AppInstallationList.kt @@ -30,8 +30,8 @@ data class AppInstallationList( fun from( map: Map, ) = AppInstallationList( - total = (map["total"] as Number).toLong(), - installations = (map["installations"] as List>).map { AppInstallation.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + installations = ((if (map.containsKey("installations")) map["installations"] else listOf()) as List>).map { AppInstallation.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/AppKey.kt b/library/src/main/java/io/appwrite/models/AppKey.kt index f59e8d50..7348cd3f 100644 --- a/library/src/main/java/io/appwrite/models/AppKey.kt +++ b/library/src/main/java/io/appwrite/models/AppKey.kt @@ -79,14 +79,14 @@ data class AppKey( fun from( map: Map, ) = AppKey( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - appId = map["appId"] as String, - secret = map["secret"] as String, - hint = map["hint"] as String, - createdById = map["createdById"] as String, - createdByName = map["createdByName"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + hint = (if (map.containsKey("hint")) map["hint"] else "") as String, + createdById = (if (map.containsKey("createdById")) map["createdById"] else "") as String, + createdByName = (if (map.containsKey("createdByName")) map["createdByName"] else "") as String, lastAccessedAt = map["lastAccessedAt"] as? String, ) } diff --git a/library/src/main/java/io/appwrite/models/AppKeyList.kt b/library/src/main/java/io/appwrite/models/AppKeyList.kt index ea4b1cca..854af040 100644 --- a/library/src/main/java/io/appwrite/models/AppKeyList.kt +++ b/library/src/main/java/io/appwrite/models/AppKeyList.kt @@ -30,8 +30,8 @@ data class AppKeyList( fun from( map: Map, ) = AppKeyList( - total = (map["total"] as Number).toLong(), - keys = (map["keys"] as List>).map { AppKey.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + keys = ((if (map.containsKey("keys")) map["keys"] else listOf()) as List>).map { AppKey.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/AppScope.kt b/library/src/main/java/io/appwrite/models/AppScope.kt index 4cc7fe95..dbacafc7 100644 --- a/library/src/main/java/io/appwrite/models/AppScope.kt +++ b/library/src/main/java/io/appwrite/models/AppScope.kt @@ -51,11 +51,11 @@ data class AppScope( fun from( map: Map, ) = AppScope( - value = map["value"] as String, - description = map["description"] as String, - type = map["type"] as String, - category = map["category"] as String, - deprecated = map["deprecated"] as Boolean, + value = (if (map.containsKey("value")) map["value"] else "") as String, + description = (if (map.containsKey("description")) map["description"] else "") as String, + type = (if (map.containsKey("type")) map["type"] else "") as String, + category = (if (map.containsKey("category")) map["category"] else "") as String, + deprecated = (if (map.containsKey("deprecated")) map["deprecated"] else false) as Boolean, ) } } diff --git a/library/src/main/java/io/appwrite/models/AppScopeList.kt b/library/src/main/java/io/appwrite/models/AppScopeList.kt index cbcf91fb..76bc7369 100644 --- a/library/src/main/java/io/appwrite/models/AppScopeList.kt +++ b/library/src/main/java/io/appwrite/models/AppScopeList.kt @@ -30,8 +30,8 @@ data class AppScopeList( fun from( map: Map, ) = AppScopeList( - total = (map["total"] as Number).toLong(), - scopes = (map["scopes"] as List>).map { AppScope.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + scopes = ((if (map.containsKey("scopes")) map["scopes"] else listOf()) as List>).map { AppScope.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/AppSecret.kt b/library/src/main/java/io/appwrite/models/AppSecret.kt index 0349ffbd..b7d70922 100644 --- a/library/src/main/java/io/appwrite/models/AppSecret.kt +++ b/library/src/main/java/io/appwrite/models/AppSecret.kt @@ -79,14 +79,14 @@ data class AppSecret( fun from( map: Map, ) = AppSecret( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - appId = map["appId"] as String, - secret = map["secret"] as String, - hint = map["hint"] as String, - createdById = map["createdById"] as String, - createdByName = map["createdByName"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + hint = (if (map.containsKey("hint")) map["hint"] else "") as String, + createdById = (if (map.containsKey("createdById")) map["createdById"] else "") as String, + createdByName = (if (map.containsKey("createdByName")) map["createdByName"] else "") as String, lastAccessedAt = map["lastAccessedAt"] as? String, ) } diff --git a/library/src/main/java/io/appwrite/models/AppSecretList.kt b/library/src/main/java/io/appwrite/models/AppSecretList.kt index 04d2ebb7..1f2946c4 100644 --- a/library/src/main/java/io/appwrite/models/AppSecretList.kt +++ b/library/src/main/java/io/appwrite/models/AppSecretList.kt @@ -30,8 +30,8 @@ data class AppSecretList( fun from( map: Map, ) = AppSecretList( - total = (map["total"] as Number).toLong(), - secrets = (map["secrets"] as List>).map { AppSecret.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + secrets = ((if (map.containsKey("secrets")) map["secrets"] else listOf()) as List>).map { AppSecret.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/AppSecretPlaintext.kt b/library/src/main/java/io/appwrite/models/AppSecretPlaintext.kt index 098b7f19..7464598c 100644 --- a/library/src/main/java/io/appwrite/models/AppSecretPlaintext.kt +++ b/library/src/main/java/io/appwrite/models/AppSecretPlaintext.kt @@ -79,14 +79,14 @@ data class AppSecretPlaintext( fun from( map: Map, ) = AppSecretPlaintext( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - appId = map["appId"] as String, - secret = map["secret"] as String, - hint = map["hint"] as String, - createdById = map["createdById"] as String, - createdByName = map["createdByName"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + hint = (if (map.containsKey("hint")) map["hint"] else "") as String, + createdById = (if (map.containsKey("createdById")) map["createdById"] else "") as String, + createdByName = (if (map.containsKey("createdByName")) map["createdByName"] else "") as String, lastAccessedAt = map["lastAccessedAt"] as? String, ) } diff --git a/library/src/main/java/io/appwrite/models/AppsList.kt b/library/src/main/java/io/appwrite/models/AppsList.kt index 5dcff821..0ad546f1 100644 --- a/library/src/main/java/io/appwrite/models/AppsList.kt +++ b/library/src/main/java/io/appwrite/models/AppsList.kt @@ -30,8 +30,8 @@ data class AppsList( fun from( map: Map, ) = AppsList( - total = (map["total"] as Number).toLong(), - apps = (map["apps"] as List>).map { App.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + apps = ((if (map.containsKey("apps")) map["apps"] else listOf()) as List>).map { App.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Continent.kt b/library/src/main/java/io/appwrite/models/Continent.kt index 0adbb5fa..e8599a03 100644 --- a/library/src/main/java/io/appwrite/models/Continent.kt +++ b/library/src/main/java/io/appwrite/models/Continent.kt @@ -30,8 +30,8 @@ data class Continent( fun from( map: Map, ) = Continent( - name = map["name"] as String, - code = map["code"] as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + code = (if (map.containsKey("code")) map["code"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/ContinentList.kt b/library/src/main/java/io/appwrite/models/ContinentList.kt index 53f95f2a..0a1f4f34 100644 --- a/library/src/main/java/io/appwrite/models/ContinentList.kt +++ b/library/src/main/java/io/appwrite/models/ContinentList.kt @@ -30,8 +30,8 @@ data class ContinentList( fun from( map: Map, ) = ContinentList( - total = (map["total"] as Number).toLong(), - continents = (map["continents"] as List>).map { Continent.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + continents = ((if (map.containsKey("continents")) map["continents"] else listOf()) as List>).map { Continent.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Country.kt b/library/src/main/java/io/appwrite/models/Country.kt index 7b104f0b..1c3065ea 100644 --- a/library/src/main/java/io/appwrite/models/Country.kt +++ b/library/src/main/java/io/appwrite/models/Country.kt @@ -30,8 +30,8 @@ data class Country( fun from( map: Map, ) = Country( - name = map["name"] as String, - code = map["code"] as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + code = (if (map.containsKey("code")) map["code"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/CountryList.kt b/library/src/main/java/io/appwrite/models/CountryList.kt index 322cdaf8..629bae23 100644 --- a/library/src/main/java/io/appwrite/models/CountryList.kt +++ b/library/src/main/java/io/appwrite/models/CountryList.kt @@ -30,8 +30,8 @@ data class CountryList( fun from( map: Map, ) = CountryList( - total = (map["total"] as Number).toLong(), - countries = (map["countries"] as List>).map { Country.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + countries = ((if (map.containsKey("countries")) map["countries"] else listOf()) as List>).map { Country.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Currency.kt b/library/src/main/java/io/appwrite/models/Currency.kt index 95ff13ac..d78a90ca 100644 --- a/library/src/main/java/io/appwrite/models/Currency.kt +++ b/library/src/main/java/io/appwrite/models/Currency.kt @@ -65,13 +65,13 @@ data class Currency( fun from( map: Map, ) = Currency( - symbol = map["symbol"] as String, - name = map["name"] as String, - symbolNative = map["symbolNative"] as String, - decimalDigits = (map["decimalDigits"] as Number).toLong(), - rounding = (map["rounding"] as Number).toDouble(), - code = map["code"] as String, - namePlural = map["namePlural"] as String, + symbol = (if (map.containsKey("symbol")) map["symbol"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + symbolNative = (if (map.containsKey("symbolNative")) map["symbolNative"] else "") as String, + decimalDigits = ((if (map.containsKey("decimalDigits")) map["decimalDigits"] else 0) as Number).toLong(), + rounding = ((if (map.containsKey("rounding")) map["rounding"] else 0.0) as Number).toDouble(), + code = (if (map.containsKey("code")) map["code"] else "") as String, + namePlural = (if (map.containsKey("namePlural")) map["namePlural"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/CurrencyList.kt b/library/src/main/java/io/appwrite/models/CurrencyList.kt index 8082a2f1..5928c9a1 100644 --- a/library/src/main/java/io/appwrite/models/CurrencyList.kt +++ b/library/src/main/java/io/appwrite/models/CurrencyList.kt @@ -30,8 +30,8 @@ data class CurrencyList( fun from( map: Map, ) = CurrencyList( - total = (map["total"] as Number).toLong(), - currencies = (map["currencies"] as List>).map { Currency.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + currencies = ((if (map.containsKey("currencies")) map["currencies"] else listOf()) as List>).map { Currency.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Document.kt b/library/src/main/java/io/appwrite/models/Document.kt index 20e803e1..a19dd7fa 100644 --- a/library/src/main/java/io/appwrite/models/Document.kt +++ b/library/src/main/java/io/appwrite/models/Document.kt @@ -92,13 +92,13 @@ data class Document( map: Map, nestedType: Class ) = Document( - id = map["\$id"] as String, - sequence = map["\$sequence"] as String, - collectionId = map["\$collectionId"] as String, - databaseId = map["\$databaseId"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - permissions = map["\$permissions"] as List, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + sequence = (if (map.containsKey("\$sequence")) map["\$sequence"] else "") as String, + collectionId = (if (map.containsKey("\$collectionId")) map["\$collectionId"] else "") as String, + databaseId = (if (map.containsKey("\$databaseId")) map["\$databaseId"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + permissions = (if (map.containsKey("\$permissions")) map["\$permissions"] else listOf()) as List, data = map["data"]?.jsonCast(to = nestedType) ?: map.jsonCast(to = nestedType) ) } diff --git a/library/src/main/java/io/appwrite/models/DocumentList.kt b/library/src/main/java/io/appwrite/models/DocumentList.kt index d8741eab..59203167 100644 --- a/library/src/main/java/io/appwrite/models/DocumentList.kt +++ b/library/src/main/java/io/appwrite/models/DocumentList.kt @@ -39,8 +39,8 @@ data class DocumentList( map: Map, nestedType: Class ) = DocumentList( - total = (map["total"] as Number).toLong(), - documents = (map["documents"] as List>).map { Document.from(map = it, nestedType) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + documents = ((if (map.containsKey("documents")) map["documents"] else listOf()) as List>).map { Document.from(map = it, nestedType) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Execution.kt b/library/src/main/java/io/appwrite/models/Execution.kt index c698d9c6..8b364141 100644 --- a/library/src/main/java/io/appwrite/models/Execution.kt +++ b/library/src/main/java/io/appwrite/models/Execution.kt @@ -53,7 +53,7 @@ data class Execution( val deploymentId: String, /** - * The trigger that caused the resource to execute. Possible values can be: `http`, `schedule`, or `event`. + * The trigger that caused the resource to execute. Possible values can be: `http`, `schedule`, `event`, or `email`. */ @SerializedName("trigger") val trigger: ExecutionTrigger, @@ -152,24 +152,24 @@ data class Execution( fun from( map: Map, ) = Execution( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - permissions = map["\$permissions"] as List, - resourceId = map["resourceId"] as String, - resourceType = ExecutionResourceType.values().find { it.value == map["resourceType"] as String }!!, - deploymentId = map["deploymentId"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + permissions = (if (map.containsKey("\$permissions")) map["\$permissions"] else listOf()) as List, + resourceId = (if (map.containsKey("resourceId")) map["resourceId"] else "") as String, + resourceType = ExecutionResourceType.values().find { it.value == (if (map.containsKey("resourceType")) map["resourceType"] else "functions") as String }!!, + deploymentId = (if (map.containsKey("deploymentId")) map["deploymentId"] else "") as String, trigger = ExecutionTrigger.values().find { it.value == map["trigger"] as String }!!, status = ExecutionStatus.values().find { it.value == map["status"] as String }!!, - requestMethod = map["requestMethod"] as String, - requestPath = map["requestPath"] as String, - requestHeaders = (map["requestHeaders"] as List>).map { Headers.from(map = it) }, - responseStatusCode = (map["responseStatusCode"] as Number).toLong(), - responseBody = map["responseBody"] as String, - responseHeaders = (map["responseHeaders"] as List>).map { Headers.from(map = it) }, - logs = map["logs"] as String, - errors = map["errors"] as String, - duration = (map["duration"] as Number).toDouble(), + requestMethod = (if (map.containsKey("requestMethod")) map["requestMethod"] else "") as String, + requestPath = (if (map.containsKey("requestPath")) map["requestPath"] else "") as String, + requestHeaders = ((if (map.containsKey("requestHeaders")) map["requestHeaders"] else listOf()) as List>).map { Headers.from(map = it) }, + responseStatusCode = ((if (map.containsKey("responseStatusCode")) map["responseStatusCode"] else 0) as Number).toLong(), + responseBody = (if (map.containsKey("responseBody")) map["responseBody"] else "") as String, + responseHeaders = ((if (map.containsKey("responseHeaders")) map["responseHeaders"] else listOf()) as List>).map { Headers.from(map = it) }, + logs = (if (map.containsKey("logs")) map["logs"] else "") as String, + errors = (if (map.containsKey("errors")) map["errors"] else "") as String, + duration = ((if (map.containsKey("duration")) map["duration"] else 0.0) as Number).toDouble(), scheduledAt = map["scheduledAt"] as? String, ) } diff --git a/library/src/main/java/io/appwrite/models/ExecutionList.kt b/library/src/main/java/io/appwrite/models/ExecutionList.kt index 4eac1d89..0eba2816 100644 --- a/library/src/main/java/io/appwrite/models/ExecutionList.kt +++ b/library/src/main/java/io/appwrite/models/ExecutionList.kt @@ -30,8 +30,8 @@ data class ExecutionList( fun from( map: Map, ) = ExecutionList( - total = (map["total"] as Number).toLong(), - executions = (map["executions"] as List>).map { Execution.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + executions = ((if (map.containsKey("executions")) map["executions"] else listOf()) as List>).map { Execution.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/File.kt b/library/src/main/java/io/appwrite/models/File.kt index d15a55b1..049bbae7 100644 --- a/library/src/main/java/io/appwrite/models/File.kt +++ b/library/src/main/java/io/appwrite/models/File.kt @@ -128,22 +128,22 @@ data class File( fun from( map: Map, ) = File( - id = map["\$id"] as String, - bucketId = map["bucketId"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - permissions = map["\$permissions"] as List, - name = map["name"] as String, - folder = map["folder"] as String, - key = map["key"] as String, - signature = map["signature"] as String, - mimeType = map["mimeType"] as String, - sizeOriginal = (map["sizeOriginal"] as Number).toLong(), - sizeActual = (map["sizeActual"] as Number).toLong(), - chunksTotal = (map["chunksTotal"] as Number).toLong(), - chunksUploaded = (map["chunksUploaded"] as Number).toLong(), - encryption = map["encryption"] as Boolean, - compression = map["compression"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + bucketId = (if (map.containsKey("bucketId")) map["bucketId"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + permissions = (if (map.containsKey("\$permissions")) map["\$permissions"] else listOf()) as List, + name = (if (map.containsKey("name")) map["name"] else "") as String, + folder = (if (map.containsKey("folder")) map["folder"] else "") as String, + key = (if (map.containsKey("key")) map["key"] else "") as String, + signature = (if (map.containsKey("signature")) map["signature"] else "") as String, + mimeType = (if (map.containsKey("mimeType")) map["mimeType"] else "") as String, + sizeOriginal = ((if (map.containsKey("sizeOriginal")) map["sizeOriginal"] else 0) as Number).toLong(), + sizeActual = ((if (map.containsKey("sizeActual")) map["sizeActual"] else 0) as Number).toLong(), + chunksTotal = ((if (map.containsKey("chunksTotal")) map["chunksTotal"] else 0) as Number).toLong(), + chunksUploaded = ((if (map.containsKey("chunksUploaded")) map["chunksUploaded"] else 0) as Number).toLong(), + encryption = (if (map.containsKey("encryption")) map["encryption"] else false) as Boolean, + compression = (if (map.containsKey("compression")) map["compression"] else "none") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/FileList.kt b/library/src/main/java/io/appwrite/models/FileList.kt index 7a462dc7..1a84a460 100644 --- a/library/src/main/java/io/appwrite/models/FileList.kt +++ b/library/src/main/java/io/appwrite/models/FileList.kt @@ -30,8 +30,8 @@ data class FileList( fun from( map: Map, ) = FileList( - total = (map["total"] as Number).toLong(), - files = (map["files"] as List>).map { File.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + files = ((if (map.containsKey("files")) map["files"] else listOf()) as List>).map { File.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Headers.kt b/library/src/main/java/io/appwrite/models/Headers.kt index 4fece1e4..d2c27770 100644 --- a/library/src/main/java/io/appwrite/models/Headers.kt +++ b/library/src/main/java/io/appwrite/models/Headers.kt @@ -30,8 +30,8 @@ data class Headers( fun from( map: Map, ) = Headers( - name = map["name"] as String, - value = map["value"] as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + value = (if (map.containsKey("value")) map["value"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Identity.kt b/library/src/main/java/io/appwrite/models/Identity.kt index 011c32e4..0269400f 100644 --- a/library/src/main/java/io/appwrite/models/Identity.kt +++ b/library/src/main/java/io/appwrite/models/Identity.kt @@ -93,17 +93,17 @@ data class Identity( fun from( map: Map, ) = Identity( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - userId = map["userId"] as String, - provider = map["provider"] as String, - providerUid = map["providerUid"] as String, - providerEmail = map["providerEmail"] as String, - providerAccessToken = map["providerAccessToken"] as String, - providerAccessTokenExpiry = map["providerAccessTokenExpiry"] as String, - providerRefreshToken = map["providerRefreshToken"] as String, - providerIdToken = map["providerIdToken"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + provider = (if (map.containsKey("provider")) map["provider"] else "") as String, + providerUid = (if (map.containsKey("providerUid")) map["providerUid"] else "") as String, + providerEmail = (if (map.containsKey("providerEmail")) map["providerEmail"] else "") as String, + providerAccessToken = (if (map.containsKey("providerAccessToken")) map["providerAccessToken"] else "") as String, + providerAccessTokenExpiry = (if (map.containsKey("providerAccessTokenExpiry")) map["providerAccessTokenExpiry"] else "") as String, + providerRefreshToken = (if (map.containsKey("providerRefreshToken")) map["providerRefreshToken"] else "") as String, + providerIdToken = (if (map.containsKey("providerIdToken")) map["providerIdToken"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/IdentityList.kt b/library/src/main/java/io/appwrite/models/IdentityList.kt index b92f4e6a..31e5b0ea 100644 --- a/library/src/main/java/io/appwrite/models/IdentityList.kt +++ b/library/src/main/java/io/appwrite/models/IdentityList.kt @@ -30,8 +30,8 @@ data class IdentityList( fun from( map: Map, ) = IdentityList( - total = (map["total"] as Number).toLong(), - identities = (map["identities"] as List>).map { Identity.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + identities = ((if (map.containsKey("identities")) map["identities"] else listOf()) as List>).map { Identity.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Jwt.kt b/library/src/main/java/io/appwrite/models/Jwt.kt index c188a853..7b27865e 100644 --- a/library/src/main/java/io/appwrite/models/Jwt.kt +++ b/library/src/main/java/io/appwrite/models/Jwt.kt @@ -23,7 +23,7 @@ data class Jwt( fun from( map: Map, ) = Jwt( - jwt = map["jwt"] as String, + jwt = (if (map.containsKey("jwt")) map["jwt"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Language.kt b/library/src/main/java/io/appwrite/models/Language.kt index be1f30e8..01eb45bd 100644 --- a/library/src/main/java/io/appwrite/models/Language.kt +++ b/library/src/main/java/io/appwrite/models/Language.kt @@ -37,9 +37,9 @@ data class Language( fun from( map: Map, ) = Language( - name = map["name"] as String, - code = map["code"] as String, - nativeName = map["nativeName"] as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + code = (if (map.containsKey("code")) map["code"] else "") as String, + nativeName = (if (map.containsKey("nativeName")) map["nativeName"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/LanguageList.kt b/library/src/main/java/io/appwrite/models/LanguageList.kt index e1b0e6e7..86f6aed4 100644 --- a/library/src/main/java/io/appwrite/models/LanguageList.kt +++ b/library/src/main/java/io/appwrite/models/LanguageList.kt @@ -30,8 +30,8 @@ data class LanguageList( fun from( map: Map, ) = LanguageList( - total = (map["total"] as Number).toLong(), - languages = (map["languages"] as List>).map { Language.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + languages = ((if (map.containsKey("languages")) map["languages"] else listOf()) as List>).map { Language.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Locale.kt b/library/src/main/java/io/appwrite/models/Locale.kt index 689de89c..dc6a1ad8 100644 --- a/library/src/main/java/io/appwrite/models/Locale.kt +++ b/library/src/main/java/io/appwrite/models/Locale.kt @@ -142,13 +142,13 @@ data class Locale( fun from( map: Map, ) = Locale( - ip = map["ip"] as String, - countryCode = map["countryCode"] as String, - country = map["country"] as String, - continentCode = map["continentCode"] as String, - continent = map["continent"] as String, - eu = map["eu"] as Boolean, - currency = map["currency"] as String, + ip = (if (map.containsKey("ip")) map["ip"] else "") as String, + countryCode = (if (map.containsKey("countryCode")) map["countryCode"] else "") as String, + country = (if (map.containsKey("country")) map["country"] else "") as String, + continentCode = (if (map.containsKey("continentCode")) map["continentCode"] else "") as String, + continent = (if (map.containsKey("continent")) map["continent"] else "") as String, + eu = (if (map.containsKey("eu")) map["eu"] else false) as Boolean, + currency = (if (map.containsKey("currency")) map["currency"] else "") as String, city = map["city"] as? String, timeZone = map["timeZone"] as? String, postalCode = map["postalCode"] as? String, diff --git a/library/src/main/java/io/appwrite/models/LocaleCode.kt b/library/src/main/java/io/appwrite/models/LocaleCode.kt index 602d9ed9..3bee5288 100644 --- a/library/src/main/java/io/appwrite/models/LocaleCode.kt +++ b/library/src/main/java/io/appwrite/models/LocaleCode.kt @@ -30,8 +30,8 @@ data class LocaleCode( fun from( map: Map, ) = LocaleCode( - code = map["code"] as String, - name = map["name"] as String, + code = (if (map.containsKey("code")) map["code"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/LocaleCodeList.kt b/library/src/main/java/io/appwrite/models/LocaleCodeList.kt index e0c568a7..86cb808f 100644 --- a/library/src/main/java/io/appwrite/models/LocaleCodeList.kt +++ b/library/src/main/java/io/appwrite/models/LocaleCodeList.kt @@ -30,8 +30,8 @@ data class LocaleCodeList( fun from( map: Map, ) = LocaleCodeList( - total = (map["total"] as Number).toLong(), - localeCodes = (map["localeCodes"] as List>).map { LocaleCode.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + localeCodes = ((if (map.containsKey("localeCodes")) map["localeCodes"] else listOf()) as List>).map { LocaleCode.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Membership.kt b/library/src/main/java/io/appwrite/models/Membership.kt index 7d5b963b..3d0c37b9 100644 --- a/library/src/main/java/io/appwrite/models/Membership.kt +++ b/library/src/main/java/io/appwrite/models/Membership.kt @@ -121,21 +121,21 @@ data class Membership( fun from( map: Map, ) = Membership( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - userId = map["userId"] as String, - userName = map["userName"] as String, - userEmail = map["userEmail"] as String, - userPhone = map["userPhone"] as String, - teamId = map["teamId"] as String, - teamName = map["teamName"] as String, - invited = map["invited"] as String, - joined = map["joined"] as String, - confirm = map["confirm"] as Boolean, - mfa = map["mfa"] as Boolean, - userAccessedAt = map["userAccessedAt"] as String, - roles = map["roles"] as List, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + userName = (if (map.containsKey("userName")) map["userName"] else "") as String, + userEmail = (if (map.containsKey("userEmail")) map["userEmail"] else "") as String, + userPhone = (if (map.containsKey("userPhone")) map["userPhone"] else "") as String, + teamId = (if (map.containsKey("teamId")) map["teamId"] else "") as String, + teamName = (if (map.containsKey("teamName")) map["teamName"] else "") as String, + invited = (if (map.containsKey("invited")) map["invited"] else "") as String, + joined = (if (map.containsKey("joined")) map["joined"] else "") as String, + confirm = (if (map.containsKey("confirm")) map["confirm"] else false) as Boolean, + mfa = (if (map.containsKey("mfa")) map["mfa"] else false) as Boolean, + userAccessedAt = (if (map.containsKey("userAccessedAt")) map["userAccessedAt"] else "") as String, + roles = (if (map.containsKey("roles")) map["roles"] else listOf()) as List, ) } } diff --git a/library/src/main/java/io/appwrite/models/MembershipList.kt b/library/src/main/java/io/appwrite/models/MembershipList.kt index 8376bc96..f6f4afe7 100644 --- a/library/src/main/java/io/appwrite/models/MembershipList.kt +++ b/library/src/main/java/io/appwrite/models/MembershipList.kt @@ -30,8 +30,8 @@ data class MembershipList( fun from( map: Map, ) = MembershipList( - total = (map["total"] as Number).toLong(), - memberships = (map["memberships"] as List>).map { Membership.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + memberships = ((if (map.containsKey("memberships")) map["memberships"] else listOf()) as List>).map { Membership.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/MfaChallenge.kt b/library/src/main/java/io/appwrite/models/MfaChallenge.kt index b3e9f6bd..c17e792d 100644 --- a/library/src/main/java/io/appwrite/models/MfaChallenge.kt +++ b/library/src/main/java/io/appwrite/models/MfaChallenge.kt @@ -44,10 +44,10 @@ data class MfaChallenge( fun from( map: Map, ) = MfaChallenge( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - userId = map["userId"] as String, - expire = map["expire"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/MfaFactors.kt b/library/src/main/java/io/appwrite/models/MfaFactors.kt index 20f7bb36..42872fa2 100644 --- a/library/src/main/java/io/appwrite/models/MfaFactors.kt +++ b/library/src/main/java/io/appwrite/models/MfaFactors.kt @@ -51,11 +51,11 @@ data class MfaFactors( fun from( map: Map, ) = MfaFactors( - totp = map["totp"] as Boolean, - phone = map["phone"] as Boolean, - email = map["email"] as Boolean, - recoveryCode = map["recoveryCode"] as Boolean, - custom = map["custom"] as Boolean, + totp = (if (map.containsKey("totp")) map["totp"] else false) as Boolean, + phone = (if (map.containsKey("phone")) map["phone"] else false) as Boolean, + email = (if (map.containsKey("email")) map["email"] else false) as Boolean, + recoveryCode = (if (map.containsKey("recoveryCode")) map["recoveryCode"] else false) as Boolean, + custom = (if (map.containsKey("custom")) map["custom"] else false) as Boolean, ) } } diff --git a/library/src/main/java/io/appwrite/models/MfaRecoveryCodes.kt b/library/src/main/java/io/appwrite/models/MfaRecoveryCodes.kt index a8967496..25e783cd 100644 --- a/library/src/main/java/io/appwrite/models/MfaRecoveryCodes.kt +++ b/library/src/main/java/io/appwrite/models/MfaRecoveryCodes.kt @@ -23,7 +23,7 @@ data class MfaRecoveryCodes( fun from( map: Map, ) = MfaRecoveryCodes( - recoveryCodes = map["recoveryCodes"] as List, + recoveryCodes = (if (map.containsKey("recoveryCodes")) map["recoveryCodes"] else listOf()) as List, ) } } diff --git a/library/src/main/java/io/appwrite/models/MfaType.kt b/library/src/main/java/io/appwrite/models/MfaType.kt index e20b7f66..5b7b9710 100644 --- a/library/src/main/java/io/appwrite/models/MfaType.kt +++ b/library/src/main/java/io/appwrite/models/MfaType.kt @@ -30,8 +30,8 @@ data class MfaType( fun from( map: Map, ) = MfaType( - secret = map["secret"] as String, - uri = map["uri"] as String, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + uri = (if (map.containsKey("uri")) map["uri"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Approve.kt b/library/src/main/java/io/appwrite/models/Oauth2Approve.kt index e09ab9ea..0aabf4de 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Approve.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Approve.kt @@ -23,7 +23,7 @@ data class Oauth2Approve( fun from( map: Map, ) = Oauth2Approve( - redirectUrl = map["redirectUrl"] as String, + redirectUrl = (if (map.containsKey("redirectUrl")) map["redirectUrl"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Authorize.kt b/library/src/main/java/io/appwrite/models/Oauth2Authorize.kt index e0812f7e..fe878e3a 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Authorize.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Authorize.kt @@ -30,8 +30,8 @@ data class Oauth2Authorize( fun from( map: Map, ) = Oauth2Authorize( - grantId = map["grantId"] as String, - redirectUrl = map["redirectUrl"] as String, + grantId = (if (map.containsKey("grantId")) map["grantId"] else "") as String, + redirectUrl = (if (map.containsKey("redirectUrl")) map["redirectUrl"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Consent.kt b/library/src/main/java/io/appwrite/models/Oauth2Consent.kt index 7769e9b1..28cde915 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Consent.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Consent.kt @@ -86,16 +86,16 @@ data class Oauth2Consent( fun from( map: Map, ) = Oauth2Consent( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - userId = map["userId"] as String, - appId = map["appId"] as String, - cimdUrl = map["cimdUrl"] as String, - scopes = map["scopes"] as List, - resources = map["resources"] as List, - authorizationDetails = map["authorizationDetails"] as String, - expire = map["expire"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + cimdUrl = (if (map.containsKey("cimdUrl")) map["cimdUrl"] else "") as String, + scopes = (if (map.containsKey("scopes")) map["scopes"] else listOf()) as List, + resources = (if (map.containsKey("resources")) map["resources"] else listOf()) as List, + authorizationDetails = (if (map.containsKey("authorizationDetails")) map["authorizationDetails"] else "[]") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2ConsentList.kt b/library/src/main/java/io/appwrite/models/Oauth2ConsentList.kt index da6acd38..2a6337bd 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2ConsentList.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2ConsentList.kt @@ -30,8 +30,8 @@ data class Oauth2ConsentList( fun from( map: Map, ) = Oauth2ConsentList( - total = (map["total"] as Number).toLong(), - consents = (map["consents"] as List>).map { Oauth2Consent.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + consents = ((if (map.containsKey("consents")) map["consents"] else listOf()) as List>).map { Oauth2Consent.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2ConsentToken.kt b/library/src/main/java/io/appwrite/models/Oauth2ConsentToken.kt index 31f6249b..1f5c1dfc 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2ConsentToken.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2ConsentToken.kt @@ -93,17 +93,17 @@ data class Oauth2ConsentToken( fun from( map: Map, ) = Oauth2ConsentToken( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - consentId = map["consentId"] as String, - userId = map["userId"] as String, - appId = map["appId"] as String, - cimdUrl = map["cimdUrl"] as String, - scopes = map["scopes"] as List, - resources = map["resources"] as List, - authorizationDetails = map["authorizationDetails"] as String, - expire = map["expire"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + consentId = (if (map.containsKey("consentId")) map["consentId"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + cimdUrl = (if (map.containsKey("cimdUrl")) map["cimdUrl"] else "") as String, + scopes = (if (map.containsKey("scopes")) map["scopes"] else listOf()) as List, + resources = (if (map.containsKey("resources")) map["resources"] else listOf()) as List, + authorizationDetails = (if (map.containsKey("authorizationDetails")) map["authorizationDetails"] else "[]") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2ConsentTokenList.kt b/library/src/main/java/io/appwrite/models/Oauth2ConsentTokenList.kt index 09278f45..b5d4c524 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2ConsentTokenList.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2ConsentTokenList.kt @@ -30,8 +30,8 @@ data class Oauth2ConsentTokenList( fun from( map: Map, ) = Oauth2ConsentTokenList( - total = (map["total"] as Number).toLong(), - tokens = (map["tokens"] as List>).map { Oauth2ConsentToken.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + tokens = ((if (map.containsKey("tokens")) map["tokens"] else listOf()) as List>).map { Oauth2ConsentToken.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2DeviceAuthorization.kt b/library/src/main/java/io/appwrite/models/Oauth2DeviceAuthorization.kt index 58c7e5ee..f4ca6e64 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2DeviceAuthorization.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2DeviceAuthorization.kt @@ -58,12 +58,12 @@ data class Oauth2DeviceAuthorization( fun from( map: Map, ) = Oauth2DeviceAuthorization( - device_code = map["device_code"] as String, - user_code = map["user_code"] as String, - verification_uri = map["verification_uri"] as String, - verification_uri_complete = map["verification_uri_complete"] as String, - expires_in = (map["expires_in"] as Number).toLong(), - interval = (map["interval"] as Number).toLong(), + device_code = (if (map.containsKey("device_code")) map["device_code"] else "") as String, + user_code = (if (map.containsKey("user_code")) map["user_code"] else "") as String, + verification_uri = (if (map.containsKey("verification_uri")) map["verification_uri"] else "") as String, + verification_uri_complete = (if (map.containsKey("verification_uri_complete")) map["verification_uri_complete"] else "") as String, + expires_in = ((if (map.containsKey("expires_in")) map["expires_in"] else 0) as Number).toLong(), + interval = ((if (map.containsKey("interval")) map["interval"] else 0) as Number).toLong(), ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Grant.kt b/library/src/main/java/io/appwrite/models/Oauth2Grant.kt index ef1a7c39..914b6f33 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Grant.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Grant.kt @@ -100,18 +100,18 @@ data class Oauth2Grant( fun from( map: Map, ) = Oauth2Grant( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - userId = map["userId"] as String, - appId = map["appId"] as String, - scopes = map["scopes"] as List, - resources = map["resources"] as List, - authorizationDetails = map["authorizationDetails"] as String, - prompt = map["prompt"] as String, - redirectUri = map["redirectUri"] as String, - authTime = (map["authTime"] as Number).toLong(), - expire = map["expire"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + scopes = (if (map.containsKey("scopes")) map["scopes"] else listOf()) as List, + resources = (if (map.containsKey("resources")) map["resources"] else listOf()) as List, + authorizationDetails = (if (map.containsKey("authorizationDetails")) map["authorizationDetails"] else "[]") as String, + prompt = (if (map.containsKey("prompt")) map["prompt"] else "") as String, + redirectUri = (if (map.containsKey("redirectUri")) map["redirectUri"] else "") as String, + authTime = ((if (map.containsKey("authTime")) map["authTime"] else 0) as Number).toLong(), + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Organization.kt b/library/src/main/java/io/appwrite/models/Oauth2Organization.kt index 605916a0..47b6a8ff 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Organization.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Organization.kt @@ -23,7 +23,7 @@ data class Oauth2Organization( fun from( map: Map, ) = Oauth2Organization( - id = map["\$id"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2OrganizationList.kt b/library/src/main/java/io/appwrite/models/Oauth2OrganizationList.kt index b90ee636..48f45063 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2OrganizationList.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2OrganizationList.kt @@ -30,8 +30,8 @@ data class Oauth2OrganizationList( fun from( map: Map, ) = Oauth2OrganizationList( - total = (map["total"] as Number).toLong(), - organizations = (map["organizations"] as List>).map { Oauth2Organization.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + organizations = ((if (map.containsKey("organizations")) map["organizations"] else listOf()) as List>).map { Oauth2Organization.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2PAR.kt b/library/src/main/java/io/appwrite/models/Oauth2PAR.kt index 94d175ee..1091bdd5 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2PAR.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2PAR.kt @@ -30,8 +30,8 @@ data class Oauth2PAR( fun from( map: Map, ) = Oauth2PAR( - request_uri = map["request_uri"] as String, - expires_in = (map["expires_in"] as Number).toLong(), + request_uri = (if (map.containsKey("request_uri")) map["request_uri"] else "") as String, + expires_in = ((if (map.containsKey("expires_in")) map["expires_in"] else 0) as Number).toLong(), ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Project.kt b/library/src/main/java/io/appwrite/models/Oauth2Project.kt index eb085dce..95c79e0b 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Project.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Project.kt @@ -37,9 +37,9 @@ data class Oauth2Project( fun from( map: Map, ) = Oauth2Project( - id = map["\$id"] as String, - region = map["region"] as String, - endpoint = map["endpoint"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + region = (if (map.containsKey("region")) map["region"] else "") as String, + endpoint = (if (map.containsKey("endpoint")) map["endpoint"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2ProjectList.kt b/library/src/main/java/io/appwrite/models/Oauth2ProjectList.kt index 551b9ee4..5eb46ed5 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2ProjectList.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2ProjectList.kt @@ -30,8 +30,8 @@ data class Oauth2ProjectList( fun from( map: Map, ) = Oauth2ProjectList( - total = (map["total"] as Number).toLong(), - projects = (map["projects"] as List>).map { Oauth2Project.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + projects = ((if (map.containsKey("projects")) map["projects"] else listOf()) as List>).map { Oauth2Project.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Reject.kt b/library/src/main/java/io/appwrite/models/Oauth2Reject.kt index b23f308c..42b3a3a3 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Reject.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Reject.kt @@ -23,7 +23,7 @@ data class Oauth2Reject( fun from( map: Map, ) = Oauth2Reject( - redirectUrl = map["redirectUrl"] as String, + redirectUrl = (if (map.containsKey("redirectUrl")) map["redirectUrl"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Token.kt b/library/src/main/java/io/appwrite/models/Oauth2Token.kt index f36f7768..53547c23 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Token.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Token.kt @@ -65,11 +65,11 @@ data class Oauth2Token( fun from( map: Map, ) = Oauth2Token( - access_token = map["access_token"] as String, - token_type = map["token_type"] as String, - expires_in = (map["expires_in"] as Number).toLong(), - refresh_token = map["refresh_token"] as String, - scope = map["scope"] as String, + access_token = (if (map.containsKey("access_token")) map["access_token"] else "") as String, + token_type = (if (map.containsKey("token_type")) map["token_type"] else "Bearer") as String, + expires_in = ((if (map.containsKey("expires_in")) map["expires_in"] else 0) as Number).toLong(), + refresh_token = (if (map.containsKey("refresh_token")) map["refresh_token"] else "") as String, + scope = (if (map.containsKey("scope")) map["scope"] else "") as String, authorization_details = map["authorization_details"] as? String, id_token = map["id_token"] as? String, ) diff --git a/library/src/main/java/io/appwrite/models/Passkey.kt b/library/src/main/java/io/appwrite/models/Passkey.kt new file mode 100644 index 00000000..90f0e497 --- /dev/null +++ b/library/src/main/java/io/appwrite/models/Passkey.kt @@ -0,0 +1,61 @@ +package io.appwrite.models + +import com.google.gson.annotations.SerializedName +import io.appwrite.extensions.jsonCast + +/** + * Passkey + */ +data class Passkey( + /** + * Passkey ID. + */ + @SerializedName("\$id") + val id: String, + + /** + * Passkey creation date in ISO 8601 format. + */ + @SerializedName("\$createdAt") + val createdAt: String, + + /** + * Passkey update date in ISO 8601 format. + */ + @SerializedName("\$updatedAt") + val updatedAt: String, + + /** + * Passkey name. + */ + @SerializedName("name") + val name: String, + + /** + * Most recent sign-in with this passkey in ISO 8601 format. Empty until the passkey is used to sign in. + */ + @SerializedName("accessedAt") + val accessedAt: String, + +) { + fun toMap(): Map = mapOf( + "\$id" to id as Any, + "\$createdAt" to createdAt as Any, + "\$updatedAt" to updatedAt as Any, + "name" to name as Any, + "accessedAt" to accessedAt as Any, + ) + + companion object { + @Suppress("UNCHECKED_CAST") + fun from( + map: Map, + ) = Passkey( + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + accessedAt = (if (map.containsKey("accessedAt")) map["accessedAt"] else "") as String, + ) + } +} diff --git a/library/src/main/java/io/appwrite/models/PasskeyChallenge.kt b/library/src/main/java/io/appwrite/models/PasskeyChallenge.kt new file mode 100644 index 00000000..dcdcb771 --- /dev/null +++ b/library/src/main/java/io/appwrite/models/PasskeyChallenge.kt @@ -0,0 +1,61 @@ +package io.appwrite.models + +import com.google.gson.annotations.SerializedName +import io.appwrite.extensions.jsonCast + +/** + * Passkey Challenge + */ +data class PasskeyChallenge( + /** + * Challenge ID. When signing in, pass it back with the credential to receive a token. + */ + @SerializedName("\$id") + val id: String, + + /** + * Challenge creation date in ISO 8601 format. + */ + @SerializedName("\$createdAt") + val createdAt: String, + + /** + * ID of the passkey being registered. Empty for sign-in challenges. + */ + @SerializedName("passkeyId") + val passkeyId: String, + + /** + * Challenge expiration date in ISO 8601 format. + */ + @SerializedName("expire") + val expire: String, + + /** + * WebAuthn options in JSON form. Pass them to `PublicKeyCredential.parseCreationOptionsFromJSON()` when registering, or `PublicKeyCredential.parseRequestOptionsFromJSON()` when signing in. + */ + @SerializedName("publicKey") + val publicKey: Map, + +) { + fun toMap(): Map = mapOf( + "\$id" to id as Any, + "\$createdAt" to createdAt as Any, + "passkeyId" to passkeyId as Any, + "expire" to expire as Any, + "publicKey" to publicKey as Any, + ) + + companion object { + @Suppress("UNCHECKED_CAST") + fun from( + map: Map, + ) = PasskeyChallenge( + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + passkeyId = (if (map.containsKey("passkeyId")) map["passkeyId"] else "") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, + publicKey = (if (map.containsKey("publicKey")) map["publicKey"] else mapOf()) as Map, + ) + } +} diff --git a/library/src/main/java/io/appwrite/models/PasskeyList.kt b/library/src/main/java/io/appwrite/models/PasskeyList.kt new file mode 100644 index 00000000..05692293 --- /dev/null +++ b/library/src/main/java/io/appwrite/models/PasskeyList.kt @@ -0,0 +1,37 @@ +package io.appwrite.models + +import com.google.gson.annotations.SerializedName +import io.appwrite.extensions.jsonCast + +/** + * Passkeys List + */ +data class PasskeyList( + /** + * Total number of passkeys that matched your query. + */ + @SerializedName("total") + val total: Long, + + /** + * List of passkeys. + */ + @SerializedName("passkeys") + val passkeys: List, + +) { + fun toMap(): Map = mapOf( + "total" to total as Any, + "passkeys" to passkeys.map { it.toMap() } as Any, + ) + + companion object { + @Suppress("UNCHECKED_CAST") + fun from( + map: Map, + ) = PasskeyList( + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + passkeys = ((if (map.containsKey("passkeys")) map["passkeys"] else listOf()) as List>).map { Passkey.from(map = it) }, + ) + } +} diff --git a/library/src/main/java/io/appwrite/models/Phone.kt b/library/src/main/java/io/appwrite/models/Phone.kt index 74bfa518..12c8eb63 100644 --- a/library/src/main/java/io/appwrite/models/Phone.kt +++ b/library/src/main/java/io/appwrite/models/Phone.kt @@ -37,9 +37,9 @@ data class Phone( fun from( map: Map, ) = Phone( - code = map["code"] as String, - countryCode = map["countryCode"] as String, - countryName = map["countryName"] as String, + code = (if (map.containsKey("code")) map["code"] else "") as String, + countryCode = (if (map.containsKey("countryCode")) map["countryCode"] else "") as String, + countryName = (if (map.containsKey("countryName")) map["countryName"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/PhoneList.kt b/library/src/main/java/io/appwrite/models/PhoneList.kt index fc0ffa3c..2c3f9cd1 100644 --- a/library/src/main/java/io/appwrite/models/PhoneList.kt +++ b/library/src/main/java/io/appwrite/models/PhoneList.kt @@ -30,8 +30,8 @@ data class PhoneList( fun from( map: Map, ) = PhoneList( - total = (map["total"] as Number).toLong(), - phones = (map["phones"] as List>).map { Phone.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + phones = ((if (map.containsKey("phones")) map["phones"] else listOf()) as List>).map { Phone.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Presence.kt b/library/src/main/java/io/appwrite/models/Presence.kt index c7f60731..b8e27fc0 100644 --- a/library/src/main/java/io/appwrite/models/Presence.kt +++ b/library/src/main/java/io/appwrite/models/Presence.kt @@ -79,13 +79,13 @@ data class Presence( fun from( map: Map, ) = Presence( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - permissions = map["\$permissions"] as List, - userId = map["userId"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + permissions = (if (map.containsKey("\$permissions")) map["\$permissions"] else listOf()) as List, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, status = map["status"] as? String, - source = map["source"] as String, + source = (if (map.containsKey("source")) map["source"] else "") as String, expiresAt = map["expiresAt"] as? String, metadata = map["metadata"] as? Map, ) diff --git a/library/src/main/java/io/appwrite/models/PresenceList.kt b/library/src/main/java/io/appwrite/models/PresenceList.kt index b69d3e13..01a20264 100644 --- a/library/src/main/java/io/appwrite/models/PresenceList.kt +++ b/library/src/main/java/io/appwrite/models/PresenceList.kt @@ -30,8 +30,8 @@ data class PresenceList( fun from( map: Map, ) = PresenceList( - total = (map["total"] as Number).toLong(), - presences = (map["presences"] as List>).map { Presence.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + presences = ((if (map.containsKey("presences")) map["presences"] else listOf()) as List>).map { Presence.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Row.kt b/library/src/main/java/io/appwrite/models/Row.kt index 1f190104..d0968469 100644 --- a/library/src/main/java/io/appwrite/models/Row.kt +++ b/library/src/main/java/io/appwrite/models/Row.kt @@ -92,13 +92,13 @@ data class Row( map: Map, nestedType: Class ) = Row( - id = map["\$id"] as String, - sequence = map["\$sequence"] as String, - tableId = map["\$tableId"] as String, - databaseId = map["\$databaseId"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - permissions = map["\$permissions"] as List, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + sequence = (if (map.containsKey("\$sequence")) map["\$sequence"] else "") as String, + tableId = (if (map.containsKey("\$tableId")) map["\$tableId"] else "") as String, + databaseId = (if (map.containsKey("\$databaseId")) map["\$databaseId"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + permissions = (if (map.containsKey("\$permissions")) map["\$permissions"] else listOf()) as List, data = map["data"]?.jsonCast(to = nestedType) ?: map.jsonCast(to = nestedType) ) } diff --git a/library/src/main/java/io/appwrite/models/RowList.kt b/library/src/main/java/io/appwrite/models/RowList.kt index f9c8f61e..0d52602c 100644 --- a/library/src/main/java/io/appwrite/models/RowList.kt +++ b/library/src/main/java/io/appwrite/models/RowList.kt @@ -39,8 +39,8 @@ data class RowList( map: Map, nestedType: Class ) = RowList( - total = (map["total"] as Number).toLong(), - rows = (map["rows"] as List>).map { Row.from(map = it, nestedType) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + rows = ((if (map.containsKey("rows")) map["rows"] else listOf()) as List>).map { Row.from(map = it, nestedType) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Session.kt b/library/src/main/java/io/appwrite/models/Session.kt index c9c39509..884d5814 100644 --- a/library/src/main/java/io/appwrite/models/Session.kt +++ b/library/src/main/java/io/appwrite/models/Session.kt @@ -219,35 +219,35 @@ data class Session( fun from( map: Map, ) = Session( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - userId = map["userId"] as String, - expire = map["expire"] as String, - provider = map["provider"] as String, - providerUid = map["providerUid"] as String, - providerAccessToken = map["providerAccessToken"] as String, - providerAccessTokenExpiry = map["providerAccessTokenExpiry"] as String, - providerRefreshToken = map["providerRefreshToken"] as String, - ip = map["ip"] as String, - osCode = map["osCode"] as String, - osName = map["osName"] as String, - osVersion = map["osVersion"] as String, - clientType = map["clientType"] as String, - clientCode = map["clientCode"] as String, - clientName = map["clientName"] as String, - clientVersion = map["clientVersion"] as String, - clientEngine = map["clientEngine"] as String, - clientEngineVersion = map["clientEngineVersion"] as String, - deviceName = map["deviceName"] as String, - deviceBrand = map["deviceBrand"] as String, - deviceModel = map["deviceModel"] as String, - countryCode = map["countryCode"] as String, - countryName = map["countryName"] as String, - current = map["current"] as Boolean, - factors = map["factors"] as List, - secret = map["secret"] as String, - mfaUpdatedAt = map["mfaUpdatedAt"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, + provider = (if (map.containsKey("provider")) map["provider"] else "") as String, + providerUid = (if (map.containsKey("providerUid")) map["providerUid"] else "") as String, + providerAccessToken = (if (map.containsKey("providerAccessToken")) map["providerAccessToken"] else "") as String, + providerAccessTokenExpiry = (if (map.containsKey("providerAccessTokenExpiry")) map["providerAccessTokenExpiry"] else "") as String, + providerRefreshToken = (if (map.containsKey("providerRefreshToken")) map["providerRefreshToken"] else "") as String, + ip = (if (map.containsKey("ip")) map["ip"] else "") as String, + osCode = (if (map.containsKey("osCode")) map["osCode"] else "") as String, + osName = (if (map.containsKey("osName")) map["osName"] else "") as String, + osVersion = (if (map.containsKey("osVersion")) map["osVersion"] else "") as String, + clientType = (if (map.containsKey("clientType")) map["clientType"] else "") as String, + clientCode = (if (map.containsKey("clientCode")) map["clientCode"] else "") as String, + clientName = (if (map.containsKey("clientName")) map["clientName"] else "") as String, + clientVersion = (if (map.containsKey("clientVersion")) map["clientVersion"] else "") as String, + clientEngine = (if (map.containsKey("clientEngine")) map["clientEngine"] else "") as String, + clientEngineVersion = (if (map.containsKey("clientEngineVersion")) map["clientEngineVersion"] else "") as String, + deviceName = (if (map.containsKey("deviceName")) map["deviceName"] else "") as String, + deviceBrand = (if (map.containsKey("deviceBrand")) map["deviceBrand"] else "") as String, + deviceModel = (if (map.containsKey("deviceModel")) map["deviceModel"] else "") as String, + countryCode = (if (map.containsKey("countryCode")) map["countryCode"] else "") as String, + countryName = (if (map.containsKey("countryName")) map["countryName"] else "") as String, + current = (if (map.containsKey("current")) map["current"] else false) as Boolean, + factors = (if (map.containsKey("factors")) map["factors"] else listOf()) as List, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + mfaUpdatedAt = (if (map.containsKey("mfaUpdatedAt")) map["mfaUpdatedAt"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/SessionList.kt b/library/src/main/java/io/appwrite/models/SessionList.kt index bda39fca..9be215ba 100644 --- a/library/src/main/java/io/appwrite/models/SessionList.kt +++ b/library/src/main/java/io/appwrite/models/SessionList.kt @@ -30,8 +30,8 @@ data class SessionList( fun from( map: Map, ) = SessionList( - total = (map["total"] as Number).toLong(), - sessions = (map["sessions"] as List>).map { Session.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + sessions = ((if (map.containsKey("sessions")) map["sessions"] else listOf()) as List>).map { Session.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Subscriber.kt b/library/src/main/java/io/appwrite/models/Subscriber.kt index 931b65db..adcd8c53 100644 --- a/library/src/main/java/io/appwrite/models/Subscriber.kt +++ b/library/src/main/java/io/appwrite/models/Subscriber.kt @@ -79,15 +79,15 @@ data class Subscriber( fun from( map: Map, ) = Subscriber( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - targetId = map["targetId"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + targetId = (if (map.containsKey("targetId")) map["targetId"] else "") as String, target = Target.from(map = map["target"] as Map), - userId = map["userId"] as String, - userName = map["userName"] as String, - topicId = map["topicId"] as String, - providerType = map["providerType"] as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + userName = (if (map.containsKey("userName")) map["userName"] else "") as String, + topicId = (if (map.containsKey("topicId")) map["topicId"] else "") as String, + providerType = (if (map.containsKey("providerType")) map["providerType"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Target.kt b/library/src/main/java/io/appwrite/models/Target.kt index 3e89e08d..d67a716c 100644 --- a/library/src/main/java/io/appwrite/models/Target.kt +++ b/library/src/main/java/io/appwrite/models/Target.kt @@ -79,15 +79,15 @@ data class Target( fun from( map: Map, ) = Target( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - name = map["name"] as String, - userId = map["userId"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, providerId = map["providerId"] as? String, - providerType = map["providerType"] as String, - identifier = map["identifier"] as String, - expired = map["expired"] as Boolean, + providerType = (if (map.containsKey("providerType")) map["providerType"] else "") as String, + identifier = (if (map.containsKey("identifier")) map["identifier"] else "") as String, + expired = (if (map.containsKey("expired")) map["expired"] else false) as Boolean, ) } } diff --git a/library/src/main/java/io/appwrite/models/Team.kt b/library/src/main/java/io/appwrite/models/Team.kt index 6cd7dbc9..dc780c8b 100644 --- a/library/src/main/java/io/appwrite/models/Team.kt +++ b/library/src/main/java/io/appwrite/models/Team.kt @@ -75,11 +75,11 @@ data class Team( map: Map, nestedType: Class ) = Team( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - name = map["name"] as String, - total = (map["total"] as Number).toLong(), + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), prefs = Preferences.from(map = map["prefs"] as Map, nestedType), ) } diff --git a/library/src/main/java/io/appwrite/models/TeamList.kt b/library/src/main/java/io/appwrite/models/TeamList.kt index 8e490f51..db4e1eb2 100644 --- a/library/src/main/java/io/appwrite/models/TeamList.kt +++ b/library/src/main/java/io/appwrite/models/TeamList.kt @@ -39,8 +39,8 @@ data class TeamList( map: Map, nestedType: Class ) = TeamList( - total = (map["total"] as Number).toLong(), - teams = (map["teams"] as List>).map { Team.from(map = it, nestedType) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + teams = ((if (map.containsKey("teams")) map["teams"] else listOf()) as List>).map { Team.from(map = it, nestedType) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Token.kt b/library/src/main/java/io/appwrite/models/Token.kt index 625e3085..c3762397 100644 --- a/library/src/main/java/io/appwrite/models/Token.kt +++ b/library/src/main/java/io/appwrite/models/Token.kt @@ -58,12 +58,12 @@ data class Token( fun from( map: Map, ) = Token( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - userId = map["userId"] as String, - secret = map["secret"] as String, - expire = map["expire"] as String, - phrase = map["phrase"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, + phrase = (if (map.containsKey("phrase")) map["phrase"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/TokenSecret.kt b/library/src/main/java/io/appwrite/models/TokenSecret.kt new file mode 100644 index 00000000..fe2dd92c --- /dev/null +++ b/library/src/main/java/io/appwrite/models/TokenSecret.kt @@ -0,0 +1,69 @@ +package io.appwrite.models + +import com.google.gson.annotations.SerializedName +import io.appwrite.extensions.jsonCast + +/** + * Token Secret + */ +data class TokenSecret( + /** + * Token ID. + */ + @SerializedName("\$id") + val id: String, + + /** + * Token creation date in ISO 8601 format. + */ + @SerializedName("\$createdAt") + val createdAt: String, + + /** + * User ID. + */ + @SerializedName("userId") + val userId: String, + + /** + * Token secret key. Exchange it for a session with the create session endpoint. + */ + @SerializedName("secret") + val secret: String, + + /** + * Token expiration date in ISO 8601 format. + */ + @SerializedName("expire") + val expire: String, + + /** + * Security phrase of a token. Empty if security phrase was not requested when creating a token. It includes randomly generated phrase which is also sent in the external resource such as email. + */ + @SerializedName("phrase") + val phrase: String, + +) { + fun toMap(): Map = mapOf( + "\$id" to id as Any, + "\$createdAt" to createdAt as Any, + "userId" to userId as Any, + "secret" to secret as Any, + "expire" to expire as Any, + "phrase" to phrase as Any, + ) + + companion object { + @Suppress("UNCHECKED_CAST") + fun from( + map: Map, + ) = TokenSecret( + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, + phrase = (if (map.containsKey("phrase")) map["phrase"] else "") as String, + ) + } +} diff --git a/library/src/main/java/io/appwrite/models/Transaction.kt b/library/src/main/java/io/appwrite/models/Transaction.kt index 8f09860e..bb5f39b3 100644 --- a/library/src/main/java/io/appwrite/models/Transaction.kt +++ b/library/src/main/java/io/appwrite/models/Transaction.kt @@ -58,12 +58,12 @@ data class Transaction( fun from( map: Map, ) = Transaction( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - status = map["status"] as String, - operations = (map["operations"] as Number).toLong(), - expiresAt = map["expiresAt"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + status = (if (map.containsKey("status")) map["status"] else "pending") as String, + operations = ((if (map.containsKey("operations")) map["operations"] else 0) as Number).toLong(), + expiresAt = (if (map.containsKey("expiresAt")) map["expiresAt"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/TransactionList.kt b/library/src/main/java/io/appwrite/models/TransactionList.kt index c55a7317..963f3732 100644 --- a/library/src/main/java/io/appwrite/models/TransactionList.kt +++ b/library/src/main/java/io/appwrite/models/TransactionList.kt @@ -30,8 +30,8 @@ data class TransactionList( fun from( map: Map, ) = TransactionList( - total = (map["total"] as Number).toLong(), - transactions = (map["transactions"] as List>).map { Transaction.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + transactions = ((if (map.containsKey("transactions")) map["transactions"] else listOf()) as List>).map { Transaction.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/User.kt b/library/src/main/java/io/appwrite/models/User.kt index 0eea3b00..22b4aeb5 100644 --- a/library/src/main/java/io/appwrite/models/User.kt +++ b/library/src/main/java/io/appwrite/models/User.kt @@ -264,31 +264,31 @@ data class User( map: Map, nestedType: Class ) = User( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - name = map["name"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, password = map["password"] as? String, hash = map["hash"] as? String, hashOptions = map["hashOptions"] as? Map, - registration = map["registration"] as String, - status = map["status"] as Boolean, - labels = map["labels"] as List, - passwordUpdate = map["passwordUpdate"] as String, - email = map["email"] as String, - phone = map["phone"] as String, - emailVerification = map["emailVerification"] as Boolean, + registration = (if (map.containsKey("registration")) map["registration"] else "") as String, + status = (if (map.containsKey("status")) map["status"] else true) as Boolean, + labels = (if (map.containsKey("labels")) map["labels"] else listOf()) as List, + passwordUpdate = (if (map.containsKey("passwordUpdate")) map["passwordUpdate"] else "") as String, + email = (if (map.containsKey("email")) map["email"] else "") as String, + phone = (if (map.containsKey("phone")) map["phone"] else "") as String, + emailVerification = (if (map.containsKey("emailVerification")) map["emailVerification"] else false) as Boolean, emailCanonical = map["emailCanonical"] as? String, emailIsFree = map["emailIsFree"] as? Boolean, emailIsDisposable = map["emailIsDisposable"] as? Boolean, emailIsCorporate = map["emailIsCorporate"] as? Boolean, emailIsCanonical = map["emailIsCanonical"] as? Boolean, passwordPwned = map["passwordPwned"] as? Boolean, - phoneVerification = map["phoneVerification"] as Boolean, - mfa = map["mfa"] as Boolean, + phoneVerification = (if (map.containsKey("phoneVerification")) map["phoneVerification"] else false) as Boolean, + mfa = (if (map.containsKey("mfa")) map["mfa"] else false) as Boolean, prefs = Preferences.from(map = map["prefs"] as Map, nestedType), - targets = (map["targets"] as List>).map { Target.from(map = it) }, - accessedAt = map["accessedAt"] as String, + targets = ((if (map.containsKey("targets")) map["targets"] else listOf()) as List>).map { Target.from(map = it) }, + accessedAt = (if (map.containsKey("accessedAt")) map["accessedAt"] else "") as String, impersonator = map["impersonator"] as? Boolean, impersonatorUserId = map["impersonatorUserId"] as? String, ) diff --git a/library/src/main/java/io/appwrite/services/Account.kt b/library/src/main/java/io/appwrite/services/Account.kt index 8df38221..e6201c46 100644 --- a/library/src/main/java/io/appwrite/services/Account.kt +++ b/library/src/main/java/io/appwrite/services/Account.kt @@ -1200,6 +1200,219 @@ class Account(client: Client) : Service(client) { nestedType = classOf(), ) + /** + * Get the list of verified passkeys registered by the currently logged in user. + * + * @param queries Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter and order on the following attributes: $id, $createdAt, $updatedAt, name, accessedAt + * @param total When set to false, the total count returned will be 0 and will not be calculated. + * @return [io.appwrite.models.PasskeyList] + */ + @JvmOverloads + suspend fun listPasskeys( + queries: List? = null, + total: Boolean? = null, + ): io.appwrite.models.PasskeyList { + val apiPath = "/account/passkeys" + val apiParams = mutableMapOf( + "queries" to queries, + "total" to total, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.PasskeyList = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.PasskeyList.from(map = it as Map) + } + return client.call( + "GET", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.PasskeyList::class.java, + converter, + ) + } + + /** + * Start registering a passkey for the currently logged in user. The session must have signed in or completed an MFA challenge within the last 10 minutes. Pass the returned `publicKey` options to `navigator.credentials.create()`, then complete the registration with [Update passkey verification](/docs/references/cloud/client-web/account#updatePasskeyVerification). The passkey stays pending until verified, and the challenge expires after 5 minutes. + * + * @param passkeyId Passkey ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars. + * @param name Passkey name, shown when listing passkeys. Max length: 128 chars. + * @return [io.appwrite.models.PasskeyChallenge] + */ + @JvmOverloads + suspend fun createPasskey( + passkeyId: String? = null, + name: String? = null, + ): io.appwrite.models.PasskeyChallenge { + val apiPath = "/account/passkeys" + val apiParams = mutableMapOf( + "passkeyId" to passkeyId, + "name" to name, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.PasskeyChallenge = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.PasskeyChallenge.from(map = it as Map) + } + return client.call( + "POST", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.PasskeyChallenge::class.java, + converter, + ) + } + + /** + * Get a verified passkey of the currently logged in user by its unique ID. + * + * @param passkeyId Passkey ID. + * @return [io.appwrite.models.Passkey] + */ + suspend fun getPasskey( + passkeyId: String, + ): io.appwrite.models.Passkey { + if (passkeyId.isEmpty()) { + throw AppwriteException("Missing required parameter: \"passkeyId\"") + } + val apiPath = ("/account/passkeys/{passkeyId}" + .replace("{passkeyId}", passkeyId) + ) + val apiParams = mutableMapOf() + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.Passkey = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.Passkey.from(map = it as Map) + } + return client.call( + "GET", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.Passkey::class.java, + converter, + ) + } + + /** + * Rename a passkey of the currently logged in user. + * + * @param passkeyId Passkey ID. + * @param name Passkey name. Max length: 128 chars. + * @return [io.appwrite.models.Passkey] + */ + suspend fun updatePasskey( + passkeyId: String, + name: String, + ): io.appwrite.models.Passkey { + if (passkeyId.isEmpty()) { + throw AppwriteException("Missing required parameter: \"passkeyId\"") + } + val apiPath = ("/account/passkeys/{passkeyId}" + .replace("{passkeyId}", passkeyId) + ) + val apiParams = mutableMapOf( + "name" to name, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.Passkey = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.Passkey.from(map = it as Map) + } + return client.call( + "PATCH", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.Passkey::class.java, + converter, + ) + } + + /** + * Delete a passkey from the currently logged in user. The passkey can no longer be used to sign in, although it may remain stored on the user's device. The session must have signed in or completed an MFA challenge within the last 10 minutes. + * + * @param passkeyId Passkey ID. + * @return [Any] + */ + suspend fun deletePasskey( + passkeyId: String, + ): Any { + if (passkeyId.isEmpty()) { + throw AppwriteException("Missing required parameter: \"passkeyId\"") + } + val apiPath = ("/account/passkeys/{passkeyId}" + .replace("{passkeyId}", passkeyId) + ) + val apiParams = mutableMapOf() + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + return client.call( + "DELETE", + apiPath, + apiHeaders, + apiParams, + responseType = Any::class.java, + ) + } + + /** + * Complete a passkey registration started with [Create passkey](/docs/references/cloud/client-web/account#createPasskey). Pass the JSON form of the credential returned by `navigator.credentials.create()`, for example `credential.toJSON()`. Each registration can be verified once; a failed attempt requires starting it again. + * + * @param passkeyId Passkey ID. + * @param credential Registration credential returned by the authenticator, in the JSON form produced by `PublicKeyCredential.toJSON()`. + * @return [io.appwrite.models.Passkey] + */ + suspend fun updatePasskeyVerification( + passkeyId: String, + credential: Map, + ): io.appwrite.models.Passkey { + if (passkeyId.isEmpty()) { + throw AppwriteException("Missing required parameter: \"passkeyId\"") + } + val apiPath = ("/account/passkeys/{passkeyId}/verification" + .replace("{passkeyId}", passkeyId) + ) + val apiParams = mutableMapOf( + "credential" to credential, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.Passkey = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.Passkey.from(map = it as Map) + } + return client.call( + "PUT", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.Passkey::class.java, + converter, + ) + } + /** * Update currently logged in user password. For validation, user is required to pass in the new password, and the old password. For users created with OAuth, Team Invites and Magic URL, oldPassword is optional. * @@ -1784,6 +1997,7 @@ class Account(client: Client) : Service(client) { * @param success URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param failure URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param scopes A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of 100 scopes are allowed, each 4096 characters long. + * @param state An opaque value your app generates and keeps, for example in a cookie. It is returned unchanged as the `state` query parameter on the success and failure URLs, so your app can check that the sign-in it receives is one it started. Printable ASCII only (RFC 6749 Appendix A.5). Max length: 256 chars. */ @JvmOverloads suspend fun createOAuth2Session( @@ -1792,6 +2006,7 @@ class Account(client: Client) : Service(client) { success: String? = null, failure: String? = null, scopes: List? = null, + state: String? = null, ) { val apiPath = ("/account/sessions/oauth2/{provider}" .replace("{provider}", provider.value) @@ -1800,6 +2015,7 @@ class Account(client: Client) : Service(client) { "success" to success, "failure" to failure, "scopes" to scopes, + "state" to state, "project" to client.config["project"], ) val apiQuery = mutableListOf() @@ -1810,11 +2026,11 @@ class Account(client: Client) : Service(client) { } is List<*> -> { (it.value as List<*>).forEach { v -> - apiQuery.add("${it.key}[]=${v.toString()}") + apiQuery.add("${Uri.encode(it.key)}[]=${Uri.encode(v.toString())}") } } else -> { - apiQuery.add("${it.key}=${it.value.toString()}") + apiQuery.add("${Uri.encode(it.key)}=${Uri.encode(it.value.toString())}") } } } @@ -2268,6 +2484,7 @@ class Account(client: Client) : Service(client) { * @param success URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param failure URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param scopes A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of 100 scopes are allowed, each 4096 characters long. + * @param state An opaque value your app generates and keeps, for example in a cookie. It is returned unchanged as the `state` query parameter on the success and failure URLs, so your app can check that the sign-in it receives is one it started. Printable ASCII only (RFC 6749 Appendix A.5). Max length: 256 chars. */ @JvmOverloads suspend fun createOAuth2Token( @@ -2276,6 +2493,7 @@ class Account(client: Client) : Service(client) { success: String? = null, failure: String? = null, scopes: List? = null, + state: String? = null, ) { val apiPath = ("/account/tokens/oauth2/{provider}" .replace("{provider}", provider.value) @@ -2284,6 +2502,7 @@ class Account(client: Client) : Service(client) { "success" to success, "failure" to failure, "scopes" to scopes, + "state" to state, "project" to client.config["project"], ) val apiQuery = mutableListOf() @@ -2294,11 +2513,11 @@ class Account(client: Client) : Service(client) { } is List<*> -> { (it.value as List<*>).forEach { v -> - apiQuery.add("${it.key}[]=${v.toString()}") + apiQuery.add("${Uri.encode(it.key)}[]=${Uri.encode(v.toString())}") } } else -> { - apiQuery.add("${it.key}=${it.value.toString()}") + apiQuery.add("${Uri.encode(it.key)}=${Uri.encode(it.value.toString())}") } } } @@ -2332,6 +2551,68 @@ class Account(client: Client) : Service(client) { } } + /** + * Start signing in with a passkey. No email or user ID is needed: pass the returned `publicKey` options to `navigator.credentials.get()` and the user picks one of their passkeys. Then call [Update passkey token](/docs/references/cloud/client-web/account#updatePasskeyToken) with the credential to receive a token, and exchange it with [Create session](/docs/references/cloud/client-web/account#createSession). The challenge expires after 5 minutes. + * + * @return [io.appwrite.models.PasskeyChallenge] + */ + suspend fun createPasskeyToken(): io.appwrite.models.PasskeyChallenge { + val apiPath = "/account/tokens/passkey" + val apiParams = mutableMapOf() + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.PasskeyChallenge = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.PasskeyChallenge.from(map = it as Map) + } + return client.call( + "POST", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.PasskeyChallenge::class.java, + converter, + ) + } + + /** + * Complete a passkey sign-in started with [Create passkey token](/docs/references/cloud/client-web/account#createPasskeyToken). Pass the challenge ID and the JSON form of the credential returned by `navigator.credentials.get()`, for example `credential.toJSON()`. The returned token includes its secret and expires after 1 minute: exchange it for a session with [Create session](/docs/references/cloud/client-web/account#createSession). Passkeys require user verification, so the session also satisfies MFA. Each challenge can only be used once. + * + * @param challengeId Challenge ID returned by createPasskeyToken. + * @param credential Authentication credential returned by the authenticator, in the JSON form produced by `PublicKeyCredential.toJSON()`. + * @return [io.appwrite.models.TokenSecret] + */ + suspend fun updatePasskeyToken( + challengeId: String, + credential: Map, + ): io.appwrite.models.TokenSecret { + val apiPath = "/account/tokens/passkey" + val apiParams = mutableMapOf( + "challengeId" to challengeId, + "credential" to credential, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.TokenSecret = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.TokenSecret.from(map = it as Map) + } + return client.call( + "PUT", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.TokenSecret::class.java, + converter, + ) + } + /** * Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST /v1/account/sessions/token](https://appwrite.io/docs/references/cloud/client-web/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes. * diff --git a/library/src/main/java/io/appwrite/services/Analytics.kt b/library/src/main/java/io/appwrite/services/Analytics.kt new file mode 100644 index 00000000..1949f76b --- /dev/null +++ b/library/src/main/java/io/appwrite/services/Analytics.kt @@ -0,0 +1,83 @@ +package io.appwrite.services + +import android.net.Uri +import io.appwrite.Client +import io.appwrite.Service +import io.appwrite.exceptions.AppwriteException +import io.appwrite.extensions.classOf +import io.appwrite.models.* +import okhttp3.Cookie +import java.io.File + +/** + * + */ +class Analytics(client: Client) : Service(client) { + /** + * Send a tracking event from a browser, native app, or server-side SDK. + * + * @param propertyId Analytics property ID. + * @param name Event name. "pageview" is just a conventional event name; events are not modeled specially. + * @param url Full page URL or screen identifier. + * @param domain Hostname (e.g. example.com). + * @param referrer Referrer URL. + * @param screenWidth Viewport width in CSS pixels. + * @param sessionHash Optional session hash (provided by SDK). + * @param scrollDepth Scroll depth percentage 0-100. + * @param engagementTime Engagement time in seconds, 0-4294967295. + * @param props Custom string properties as a flat key=value list (max 32 entries, alternating key,value). + * @param userId Override user ID. Requires API-key auth with analytics.write scope. + * @param ip Override IP address. Requires API-key auth with analytics.write scope. + * @param userAgent Override user agent. Requires API-key auth with analytics.write scope. + * @return [Any] + */ + @JvmOverloads + suspend fun createEvent( + propertyId: String, + name: String, + url: String, + domain: String? = null, + referrer: String? = null, + screenWidth: Long? = null, + sessionHash: String? = null, + scrollDepth: Long? = null, + engagementTime: Long? = null, + props: List? = null, + userId: String? = null, + ip: String? = null, + userAgent: String? = null, + ): Any { + if (propertyId.isEmpty()) { + throw AppwriteException("Missing required parameter: \"propertyId\"") + } + val apiPath = ("/analytics/properties/{propertyId}/events" + .replace("{propertyId}", propertyId) + ) + val apiParams = mutableMapOf( + "name" to name, + "url" to url, + "domain" to domain, + "referrer" to referrer, + "screenWidth" to screenWidth, + "sessionHash" to sessionHash, + "scrollDepth" to scrollDepth, + "engagementTime" to engagementTime, + "props" to props, + "userId" to userId, + "ip" to ip, + "userAgent" to userAgent, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + return client.call( + "POST", + apiPath, + apiHeaders, + apiParams, + responseType = Any::class.java, + ) + } +} diff --git a/library/src/main/java/io/appwrite/services/Oauth2.kt b/library/src/main/java/io/appwrite/services/Oauth2.kt index 030f4cef..f5cbc3cb 100644 --- a/library/src/main/java/io/appwrite/services/Oauth2.kt +++ b/library/src/main/java/io/appwrite/services/Oauth2.kt @@ -85,7 +85,7 @@ class Oauth2(client: Client) : Service(client) { prompt: String? = null, maxAge: Long? = null, authorizationDetails: String? = null, - resource: String? = null, + resource: List? = null, audience: String? = null, requestUri: String? = null, ): io.appwrite.models.Oauth2Authorize { @@ -157,7 +157,7 @@ class Oauth2(client: Client) : Service(client) { prompt: String? = null, maxAge: Long? = null, authorizationDetails: String? = null, - resource: String? = null, + resource: List? = null, audience: String? = null, requestUri: String? = null, ): io.appwrite.models.Oauth2Authorize { @@ -213,7 +213,7 @@ class Oauth2(client: Client) : Service(client) { clientId: String? = null, scope: String? = null, authorizationDetails: String? = null, - resource: String? = null, + resource: List? = null, audience: String? = null, ): io.appwrite.models.Oauth2DeviceAuthorization { val apiPath = ("/oauth2/{project_id}/device_authorization" @@ -468,7 +468,7 @@ class Oauth2(client: Client) : Service(client) { prompt: String? = null, maxAge: Long? = null, authorizationDetails: String? = null, - resource: String? = null, + resource: List? = null, audience: String? = null, ): io.appwrite.models.Oauth2PAR { val apiPath = ("/oauth2/{project_id}/par" @@ -642,7 +642,7 @@ class Oauth2(client: Client) : Service(client) { clientSecret: String? = null, codeVerifier: String? = null, redirectUri: String? = null, - resource: String? = null, + resource: List? = null, audience: String? = null, ): io.appwrite.models.Oauth2Token { val apiPath = ("/oauth2/{project_id}/token" diff --git a/library/src/main/java/io/appwrite/services/Push.kt b/library/src/main/java/io/appwrite/services/Push.kt index 48488c06..21409d73 100644 --- a/library/src/main/java/io/appwrite/services/Push.kt +++ b/library/src/main/java/io/appwrite/services/Push.kt @@ -14,7 +14,6 @@ import androidx.core.content.ContextCompat import com.hivemq.client.mqtt.datatypes.MqttQos import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient import io.appwrite.Client -import io.appwrite.ResolvedTopic import io.appwrite.Service import io.appwrite.Topic import io.appwrite.exceptions.AppwriteException @@ -338,7 +337,7 @@ class Push( /** * Subscribe to a single topic built with [io.appwrite.Topic], e.g. - * `push.subscribe(Topic.path(listOf("user", userId)).any()) { message -> ... }`. + * `push.subscribe(Topic.path(listOf("user", userId))) { message -> ... }`. * Returns a [PushSubscription] handle; see the `List` overload for the options. */ @JvmOverloads @@ -352,28 +351,12 @@ class Push( ): PushSubscription = subscribe(listOf(topic.toString()), background, title, retry, notifyInForeground, callback) - /** - * Subscribe to a single topic ended with `all()`, e.g. - * `push.subscribe(Topic.path(listOf("user", userId)).all()) { message -> ... }`. - * Returns a [PushSubscription] handle; see the `List` overload for the options. - */ - @JvmOverloads - fun subscribe( - topic: ResolvedTopic, - background: Boolean = false, - title: String? = null, - retry: Boolean = true, - notifyInForeground: Boolean = false, - callback: (PushMessage) -> Unit, - ): PushSubscription = - subscribe(listOf(topic.toString()), background, title, retry, notifyInForeground, callback) - /** * Subscribe to a list mixing raw strings and topics built with [io.appwrite.Topic], e.g. - * `push.subscribe(listOf("news", Topic.path(listOf("chat")).any())) { message -> ... }`. - * Each element must be a `String`, `Topic` or `ResolvedTopic` (Kotlin has no common type for - * them, so this is checked at runtime). Returns a [PushSubscription] handle; see the - * `List` overload for the options. + * `push.subscribe(listOf("news", Topic.path(listOf("chat", chatId)))) { message -> ... }`. + * Each element must be a `String` or `Topic` (Kotlin has no common type for them, so this is + * checked at runtime). Returns a [PushSubscription] handle; see the `List` overload + * for the options. */ @JvmOverloads @JvmName("subscribeTopics") @@ -386,8 +369,8 @@ class Push( callback: (PushMessage) -> Unit, ): PushSubscription { val filters = topics.mapIndexed { index, topic -> - require(topic is String || topic is Topic || topic is ResolvedTopic) { - "topics[$index] must be a String, Topic or ResolvedTopic, got ${topic::class.simpleName}" + require(topic is String || topic is Topic) { + "topics[$index] must be a String or Topic, got ${topic::class.simpleName}" } topic.toString() } @@ -397,8 +380,8 @@ class Push( /** * Subscribe to one or more topics. Returns a [PushSubscription] handle. * - * Topics are MQTT filters: pass raw strings, or build them with [io.appwrite.Topic] - * through the `Topic` overloads, e.g. `push.subscribe(Topic.path(listOf("user")).any()) { ... }`. + * Pass raw strings, or build them with [io.appwrite.Topic] + * through the `Topic` overloads, e.g. `push.subscribe(Topic.path(listOf("user", userId))) { ... }`. * * For a foreground subscription this blocks until the broker has acknowledged every * SUBSCRIBE (SUBACK): the broker only starts routing a topic once its subscription is @@ -828,10 +811,14 @@ class Push( const val EXTRA_PAYLOAD = PushBackground.EXTRA_PAYLOAD /** - * What background delivery can rely on: exact alarms, battery-optimisation exemption and - * foreground mode. When [PushBackgroundStatus.bestEffort] is true, wake-ups are inexact and - * messages can arrive late while the app is closed; explain why before asking with - * [requestExactAlarms] or [requestIgnoreBatteryOptimizations]. + * What background delivery can rely on: exact alarms, battery-optimisation exemption, + * foreground mode and restricted background usage. When [PushBackgroundStatus.bestEffort] + * is true, messages can arrive late while the app is closed; explain why before asking with + * [requestExactAlarms] or [requestIgnoreBatteryOptimizations]. When + * [PushBackgroundStatus.backgroundRestricted] is true, neither exact alarms nor foreground + * mode lift it: ask the user to choose Unrestricted in the app's battery settings + * (Settings > Apps > your app > Battery), or for the battery-optimisation exemption, which + * also lets the SDK's alarms and jobs run. */ @JvmStatic fun backgroundStatus(context: Context): PushBackgroundStatus = PushBackground.backgroundStatus(context) diff --git a/library/src/main/java/io/appwrite/services/PushBackground.kt b/library/src/main/java/io/appwrite/services/PushBackground.kt index e06dfabe..73103d5d 100644 --- a/library/src/main/java/io/appwrite/services/PushBackground.kt +++ b/library/src/main/java/io/appwrite/services/PushBackground.kt @@ -52,8 +52,15 @@ import java.util.concurrent.atomic.AtomicInteger private const val LOG_TAG = "AppwritePush" -/** What a message's `notification` block asks a background notification to show; [present] is false without one. */ -internal data class PushNotificationContent(val present: Boolean, val title: String?, val body: String?, val image: String?) +/** + * What a message asks a background notification to show. A [raw] payload, one that is not a JSON + * object, is shown as the body under the subscription's title; an envelope is [visible] only with a + * title or body in its `notification`. + */ +internal data class PushNotificationContent(val raw: Boolean, val title: String?, val body: String?, val image: String?) { + val visible: Boolean + get() = raw || title != null || body != null +} /** A background subscription saved across restarts: its filter, QoS choice and notification title. */ internal data class PushEntry( @@ -887,7 +894,7 @@ internal object PushBackground { // Deliver a message to the matching live callbacks; with none, to the declared PushReceiver // and/or a notification per saved subscription that matches it. [deferAck] hands a listener // that acknowledges messages itself its acknowledgement. - private fun deliverMessage(context: Context, message: PushMessage, deferAck: () -> () -> Unit) { + internal fun deliverMessage(context: Context, message: PushMessage, deferAck: () -> () -> Unit) { val matching = listeners.filter { listener -> listener.topics.any { matchesTopic(it, message.topic) } } // Saved background subscriptions no live one has taken over; they deliver even when a live // foreground-only subscription on an overlapping filter received the message too. @@ -952,16 +959,21 @@ internal object PushBackground { /** * Post a notification for [message] that opens the app, with the message in its extras. It shows - * the server's title, body and image from [content], falling back to [title] and the raw payload. + * the server's title, body and image from [content], falling back to [title], and to the payload + * as the body when it is not a JSON object. An envelope with neither a title nor a body, such as + * a data-only push, posts nothing. */ fun notify(context: Context, message: PushMessage, title: String, content: PushNotificationContent = notificationContent(message)) { + if (!content.visible) { + return + } val manager = NotificationManagerCompat.from(context) if (!manager.areNotificationsEnabled()) { return } createChannels(context) val id = 31 * message.hashCode() + title.hashCode() - val body = content.body ?: message.data.takeIf { !content.present } + val body = content.body ?: message.data.takeIf { content.raw } val builder = NotificationCompat.Builder(context, CHANNEL_ID) .setContentTitle(content.title ?: title) .setSmallIcon(notificationIcon(context)) @@ -992,12 +1004,13 @@ internal object PushBackground { .also { ActivityManager.getMyMemoryState(it) } .importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND - /** The server's `notification` block in [message]: nulls when the payload has none or is not JSON. */ + /** The server's `notification` block in [message], [PushNotificationContent.raw] when the payload is not a JSON object. */ fun notificationContent(message: PushMessage): PushNotificationContent { - val notification = runCatching { JSONObject(message.data) }.getOrNull()?.optJSONObject("notification") - ?: return PushNotificationContent(false, null, null, null) - fun field(name: String) = (notification.opt(name) as? String)?.takeIf { it.isNotEmpty() } - return PushNotificationContent(true, field("title"), field("body"), field("image")) + val envelope = runCatching { JSONObject(message.data) }.getOrNull() + ?: return PushNotificationContent(true, null, null, null) + val notification = envelope.optJSONObject("notification") + fun field(name: String) = (notification?.opt(name) as? String)?.takeIf { it.isNotEmpty() } + return PushNotificationContent(false, field("title"), field("body"), field("image")) } // Downloads notification images, so a slow one is abandoned without holding up delivery. @@ -1201,6 +1214,8 @@ internal object PushBackground { app.getSystemService(AlarmManager::class.java)?.canScheduleExactAlarms() == true, ignoringBatteryOptimizations = app.getSystemService(PowerManager::class.java)?.isIgnoringBatteryOptimizations(app.packageName) == true, foregroundService = PushStore.foreground(app), + backgroundRestricted = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && + app.getSystemService(ActivityManager::class.java)?.isBackgroundRestricted == true, ) } diff --git a/library/src/main/java/io/appwrite/services/PushCore.kt b/library/src/main/java/io/appwrite/services/PushCore.kt index 15f86b6a..bc151295 100644 --- a/library/src/main/java/io/appwrite/services/PushCore.kt +++ b/library/src/main/java/io/appwrite/services/PushCore.kt @@ -54,19 +54,36 @@ data class PushMessage( /** * What background delivery can rely on. Without exact alarms, the scheduled wake-ups are inexact * and Doze can defer them, so delivery is [bestEffort] unless foreground mode keeps the - * connection open. + * connection open. When the user restricted background usage, delivery is [bestEffort] unless the + * app is also exempt from battery optimisation, and foreground mode no longer counts: Android stops + * a restricted app's foreground service once it leaves the screen, exemption or not. */ -data class PushBackgroundStatus( +data class PushBackgroundStatus @JvmOverloads constructor( /** The app may schedule exact alarms (`SCHEDULE_EXACT_ALARM`, granted). */ val exactAlarms: Boolean, /** The app is exempt from battery optimisation. */ val ignoringBatteryOptimizations: Boolean, /** Foreground mode (`setForeground(true)`) keeps the connection open in a service. */ val foregroundService: Boolean, + /** + * The user restricted the app's background usage in its battery settings (Android 9+). Android + * may then hold its alarms, jobs and foreground service while it is closed, so messages can + * wait until the app is opened. The battery-optimisation exemption lets the alarms and jobs + * run again, but foreground mode still stops once the app leaves the screen. + */ + val backgroundRestricted: Boolean = false, ) { - /** Wake-ups may be deferred by Doze, so messages can arrive late while the app is closed. */ + /** + * Messages can arrive late, or not until the app is opened, while the app is closed: background + * usage is restricted without the battery-optimisation exemption, or wake-ups may be deferred + * by Doze. + */ val bestEffort: Boolean - get() = !exactAlarms && !foregroundService + get() { + val held = backgroundRestricted && !ignoringBatteryOptimizations + val foreground = foregroundService && !backgroundRestricted + return held || (!exactAlarms && !foreground) + } } /** Connection parameters shared between the in-process client and the foreground service. */ diff --git a/library/src/main/java/io/appwrite/services/Teams.kt b/library/src/main/java/io/appwrite/services/Teams.kt index 7227eba6..8a4bb87d 100644 --- a/library/src/main/java/io/appwrite/services/Teams.kt +++ b/library/src/main/java/io/appwrite/services/Teams.kt @@ -539,6 +539,7 @@ class Teams(client: Client) : Service(client) { * @param phone Phone number. Format this number with a leading '+' and a country code, e.g., +16175551212. * @param url URL to redirect the user back to your app from the invitation email. This parameter is not required when an API key is supplied. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param name Name of the new team member. Max length: 128 chars. + * @param membershipId Membership ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars. * @return [io.appwrite.models.Membership] */ @JvmOverloads @@ -550,6 +551,7 @@ class Teams(client: Client) : Service(client) { phone: String? = null, url: String? = null, name: String? = null, + membershipId: String? = null, ): io.appwrite.models.Membership { if (teamId.isEmpty()) { throw AppwriteException("Missing required parameter: \"teamId\"") @@ -564,6 +566,7 @@ class Teams(client: Client) : Service(client) { "roles" to roles, "url" to url, "name" to name, + "membershipId" to membershipId, ) val apiHeaders = mutableMapOf( "X-Appwrite-Project" to client.config["project"].orEmpty(), diff --git a/library/src/test/java/io/appwrite/QueryTest.kt b/library/src/test/java/io/appwrite/QueryTest.kt new file mode 100644 index 00000000..7a6525d7 --- /dev/null +++ b/library/src/test/java/io/appwrite/QueryTest.kt @@ -0,0 +1,166 @@ +package io.appwrite + +import com.google.gson.JsonParser +import org.junit.Assert.assertEquals +import org.junit.Test + +class QueryTest { + private fun method(json: String): String = + JsonParser.parseString(json).asJsonObject.get("method").asString + + @Test + fun countDistinct() { + assertEquals("countDistinct", method(Query.countDistinct("year", "uniqueYears"))) + } + + @Test + fun sum() { + assertEquals("sum", method(Query.sum("price", "total"))) + } + + @Test + fun avg() { + assertEquals("avg", method(Query.avg("price", "avgPrice"))) + } + + @Test + fun min() { + assertEquals("min", method(Query.min("price", "lowest"))) + } + + @Test + fun max() { + assertEquals("max", method(Query.max("price", "highest"))) + } + + @Test + fun stddev() { + assertEquals("stddev", method(Query.stddev("price", "sd"))) + } + + @Test + fun stddevPop() { + assertEquals("stddevPop", method(Query.stddevPop("price", "sdp"))) + } + + @Test + fun stddevSamp() { + assertEquals("stddevSamp", method(Query.stddevSamp("price", "sds"))) + } + + @Test + fun variance() { + assertEquals("variance", method(Query.variance("price", "var"))) + } + + @Test + fun varPop() { + assertEquals("varPop", method(Query.varPop("price", "vp"))) + } + + @Test + fun varSamp() { + assertEquals("varSamp", method(Query.varSamp("price", "vs"))) + } + + @Test + fun bitAnd() { + assertEquals("bitAnd", method(Query.bitAnd("flags", "band"))) + } + + @Test + fun bitOr() { + assertEquals("bitOr", method(Query.bitOr("flags", "bor"))) + } + + @Test + fun bitXor() { + assertEquals("bitXor", method(Query.bitXor("flags", "bxor"))) + } + + @Test + fun having() { + val json = JsonParser.parseString(Query.having(listOf(Query.greaterThan("total", 1)))).asJsonObject + assertEquals("having", json.get("method").asString) + assertEquals("greaterThan", json.get("values").asJsonArray[0].asJsonObject.get("method").asString) + } + + @Test + fun leftJoin() { + assertEquals("leftJoin", method(Query.leftJoin("orders", "\$id", "customerId", "=", "ord"))) + } + + @Test + fun rightJoin() { + assertEquals("rightJoin", method(Query.rightJoin("orders", "\$id", "customerId"))) + } + + @Test + fun fullOuterJoin() { + assertEquals("fullOuterJoin", method(Query.fullOuterJoin("orders", "\$id", "customerId"))) + } + + @Test + fun crossJoin() { + assertEquals("crossJoin", method(Query.crossJoin("orders", "ord"))) + } + + @Test + fun on() { + assertEquals("on", method(Query.on("\$id", "customerId"))) + } + + @Test + fun relationship() { + val query = Query.relationship("comments", listOf(Query.equal("approved", true), Query.limit(2))) + val json = JsonParser.parseString(query).asJsonObject + assertEquals("relationship", json.get("method").asString) + assertEquals("comments", json.get("attribute").asString) + val values = json.get("values").asJsonArray + assertEquals("equal", values[0].asJsonObject.get("method").asString) + assertEquals("limit", values[1].asJsonObject.get("method").asString) + assertEquals(listOf(query), Query.builder().relationship("comments", listOf(Query.equal("approved", true), Query.limit(2))).build()) + } + + @Test + fun nestedLeftJoin() { + val json = JsonParser.parseString( + Query.leftJoin( + "orders", + "ord", + listOf( + Query.on("\$id", "customerId"), + Query.equal("ord.status", "paid"), + ) + ) + ).asJsonObject + assertEquals("leftJoin", json.get("method").asString) + val values = json.get("values").asJsonArray + assertEquals("ord", values[0].asString) + assertEquals("on", values[1].asJsonObject.get("method").asString) + assertEquals("equal", values[2].asJsonObject.get("method").asString) + } + + @Test + fun notCovers() { + assertEquals("notCovers", method(Query.notCovers("location", listOf(1, 2)))) + } + + @Test + fun spatialEquals() { + assertEquals("spatialEquals", method(Query.spatialEquals("location", listOf(1, 2)))) + } + + @Test + fun notSpatialEquals() { + assertEquals("notSpatialEquals", method(Query.notSpatialEquals("location", listOf(1, 2)))) + } + + @Test + fun page() { + val queries = Query.page(2, 10) + assertEquals(2, queries.size) + assertEquals("limit", method(queries[0])) + assertEquals("offset", method(queries[1])) + } +} diff --git a/library/src/test/java/io/appwrite/TopicTest.kt b/library/src/test/java/io/appwrite/TopicTest.kt index 0dd65bf0..94e2cf2a 100644 --- a/library/src/test/java/io/appwrite/TopicTest.kt +++ b/library/src/test/java/io/appwrite/TopicTest.kt @@ -7,7 +7,7 @@ import org.junit.Test class TopicTest { private fun assertPathFails(levels: List) { assertThrows(IllegalArgumentException::class.java) { Topic.path(levels) } - assertThrows(IllegalArgumentException::class.java) { Topic.any().path(levels) } + assertThrows(IllegalArgumentException::class.java) { Topic.path(listOf("user")).path(levels) } } @Test @@ -23,23 +23,6 @@ class TopicTest { ) } - @Test - fun anyAppendsSingleLevelWildcard() { - assertEquals("user/+/notification", Topic.path(listOf("user")).any().path(listOf("notification")).toString()) - assertEquals("chat/+/+/message", Topic.path(listOf("chat")).any().any().path(listOf("message")).toString()) - } - - @Test - fun allEndsTopic() { - assertEquals("org/+/logs/#", Topic.path(listOf("org")).any().path(listOf("logs")).all().toString()) - } - - @Test - fun staticAnyAndAll() { - assertEquals("+/notification", Topic.any().path(listOf("notification")).toString()) - assertEquals("#", Topic.all().toString()) - } - @Test fun pathRejectsInvalidLevels() { assertPathFails(emptyList()) From 445b96e9b3a1378adebfcbb09add74af8ae19c3b Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 15 Oct 2026 12:53:47 +0100 Subject: [PATCH 4/4] feat: SDK update for the 2.5.x majors --- CHANGELOG.md | 52 +- README.md | 67 +- .../java/account/create-o-auth-2-session.md | 1 + .../java/account/create-o-auth-2-token.md | 1 + .../java/account/create-passkey-token.md | 22 + docs/examples/java/account/create-passkey.md | 27 + docs/examples/java/account/delete-passkey.md | 26 + docs/examples/java/account/get-passkey.md | 26 + docs/examples/java/account/list-passkeys.md | 27 + .../java/account/update-passkey-token.md | 27 + .../account/update-passkey-verification.md | 27 + docs/examples/java/account/update-passkey.md | 27 + docs/examples/java/analytics/create-event.md | 38 ++ docs/examples/java/oauth2/authorize-post.md | 2 +- docs/examples/java/oauth2/authorize.md | 2 +- .../oauth2/create-device-authorization.md | 2 +- docs/examples/java/oauth2/create-par.md | 2 +- docs/examples/java/oauth2/create-token.md | 2 +- docs/examples/java/teams/create-membership.md | 1 + .../kotlin/account/create-o-auth-2-session.md | 1 + .../kotlin/account/create-o-auth-2-token.md | 1 + .../kotlin/account/create-passkey-token.md | 13 + .../examples/kotlin/account/create-passkey.md | 16 + .../examples/kotlin/account/delete-passkey.md | 15 + docs/examples/kotlin/account/get-passkey.md | 15 + docs/examples/kotlin/account/list-passkeys.md | 16 + .../kotlin/account/update-passkey-token.md | 16 + .../account/update-passkey-verification.md | 16 + .../examples/kotlin/account/update-passkey.md | 16 + .../examples/kotlin/analytics/create-event.md | 27 + docs/examples/kotlin/oauth2/authorize-post.md | 2 +- docs/examples/kotlin/oauth2/authorize.md | 2 +- .../oauth2/create-device-authorization.md | 2 +- docs/examples/kotlin/oauth2/create-par.md | 2 +- docs/examples/kotlin/oauth2/create-token.md | 2 +- .../kotlin/teams/create-membership.md | 1 + library/src/main/java/io/appwrite/Client.kt | 51 +- library/src/main/java/io/appwrite/Query.kt | 574 ++++++++++++++++++ library/src/main/java/io/appwrite/Topic.kt | 34 +- .../io/appwrite/enums/ExecutionTrigger.kt | 4 +- .../main/java/io/appwrite/models/Account.kt | 30 +- .../java/io/appwrite/models/AlgoArgon2.kt | 8 +- .../java/io/appwrite/models/AlgoBcrypt.kt | 2 +- .../main/java/io/appwrite/models/AlgoMd5.kt | 2 +- .../java/io/appwrite/models/AlgoPhpass.kt | 2 +- .../java/io/appwrite/models/AlgoScrypt.kt | 10 +- .../io/appwrite/models/AlgoScryptModified.kt | 8 +- .../main/java/io/appwrite/models/AlgoSha.kt | 2 +- .../src/main/java/io/appwrite/models/App.kt | 52 +- .../io/appwrite/models/AppInstallation.kt | 18 +- .../io/appwrite/models/AppInstallationList.kt | 4 +- .../main/java/io/appwrite/models/AppKey.kt | 16 +- .../java/io/appwrite/models/AppKeyList.kt | 4 +- .../main/java/io/appwrite/models/AppScope.kt | 10 +- .../java/io/appwrite/models/AppScopeList.kt | 4 +- .../main/java/io/appwrite/models/AppSecret.kt | 16 +- .../java/io/appwrite/models/AppSecretList.kt | 4 +- .../io/appwrite/models/AppSecretPlaintext.kt | 16 +- .../main/java/io/appwrite/models/AppsList.kt | 4 +- .../main/java/io/appwrite/models/Continent.kt | 4 +- .../java/io/appwrite/models/ContinentList.kt | 4 +- .../main/java/io/appwrite/models/Country.kt | 4 +- .../java/io/appwrite/models/CountryList.kt | 4 +- .../main/java/io/appwrite/models/Currency.kt | 14 +- .../java/io/appwrite/models/CurrencyList.kt | 4 +- .../main/java/io/appwrite/models/Document.kt | 14 +- .../java/io/appwrite/models/DocumentList.kt | 4 +- .../main/java/io/appwrite/models/Execution.kt | 34 +- .../java/io/appwrite/models/ExecutionList.kt | 4 +- .../src/main/java/io/appwrite/models/File.kt | 32 +- .../main/java/io/appwrite/models/FileList.kt | 4 +- .../main/java/io/appwrite/models/Headers.kt | 4 +- .../main/java/io/appwrite/models/Identity.kt | 22 +- .../java/io/appwrite/models/IdentityList.kt | 4 +- .../src/main/java/io/appwrite/models/Jwt.kt | 2 +- .../main/java/io/appwrite/models/Language.kt | 6 +- .../java/io/appwrite/models/LanguageList.kt | 4 +- .../main/java/io/appwrite/models/Locale.kt | 14 +- .../java/io/appwrite/models/LocaleCode.kt | 4 +- .../java/io/appwrite/models/LocaleCodeList.kt | 4 +- .../java/io/appwrite/models/Membership.kt | 30 +- .../java/io/appwrite/models/MembershipList.kt | 4 +- .../java/io/appwrite/models/MfaChallenge.kt | 8 +- .../java/io/appwrite/models/MfaFactors.kt | 10 +- .../io/appwrite/models/MfaRecoveryCodes.kt | 2 +- .../main/java/io/appwrite/models/MfaType.kt | 4 +- .../java/io/appwrite/models/Oauth2Approve.kt | 2 +- .../io/appwrite/models/Oauth2Authorize.kt | 4 +- .../java/io/appwrite/models/Oauth2Consent.kt | 20 +- .../io/appwrite/models/Oauth2ConsentList.kt | 4 +- .../io/appwrite/models/Oauth2ConsentToken.kt | 22 +- .../appwrite/models/Oauth2ConsentTokenList.kt | 4 +- .../models/Oauth2DeviceAuthorization.kt | 12 +- .../java/io/appwrite/models/Oauth2Grant.kt | 24 +- .../io/appwrite/models/Oauth2Organization.kt | 2 +- .../appwrite/models/Oauth2OrganizationList.kt | 4 +- .../main/java/io/appwrite/models/Oauth2PAR.kt | 4 +- .../java/io/appwrite/models/Oauth2Project.kt | 6 +- .../io/appwrite/models/Oauth2ProjectList.kt | 4 +- .../java/io/appwrite/models/Oauth2Reject.kt | 2 +- .../java/io/appwrite/models/Oauth2Token.kt | 10 +- .../main/java/io/appwrite/models/Passkey.kt | 61 ++ .../io/appwrite/models/PasskeyChallenge.kt | 61 ++ .../java/io/appwrite/models/PasskeyList.kt | 37 ++ .../src/main/java/io/appwrite/models/Phone.kt | 6 +- .../main/java/io/appwrite/models/PhoneList.kt | 4 +- .../main/java/io/appwrite/models/Presence.kt | 12 +- .../java/io/appwrite/models/PresenceList.kt | 4 +- .../src/main/java/io/appwrite/models/Row.kt | 14 +- .../main/java/io/appwrite/models/RowList.kt | 4 +- .../main/java/io/appwrite/models/Session.kt | 58 +- .../java/io/appwrite/models/SessionList.kt | 4 +- .../java/io/appwrite/models/Subscriber.kt | 16 +- .../main/java/io/appwrite/models/Target.kt | 16 +- .../src/main/java/io/appwrite/models/Team.kt | 10 +- .../main/java/io/appwrite/models/TeamList.kt | 4 +- .../src/main/java/io/appwrite/models/Token.kt | 12 +- .../java/io/appwrite/models/TokenSecret.kt | 69 +++ .../java/io/appwrite/models/Transaction.kt | 12 +- .../io/appwrite/models/TransactionList.kt | 4 +- .../src/main/java/io/appwrite/models/User.kt | 30 +- .../main/java/io/appwrite/services/Account.kt | 289 ++++++++- .../java/io/appwrite/services/Analytics.kt | 83 +++ .../main/java/io/appwrite/services/Oauth2.kt | 10 +- .../main/java/io/appwrite/services/Push.kt | 47 +- .../io/appwrite/services/PushBackground.kt | 35 +- .../java/io/appwrite/services/PushCore.kt | 25 +- .../main/java/io/appwrite/services/Teams.kt | 3 + .../src/test/java/io/appwrite/QueryTest.kt | 171 ++++++ .../src/test/java/io/appwrite/TopicTest.kt | 20 +- 130 files changed, 2347 insertions(+), 563 deletions(-) create mode 100644 docs/examples/java/account/create-passkey-token.md create mode 100644 docs/examples/java/account/create-passkey.md create mode 100644 docs/examples/java/account/delete-passkey.md create mode 100644 docs/examples/java/account/get-passkey.md create mode 100644 docs/examples/java/account/list-passkeys.md create mode 100644 docs/examples/java/account/update-passkey-token.md create mode 100644 docs/examples/java/account/update-passkey-verification.md create mode 100644 docs/examples/java/account/update-passkey.md create mode 100644 docs/examples/java/analytics/create-event.md create mode 100644 docs/examples/kotlin/account/create-passkey-token.md create mode 100644 docs/examples/kotlin/account/create-passkey.md create mode 100644 docs/examples/kotlin/account/delete-passkey.md create mode 100644 docs/examples/kotlin/account/get-passkey.md create mode 100644 docs/examples/kotlin/account/list-passkeys.md create mode 100644 docs/examples/kotlin/account/update-passkey-token.md create mode 100644 docs/examples/kotlin/account/update-passkey-verification.md create mode 100644 docs/examples/kotlin/account/update-passkey.md create mode 100644 docs/examples/kotlin/analytics/create-event.md create mode 100644 library/src/main/java/io/appwrite/models/Passkey.kt create mode 100644 library/src/main/java/io/appwrite/models/PasskeyChallenge.kt create mode 100644 library/src/main/java/io/appwrite/models/PasskeyList.kt create mode 100644 library/src/main/java/io/appwrite/models/TokenSecret.kt create mode 100644 library/src/main/java/io/appwrite/services/Analytics.kt create mode 100644 library/src/test/java/io/appwrite/QueryTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 21f1b0c0..3197c210 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,29 +1,33 @@ # Change Log -## 29.1.0-rc.3 - -* Added: first background subscription auto-requests `POST_NOTIFICATIONS` (Android 13+) from the visible activity -* Fixed: `onOpen`/`onClose` callbacks fire correctly when joining or leaving the background host -* Fixed: notification taps are recorded after launching the app, so tap-opened screens land on top - -## 29.1.0-rc.2 - -* Added: `getInitialNotification()`, `onNotificationOpened()`, and the `PushNotificationOpened` data class (topic + data) -* Added: `onOpen`/`onClose` callbacks now fire for background push connections -* Added: push auth falls back to the client's cookie-store session -* Fixed: a foreground message with no live callback now still posts a notification -* Updated: notification taps route through an internal `PushOpenActivity` - -## 29.1.0-rc.1 - -* Updated: regenerated with sdk-generator 5.5.0; no SDK code changes - -## 29.1.0-rc.0 - -* Added: optional `method` param on `Client.chunkedUpload()` to set the upload HTTP method -* Added: background push notifications render the server `notification` title, body, and image -* Updated: `Avatars.updatePhoto()` uploads via `PUT` -* Fixed: an exactly-chunk-sized file uploads in a single request +## 30.0.0 + +* Breaking: requests send `x-appwrite-response-format: 2.5.0`; responses follow the 2.5 shapes +* Breaking: missing response fields map to their documented defaults +* Breaking: `oauth2` `resource` params now take `List` on the authorize, PAR, device and token methods +* Breaking: `Push.subscribe` takes `notifyInForeground` before the callback; Java callers must pass it +* Breaking: removed the `Topic.any()` and `Topic.all()` wildcard builders and `ResolvedTopic`; `Push.subscribe` takes strings or `Topic` +* Added: `notifyInForeground` option on `Push.subscribe` +* Added: `PushOpenActivity` for opening notifications +* Added: `Query` aggregates (`count`, `countDistinct`, `sum`, `avg`, `min`, `max` and more), `groupBy`, `having`, `distinct`, joins, `relationship`, `covers`, `spatialEquals` and `page` +* Added: `Query.builder()` to chain queries; `build()` returns the query list +* Added: `ExecutionTrigger.EMAIL` +* Updated: `Topic.path` rejects levels containing a NUL character +* Fixed: a file exactly `CHUNK_SIZE` bytes long uploads in a single request + +## 29.2.0 + +* Added: `analytics.createEvent` in a new `Analytics` service to send tracking events +* Added: optional `state` on `account.createOAuth2Session` and `account.createOAuth2Token`, returned unchanged on the success and failure URLs +* Added: optional `membershipId` on `teams.createMembership` +* Fixed: `avatars.updatePhoto` sends `PUT`, so uploading a profile photo no longer returns 404 +* Fixed: OAuth2 query parameters (`success`, `failure`, `scopes`, `state`) are URL-encoded, so values containing `&`, `+` or `#` survive the redirect + +## 29.1.0 + +* Added: `account` passkey methods `listPasskeys`, `createPasskey`, `getPasskey`, `updatePasskey`, `deletePasskey` and `updatePasskeyVerification` +* Added: `account.createPasskeyToken` and `account.updatePasskeyToken` for passkey sign-in +* Added: `Passkey`, `PasskeyList`, `PasskeyChallenge` and `TokenSecret` models ## 29.0.0 diff --git a/README.md b/README.md index afcc18e1..d045d7a4 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ ![Maven Central](https://img.shields.io/maven-central/v/io.appwrite/sdk-for-android.svg?color=green&style=flat-square) ![License](https://img.shields.io/github/license/appwrite/sdk-for-android.svg?style=flat-square) -![Version](https://img.shields.io/badge/api%20version-2.3.0-blue.svg?style=flat-square) +![Version](https://img.shields.io/badge/api%20version-2.5.0-blue.svg?style=flat-square) [![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator) [![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite) [![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord) -**This SDK targets Appwrite server version 2.3.x as shipped on Appwrite Cloud.** Self-hosted releases can lag behind Cloud — if you run an older self-hosted build, use a matching older SDK from [previous releases](https://github.com/appwrite/sdk-for-android/releases) when APIs differ. +**This SDK targets Appwrite server version 2.5.x as shipped on Appwrite Cloud.** Self-hosted releases can lag behind Cloud — if you run an older self-hosted build, use a matching older SDK from [previous releases](https://github.com/appwrite/sdk-for-android/releases) when APIs differ. Appwrite is an open-source backend as a service server that abstracts and simplifies complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the Android SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs) @@ -38,7 +38,7 @@ repositories { Next, add the dependency to your project's `build.gradle(.kts)` file: ```groovy -implementation("io.appwrite:sdk-for-android:29.1.0-rc.3") +implementation("io.appwrite:sdk-for-android:30.0.0") ``` ### Maven @@ -49,7 +49,7 @@ Add this to your project's `pom.xml` file: io.appwrite sdk-for-android - 29.1.0-rc.3 + 30.0.0 ``` @@ -79,8 +79,10 @@ app every 15 to 60 seconds to reconnect, and the broker replays what was sent in While the app is not on screen, each message is also posted as a notification that opens your launch activity with `Push.EXTRA_TOPIC` and `Push.EXTRA_PAYLOAD` in its extras. It shows the -title, body and image sent with `createPush`, falling back to the subscription's `title` and the -raw payload for other messages. On Android 13 and later it needs the `POST_NOTIFICATIONS` +title, body and image sent with `createPush`, falling back to the subscription's `title`, and to +the raw payload as the body for messages that are not a JSON object. A push with neither a title +nor a body, such as a data-only push, posts no notification; it still reaches your callback or +`PushReceiver`. On Android 13 and later it needs the `POST_NOTIFICATIONS` permission: the first background subscription asks for it from the visible activity (the one `Push` is created with, else the next one to resume), or call `Push.requestNotificationPermission(activity)` to ask yourself. Set its @@ -119,6 +121,13 @@ The SDK uses exact alarms on its own whenever the app may schedule them. To allo nothing to ask, including when the permission is not declared. The SDK never opens these screens on its own. +If `backgroundRestricted` is true, the user restricted the app's background usage and messages can +wait until the app is opened. Exact alarms don't lift it, and neither does foreground mode: Android +stops a restricted app's foreground service once it leaves the screen. Ask the user to choose +Unrestricted in the app's battery settings (Settings > Apps > your app > Battery); granting the +battery optimisation exemption also lets the SDK's alarms and jobs run, and `bestEffort` then +reflects only the exact-alarm state. + If the user force-stops the app (Settings > Force stop, and on some devices swiping it away from recents), Android cancels its alarms and jobs: nothing is delivered until the app is opened again, and the broker then replays what was sent meanwhile. @@ -232,7 +241,7 @@ val user = account.create( ### Type Safety with Models -The Appwrite Android SDK provides type safety when working with database documents through generic methods. Methods like `listDocuments`, `getDocument`, and others accept a `nestedType` parameter that allows you to specify your custom model type for full type safety. +The Appwrite Android SDK provides type safety when working with table rows through generic methods. Methods like `listRows`, `getRow`, and others accept a `nestedType` parameter that allows you to specify your custom model type for full type safety. **Kotlin:** ```kotlin @@ -245,16 +254,17 @@ data class Book( val isCheckedOut: Boolean ) -val databases = Databases(client) +val tablesDB = TablesDB(client) try { - val documents = databases.listDocuments( + val result = tablesDB.listRows( databaseId = "your-database-id", - collectionId = "your-collection-id", + tableId = "your-table-id", nestedType = Book::class.java // Pass in your custom model type ) - for (book in documents.documents) { + for (row in result.rows) { + val book = row.data Log.d("Appwrite", "Book: ${book.name} by ${book.author}") // Now you have full type safety } } catch (e: AppwriteException) { @@ -299,21 +309,24 @@ public class Book { public void setCheckedOut(boolean checkedOut) { isCheckedOut = checkedOut; } } -Databases databases = new Databases(client); - -try { - DocumentList documents = databases.listDocuments( - "your-database-id", - "your-collection-id", - Book.class // Pass in your custom model type - ); - - for (Book book : documents.getDocuments()) { - Log.d("Appwrite", "Book: " + book.getName() + " by " + book.getAuthor()); // Now you have full type safety - } -} catch (AppwriteException e) { - Log.e("Appwrite", e.getMessage() != null ? e.getMessage() : "Unknown error"); -} +TablesDB tablesDB = new TablesDB(client); + +tablesDB.listRows( + "your-database-id", + "your-table-id", + Book.class, // Pass in your custom model type + new CoroutineCallback<>((result, error) -> { + if (error != null) { + Log.e("Appwrite", error.getMessage() != null ? error.getMessage() : "Unknown error"); + return; + } + + for (Row row : result.getRows()) { + Book book = row.getData(); + Log.d("Appwrite", "Book: " + book.getName() + " by " + book.getAuthor()); // Now you have full type safety + } + }) +); ``` **Tip**: You can use the `appwrite types` command to automatically generate model definitions based on your Appwrite database schema. Learn more about [type generation](https://appwrite.io/docs/products/databases/type-generation). @@ -363,7 +376,7 @@ The Appwrite Android SDK raises an `AppwriteException` object with `message`, `c ```kotlin try { var user = account.create(ID.unique(),"email@example.com","password","Walter O'Brien") - Log.d("Appwrite user", user.toMap()) + Log.d("Appwrite user", user.toMap().toString()) } catch(e : AppwriteException) { e.printStackTrace() } diff --git a/docs/examples/java/account/create-o-auth-2-session.md b/docs/examples/java/account/create-o-auth-2-session.md index da547421..f2a07eff 100644 --- a/docs/examples/java/account/create-o-auth-2-session.md +++ b/docs/examples/java/account/create-o-auth-2-session.md @@ -17,6 +17,7 @@ account.createOAuth2Session( "https://example.com", // success (optional) "https://example.com", // failure (optional) List.of(), // scopes (optional) + "", // state (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/java/account/create-o-auth-2-token.md b/docs/examples/java/account/create-o-auth-2-token.md index ae685850..885c066f 100644 --- a/docs/examples/java/account/create-o-auth-2-token.md +++ b/docs/examples/java/account/create-o-auth-2-token.md @@ -17,6 +17,7 @@ account.createOAuth2Token( "https://example.com", // success (optional) "https://example.com", // failure (optional) List.of(), // scopes (optional) + "", // state (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/java/account/create-passkey-token.md b/docs/examples/java/account/create-passkey-token.md new file mode 100644 index 00000000..12db5fe5 --- /dev/null +++ b/docs/examples/java/account/create-passkey-token.md @@ -0,0 +1,22 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.createPasskeyToken(new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); +})); +``` diff --git a/docs/examples/java/account/create-passkey.md b/docs/examples/java/account/create-passkey.md new file mode 100644 index 00000000..39a9a29d --- /dev/null +++ b/docs/examples/java/account/create-passkey.md @@ -0,0 +1,27 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.createPasskey( + "", // passkeyId (optional) + "", // name (optional) + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/delete-passkey.md b/docs/examples/java/account/delete-passkey.md new file mode 100644 index 00000000..a59d3848 --- /dev/null +++ b/docs/examples/java/account/delete-passkey.md @@ -0,0 +1,26 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.deletePasskey( + "", // passkeyId + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/get-passkey.md b/docs/examples/java/account/get-passkey.md new file mode 100644 index 00000000..5d988e86 --- /dev/null +++ b/docs/examples/java/account/get-passkey.md @@ -0,0 +1,26 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.getPasskey( + "", // passkeyId + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/list-passkeys.md b/docs/examples/java/account/list-passkeys.md new file mode 100644 index 00000000..6b04b1fb --- /dev/null +++ b/docs/examples/java/account/list-passkeys.md @@ -0,0 +1,27 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.listPasskeys( + List.of(), // queries (optional) + false, // total (optional) + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/update-passkey-token.md b/docs/examples/java/account/update-passkey-token.md new file mode 100644 index 00000000..6370aaaf --- /dev/null +++ b/docs/examples/java/account/update-passkey-token.md @@ -0,0 +1,27 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.updatePasskeyToken( + "", // challengeId + Map.of("a", "b"), // credential + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/update-passkey-verification.md b/docs/examples/java/account/update-passkey-verification.md new file mode 100644 index 00000000..53eed3a1 --- /dev/null +++ b/docs/examples/java/account/update-passkey-verification.md @@ -0,0 +1,27 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.updatePasskeyVerification( + "", // passkeyId + Map.of("a", "b"), // credential + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/account/update-passkey.md b/docs/examples/java/account/update-passkey.md new file mode 100644 index 00000000..3cf1078a --- /dev/null +++ b/docs/examples/java/account/update-passkey.md @@ -0,0 +1,27 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Account; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Account account = new Account(client); + +account.updatePasskey( + "", // passkeyId + "", // name + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/analytics/create-event.md b/docs/examples/java/analytics/create-event.md new file mode 100644 index 00000000..e2b8dbe0 --- /dev/null +++ b/docs/examples/java/analytics/create-event.md @@ -0,0 +1,38 @@ +```java +import android.util.Log; + +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Analytics; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Analytics analytics = new Analytics(client); + +analytics.createEvent( + "", // propertyId + "", // name + "https://example.com", // url + "", // domain (optional) + "", // referrer (optional) + 0L, // screenWidth (optional) + "", // sessionHash (optional) + 0L, // scrollDepth (optional) + 0L, // engagementTime (optional) + List.of(), // props (optional) + "", // userId (optional) + "", // ip (optional) + "", // userAgent (optional) + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + +``` diff --git a/docs/examples/java/oauth2/authorize-post.md b/docs/examples/java/oauth2/authorize-post.md index 99ed6b8e..077546fd 100644 --- a/docs/examples/java/oauth2/authorize-post.md +++ b/docs/examples/java/oauth2/authorize-post.md @@ -23,7 +23,7 @@ oauth2.authorizePost( "", // prompt (optional) 0L, // max_age (optional) "", // authorization_details (optional) - "", // resource (optional) + List.of(), // resource (optional) "", // audience (optional) "", // request_uri (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/java/oauth2/authorize.md b/docs/examples/java/oauth2/authorize.md index 62a452c8..d5b72cf1 100644 --- a/docs/examples/java/oauth2/authorize.md +++ b/docs/examples/java/oauth2/authorize.md @@ -23,7 +23,7 @@ oauth2.authorize( "", // prompt (optional) 0L, // max_age (optional) "", // authorization_details (optional) - "", // resource (optional) + List.of(), // resource (optional) "", // audience (optional) "", // request_uri (optional) new CoroutineCallback<>((result, error) -> { diff --git a/docs/examples/java/oauth2/create-device-authorization.md b/docs/examples/java/oauth2/create-device-authorization.md index 0b65a1ab..c2fc4adb 100644 --- a/docs/examples/java/oauth2/create-device-authorization.md +++ b/docs/examples/java/oauth2/create-device-authorization.md @@ -15,7 +15,7 @@ oauth2.createDeviceAuthorization( "", // client_id (optional) "", // scope (optional) "", // authorization_details (optional) - "", // resource (optional) + List.of(), // resource (optional) "", // audience (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/java/oauth2/create-par.md b/docs/examples/java/oauth2/create-par.md index e65bdb54..e1f14de9 100644 --- a/docs/examples/java/oauth2/create-par.md +++ b/docs/examples/java/oauth2/create-par.md @@ -23,7 +23,7 @@ oauth2.createPAR( "", // prompt (optional) 0L, // max_age (optional) "", // authorization_details (optional) - "", // resource (optional) + List.of(), // resource (optional) "", // audience (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/java/oauth2/create-token.md b/docs/examples/java/oauth2/create-token.md index 39b6fa25..5c808df3 100644 --- a/docs/examples/java/oauth2/create-token.md +++ b/docs/examples/java/oauth2/create-token.md @@ -20,7 +20,7 @@ oauth2.createToken( "", // client_secret (optional) "", // code_verifier (optional) "https://example.com", // redirect_uri (optional) - "", // resource (optional) + List.of(), // resource (optional) "", // audience (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { diff --git a/docs/examples/java/teams/create-membership.md b/docs/examples/java/teams/create-membership.md index d7fba7c1..b6d23a6a 100644 --- a/docs/examples/java/teams/create-membership.md +++ b/docs/examples/java/teams/create-membership.md @@ -19,6 +19,7 @@ teams.createMembership( "+12065550100", // phone (optional) "https://example.com", // url (optional) "", // name (optional) + "", // membershipId (optional) new CoroutineCallback<>((result, error) -> { if (error != null) { error.printStackTrace(); diff --git a/docs/examples/kotlin/account/create-o-auth-2-session.md b/docs/examples/kotlin/account/create-o-auth-2-session.md index cde941c7..c9b3a92a 100644 --- a/docs/examples/kotlin/account/create-o-auth-2-session.md +++ b/docs/examples/kotlin/account/create-o-auth-2-session.md @@ -15,5 +15,6 @@ account.createOAuth2Session( success = "https://example.com", // (optional) failure = "https://example.com", // (optional) scopes = listOf(), // (optional) + state = "", // (optional) ) ``` diff --git a/docs/examples/kotlin/account/create-o-auth-2-token.md b/docs/examples/kotlin/account/create-o-auth-2-token.md index 5e905649..3d5b0d77 100644 --- a/docs/examples/kotlin/account/create-o-auth-2-token.md +++ b/docs/examples/kotlin/account/create-o-auth-2-token.md @@ -15,5 +15,6 @@ account.createOAuth2Token( success = "https://example.com", // (optional) failure = "https://example.com", // (optional) scopes = listOf(), // (optional) + state = "", // (optional) ) ``` diff --git a/docs/examples/kotlin/account/create-passkey-token.md b/docs/examples/kotlin/account/create-passkey-token.md new file mode 100644 index 00000000..52b6bf3d --- /dev/null +++ b/docs/examples/kotlin/account/create-passkey-token.md @@ -0,0 +1,13 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.createPasskeyToken() +``` diff --git a/docs/examples/kotlin/account/create-passkey.md b/docs/examples/kotlin/account/create-passkey.md new file mode 100644 index 00000000..69a06515 --- /dev/null +++ b/docs/examples/kotlin/account/create-passkey.md @@ -0,0 +1,16 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.createPasskey( + passkeyId = "", // (optional) + name = "", // (optional) +) +``` diff --git a/docs/examples/kotlin/account/delete-passkey.md b/docs/examples/kotlin/account/delete-passkey.md new file mode 100644 index 00000000..08bf8fd6 --- /dev/null +++ b/docs/examples/kotlin/account/delete-passkey.md @@ -0,0 +1,15 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.deletePasskey( + passkeyId = "", +) +``` diff --git a/docs/examples/kotlin/account/get-passkey.md b/docs/examples/kotlin/account/get-passkey.md new file mode 100644 index 00000000..ddf9e833 --- /dev/null +++ b/docs/examples/kotlin/account/get-passkey.md @@ -0,0 +1,15 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.getPasskey( + passkeyId = "", +) +``` diff --git a/docs/examples/kotlin/account/list-passkeys.md b/docs/examples/kotlin/account/list-passkeys.md new file mode 100644 index 00000000..74c8add8 --- /dev/null +++ b/docs/examples/kotlin/account/list-passkeys.md @@ -0,0 +1,16 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.listPasskeys( + queries = listOf(), // (optional) + total = false, // (optional) +) +``` diff --git a/docs/examples/kotlin/account/update-passkey-token.md b/docs/examples/kotlin/account/update-passkey-token.md new file mode 100644 index 00000000..f48e29f9 --- /dev/null +++ b/docs/examples/kotlin/account/update-passkey-token.md @@ -0,0 +1,16 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.updatePasskeyToken( + challengeId = "", + credential = mapOf( "a" to "b" ), +) +``` diff --git a/docs/examples/kotlin/account/update-passkey-verification.md b/docs/examples/kotlin/account/update-passkey-verification.md new file mode 100644 index 00000000..41c016a8 --- /dev/null +++ b/docs/examples/kotlin/account/update-passkey-verification.md @@ -0,0 +1,16 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.updatePasskeyVerification( + passkeyId = "", + credential = mapOf( "a" to "b" ), +) +``` diff --git a/docs/examples/kotlin/account/update-passkey.md b/docs/examples/kotlin/account/update-passkey.md new file mode 100644 index 00000000..c8c2bc34 --- /dev/null +++ b/docs/examples/kotlin/account/update-passkey.md @@ -0,0 +1,16 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val account = Account(client) + +val result = account.updatePasskey( + passkeyId = "", + name = "", +) +``` diff --git a/docs/examples/kotlin/analytics/create-event.md b/docs/examples/kotlin/analytics/create-event.md new file mode 100644 index 00000000..820e0bac --- /dev/null +++ b/docs/examples/kotlin/analytics/create-event.md @@ -0,0 +1,27 @@ +```kotlin +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Analytics + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val analytics = Analytics(client) + +val result = analytics.createEvent( + propertyId = "", + name = "", + url = "https://example.com", + domain = "", // (optional) + referrer = "", // (optional) + screenWidth = 0, // (optional) + sessionHash = "", // (optional) + scrollDepth = 0, // (optional) + engagementTime = 0, // (optional) + props = listOf(), // (optional) + userId = "", // (optional) + ip = "", // (optional) + userAgent = "", // (optional) +) +``` diff --git a/docs/examples/kotlin/oauth2/authorize-post.md b/docs/examples/kotlin/oauth2/authorize-post.md index 88bc4ee0..ea490994 100644 --- a/docs/examples/kotlin/oauth2/authorize-post.md +++ b/docs/examples/kotlin/oauth2/authorize-post.md @@ -21,7 +21,7 @@ val result = oauth2.authorizePost( prompt = "", // (optional) maxAge = 0, // (optional) authorizationDetails = "", // (optional) - resource = "", // (optional) + resource = listOf(), // (optional) audience = "", // (optional) requestUri = "", // (optional) ) diff --git a/docs/examples/kotlin/oauth2/authorize.md b/docs/examples/kotlin/oauth2/authorize.md index 4d307e8d..6c598343 100644 --- a/docs/examples/kotlin/oauth2/authorize.md +++ b/docs/examples/kotlin/oauth2/authorize.md @@ -21,7 +21,7 @@ val result = oauth2.authorize( prompt = "", // (optional) maxAge = 0, // (optional) authorizationDetails = "", // (optional) - resource = "", // (optional) + resource = listOf(), // (optional) audience = "", // (optional) requestUri = "", // (optional) ) diff --git a/docs/examples/kotlin/oauth2/create-device-authorization.md b/docs/examples/kotlin/oauth2/create-device-authorization.md index ab307c37..d4fb9b02 100644 --- a/docs/examples/kotlin/oauth2/create-device-authorization.md +++ b/docs/examples/kotlin/oauth2/create-device-authorization.md @@ -13,7 +13,7 @@ val result = oauth2.createDeviceAuthorization( clientId = "", // (optional) scope = "", // (optional) authorizationDetails = "", // (optional) - resource = "", // (optional) + resource = listOf(), // (optional) audience = "", // (optional) ) ``` diff --git a/docs/examples/kotlin/oauth2/create-par.md b/docs/examples/kotlin/oauth2/create-par.md index 752c64eb..213096c1 100644 --- a/docs/examples/kotlin/oauth2/create-par.md +++ b/docs/examples/kotlin/oauth2/create-par.md @@ -21,7 +21,7 @@ val result = oauth2.createPAR( prompt = "", // (optional) maxAge = 0, // (optional) authorizationDetails = "", // (optional) - resource = "", // (optional) + resource = listOf(), // (optional) audience = "", // (optional) ) ``` diff --git a/docs/examples/kotlin/oauth2/create-token.md b/docs/examples/kotlin/oauth2/create-token.md index 5e23344f..a0e4f20e 100644 --- a/docs/examples/kotlin/oauth2/create-token.md +++ b/docs/examples/kotlin/oauth2/create-token.md @@ -18,7 +18,7 @@ val result = oauth2.createToken( clientSecret = "", // (optional) codeVerifier = "", // (optional) redirectUri = "https://example.com", // (optional) - resource = "", // (optional) + resource = listOf(), // (optional) audience = "", // (optional) ) ``` diff --git a/docs/examples/kotlin/teams/create-membership.md b/docs/examples/kotlin/teams/create-membership.md index 219119af..2f73c039 100644 --- a/docs/examples/kotlin/teams/create-membership.md +++ b/docs/examples/kotlin/teams/create-membership.md @@ -17,5 +17,6 @@ val result = teams.createMembership( phone = "+12065550100", // (optional) url = "https://example.com", // (optional) name = "", // (optional) + membershipId = "", // (optional) ) ``` diff --git a/library/src/main/java/io/appwrite/Client.kt b/library/src/main/java/io/appwrite/Client.kt index 3df991a3..ecc12c3c 100644 --- a/library/src/main/java/io/appwrite/Client.kt +++ b/library/src/main/java/io/appwrite/Client.kt @@ -101,8 +101,8 @@ class Client @JvmOverloads constructor( "x-sdk-name" to "Android", "x-sdk-platform" to "client", "x-sdk-language" to "android", - "x-sdk-version" to "29.1.0-rc.3", - "x-appwrite-response-format" to "2.3.0" + "x-sdk-version" to "30.0.0", + "x-appwrite-response-format" to "2.5.0" ) config = mutableMapOf() @@ -434,7 +434,10 @@ class Client @JvmOverloads constructor( headers: Map = mapOf(), params: Map = mapOf(), ): Request { - val filteredParams = params.filterValues { it != null } + // flatten() resolves Query/Query.Builder values before the null filter, + // so a builder that renders to null is dropped like any other null. + val flattenedParams = params.mapValues { flatten(it.value) } + val filteredParams = flattenedParams.filterValues { it != null } val requestHeaders = this.headers.toHeaders().newBuilder() .addAll(headers.toHeaders()) @@ -497,7 +500,7 @@ class Client @JvmOverloads constructor( } builder.build() } else { - params + flattenedParams .toJsonRequestBody() .toRequestBody("application/json".toMediaType()) } @@ -841,4 +844,44 @@ class Client @JvmOverloads constructor( } }) } + + fun flatten(value: Any?): Any? { + if (value is Query.Builder) { + return value.build() + } + if (value is Query) { + return value.toString() + } + + val items = when (value) { + is Array<*> -> value.asList() + is Collection<*> -> value + else -> return value + } + + val out = ArrayList(items.size) + var changed = false + for (item in items) { + val flat = flatten(item) + if (item is Query.Builder || item is Query) { + if (flat is Collection<*>) { + out.addAll(flat) + } else { + out.add(flat) + } + changed = true + continue + } + if ((item is Array<*> || item is Collection<*>) && flat is Collection<*> && flat.all { it is String }) { + out.addAll(flat) + changed = true + continue + } + if (flat !== item) { + changed = true + } + out.add(flat) + } + return if (changed || value is Array<*>) out else value + } } diff --git a/library/src/main/java/io/appwrite/Query.kt b/library/src/main/java/io/appwrite/Query.kt index 23f100df..6be7a6da 100644 --- a/library/src/main/java/io/appwrite/Query.kt +++ b/library/src/main/java/io/appwrite/Query.kt @@ -377,6 +377,15 @@ class Query( */ fun elemMatch(attribute: String, queries: List) = Query("elemMatch", attribute, queries.map { it.fromJson>() }).toJson() + /** + * Constrain populated related documents on a relationship attribute. + * + * @param attribute The relationship attribute to query. + * @param queries Filter, order, select, and pagination queries applied to related documents. + * @returns The query string. + */ + fun relationship(attribute: String, queries: List) = Query("relationship", attribute, queries.map { it.fromJson>() }).toJson() + /** * Filter resources where attribute is at a specific distance from the given coordinates. * @@ -520,6 +529,377 @@ class Query( */ fun notTouches(attribute: String, values: List) = Query("notTouches", attribute, listOf(values)).toJson() + /** + * Count rows of [attribute], optionally aliased. + * + * @param attribute The attribute to count. Defaults to `*`. + * @param alias Optional result alias. + * @returns The query string. + */ + fun count(attribute: String = "*", alias: String = "") = Query("count", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Count distinct values of [attribute], optionally aliased. + * + * @param attribute The attribute to count. + * @param alias Optional result alias. + * @returns The query string. + */ + fun countDistinct(attribute: String, alias: String = "") = Query("countDistinct", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Sum values of [attribute], optionally aliased. + * + * @param attribute The attribute to sum. + * @param alias Optional result alias. + * @returns The query string. + */ + fun sum(attribute: String, alias: String = "") = Query("sum", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Average values of [attribute], optionally aliased. + * + * @param attribute The attribute to average. + * @param alias Optional result alias. + * @returns The query string. + */ + fun avg(attribute: String, alias: String = "") = Query("avg", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Minimum value of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ + fun min(attribute: String, alias: String = "") = Query("min", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Maximum value of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ + fun max(attribute: String, alias: String = "") = Query("max", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Standard deviation of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ + fun stddev(attribute: String, alias: String = "") = Query("stddev", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Population standard deviation of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ + fun stddevPop(attribute: String, alias: String = "") = Query("stddevPop", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Sample standard deviation of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ + fun stddevSamp(attribute: String, alias: String = "") = Query("stddevSamp", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Variance of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ + fun variance(attribute: String, alias: String = "") = Query("variance", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Population variance of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ + fun varPop(attribute: String, alias: String = "") = Query("varPop", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Sample variance of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ + fun varSamp(attribute: String, alias: String = "") = Query("varSamp", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Bitwise AND of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ + fun bitAnd(attribute: String, alias: String = "") = Query("bitAnd", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Bitwise OR of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ + fun bitOr(attribute: String, alias: String = "") = Query("bitOr", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Bitwise XOR of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ + fun bitXor(attribute: String, alias: String = "") = Query("bitXor", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Group results by [attributes]. + * + * @param attributes Attributes to group by. + * @returns The query string. + */ + fun groupBy(attributes: List) = Query("groupBy", null, attributes).toJson() + + /** + * Filter grouped results. [queries] are nested like `and`/`or`. + * + * @param queries Nested query strings. + * @returns The query string. + */ + fun having(queries: List) = Query("having", null, queries.map { it.fromJson>() }).toJson() + + /** + * Select distinct rows. + * + * @returns The query string. + */ + fun distinct() = Query("distinct").toJson() + + /** + * Column-to-column join ON condition. + * + * @param left Left-hand column. + * @param right Right-hand column. + * @param operator Comparison operator. Defaults to `=`. + * @returns The query string. + */ + fun on(left: String, right: String, operator: String = "=") = Query("on", values = listOf(left, operator, right)).toJson() + + /** + * Inner join. [table] is the joined collection/table ID. + * + * @param table Joined table ID. + * @param left Left-hand join column. + * @param right Right-hand join column. + * @param operator Comparison operator. Defaults to `=`. + * @param alias Optional table alias. + * @returns The query string. + */ + fun join(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("join", table, left, right, operator, alias) + + /** + * Inner join with nested ON queries and no alias. + * + * @param table Joined table ID. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ + fun join(table: String, on: List) = nestedJoin("join", table, "", on) + + /** + * Inner join with nested ON queries and an alias. + * + * @param table Joined table ID. + * @param alias Table alias. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ + fun join(table: String, alias: String, on: List) = nestedJoin("join", table, alias, on) + + /** + * Left join. [table] is the joined collection/table ID. + * + * @param table Joined table ID. + * @param left Left-hand join column. + * @param right Right-hand join column. + * @param operator Comparison operator. Defaults to `=`. + * @param alias Optional table alias. + * @returns The query string. + */ + fun leftJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("leftJoin", table, left, right, operator, alias) + + /** + * Left join with nested ON queries and no alias. + * + * @param table Joined table ID. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ + fun leftJoin(table: String, on: List) = nestedJoin("leftJoin", table, "", on) + + /** + * Left join with nested ON queries and an alias. + * + * @param table Joined table ID. + * @param alias Table alias. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ + fun leftJoin(table: String, alias: String, on: List) = nestedJoin("leftJoin", table, alias, on) + + /** + * Right join. [table] is the joined collection/table ID. + * + * @param table Joined table ID. + * @param left Left-hand join column. + * @param right Right-hand join column. + * @param operator Comparison operator. Defaults to `=`. + * @param alias Optional table alias. + * @returns The query string. + */ + fun rightJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("rightJoin", table, left, right, operator, alias) + + /** + * Right join with nested ON queries and no alias. + * + * @param table Joined table ID. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ + fun rightJoin(table: String, on: List) = nestedJoin("rightJoin", table, "", on) + + /** + * Right join with nested ON queries and an alias. + * + * @param table Joined table ID. + * @param alias Table alias. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ + fun rightJoin(table: String, alias: String, on: List) = nestedJoin("rightJoin", table, alias, on) + + /** + * Full outer join. [table] is the joined collection/table ID. + * + * @param table Joined table ID. + * @param left Left-hand join column. + * @param right Right-hand join column. + * @param operator Comparison operator. Defaults to `=`. + * @param alias Optional table alias. + * @returns The query string. + */ + fun fullOuterJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("fullOuterJoin", table, left, right, operator, alias) + + /** + * Full outer join with nested ON queries and no alias. + * + * @param table Joined table ID. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ + fun fullOuterJoin(table: String, on: List) = nestedJoin("fullOuterJoin", table, "", on) + + /** + * Full outer join with nested ON queries and an alias. + * + * @param table Joined table ID. + * @param alias Table alias. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ + fun fullOuterJoin(table: String, alias: String, on: List) = nestedJoin("fullOuterJoin", table, alias, on) + + /** + * Cross join. [table] is the joined collection/table ID. + * + * @param table Joined table ID. + * @param alias Optional table alias. + * @returns The query string. + */ + fun crossJoin(table: String, alias: String = "") = Query("crossJoin", table, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + /** + * Filter resources where [attribute] covers the given geometry. + * + * @param attribute The attribute to filter on. + * @param values The coordinate values. + * @returns The query string. + */ + fun covers(attribute: String, values: List) = Query("covers", attribute, listOf(values)).toJson() + + /** + * Filter resources where [attribute] does not cover the given geometry. + * + * @param attribute The attribute to filter on. + * @param values The coordinate values. + * @returns The query string. + */ + fun notCovers(attribute: String, values: List) = Query("notCovers", attribute, listOf(values)).toJson() + + /** + * Filter resources where [attribute] is spatially equal to the given geometry. + * + * @param attribute The attribute to filter on. + * @param values The coordinate values. + * @returns The query string. + */ + fun spatialEquals(attribute: String, values: List) = Query("spatialEquals", attribute, listOf(values)).toJson() + + /** + * Filter resources where [attribute] is not spatially equal to the given geometry. + * + * @param attribute The attribute to filter on. + * @param values The coordinate values. + * @returns The query string. + */ + fun notSpatialEquals(attribute: String, values: List) = Query("notSpatialEquals", attribute, listOf(values)).toJson() + + /** + * Return limit and offset queries for 1-based page pagination. + * + * @param page 1-based page number. + * @param perPage Rows per page. Defaults to 25. + * @returns Limit and offset query strings. + */ + fun page(page: Int, perPage: Int = 25): List { + require(page >= 1) { "Page must be >= 1, got $page" } + require(perPage >= 1) { "Per page must be >= 1, got $perPage" } + return listOf(limit(perPage), offset(Math.multiplyExact(page - 1, perPage))) + } + + /** + * Start a fluent query builder. + * + * @returns A new query builder. + */ + fun builder() = Builder() + + private fun joinQuery(method: String, table: String, left: String, right: String, operator: String, alias: String): String { + val values = if (alias.isNotEmpty()) listOf(left, operator, right, alias) else listOf(left, operator, right) + return Query(method, table, values).toJson() + } + + private fun nestedJoin(method: String, table: String, alias: String, on: List): String { + val values = mutableListOf() + if (alias.isNotEmpty()) { + values.add(alias) + } + on.forEach { values.add(it.fromJson>()) } + return Query(method, table, values).toJson() + } + /** * Parse the value to a list of values. * @@ -533,4 +913,198 @@ class Query( } } } + + class Builder { + private val queries = mutableListOf() + + fun build(): List = queries.toList() + + fun equal(attribute: String, value: Any) = push(Query.equal(attribute, value)) + + fun notEqual(attribute: String, value: Any) = push(Query.notEqual(attribute, value)) + + fun lessThan(attribute: String, value: Any) = push(Query.lessThan(attribute, value)) + + fun lessThanEqual(attribute: String, value: Any) = push(Query.lessThanEqual(attribute, value)) + + fun greaterThan(attribute: String, value: Any) = push(Query.greaterThan(attribute, value)) + + fun greaterThanEqual(attribute: String, value: Any) = push(Query.greaterThanEqual(attribute, value)) + + fun search(attribute: String, value: String) = push(Query.search(attribute, value)) + + fun isNull(attribute: String) = push(Query.isNull(attribute)) + + fun isNotNull(attribute: String) = push(Query.isNotNull(attribute)) + + fun between(attribute: String, start: Any, end: Any) = push(Query.between(attribute, start, end)) + + fun startsWith(attribute: String, value: String) = push(Query.startsWith(attribute, value)) + + fun endsWith(attribute: String, value: String) = push(Query.endsWith(attribute, value)) + + fun select(attributes: List) = push(Query.select(attributes)) + + fun cursorAfter(documentId: String) = push(Query.cursorAfter(documentId)) + + fun cursorBefore(documentId: String) = push(Query.cursorBefore(documentId)) + + fun orderAsc(attribute: String) = push(Query.orderAsc(attribute)) + + fun orderDesc(attribute: String) = push(Query.orderDesc(attribute)) + + fun orderRandom() = push(Query.orderRandom()) + + fun limit(limit: Int) = push(Query.limit(limit)) + + fun offset(offset: Int) = push(Query.offset(offset)) + + fun contains(attribute: String, value: Any) = push(Query.contains(attribute, value)) + + fun containsAny(attribute: String, value: List) = push(Query.containsAny(attribute, value)) + + fun containsAll(attribute: String, value: List) = push(Query.containsAll(attribute, value)) + + fun notContains(attribute: String, value: Any) = push(Query.notContains(attribute, value)) + + fun notSearch(attribute: String, value: String) = push(Query.notSearch(attribute, value)) + + fun notBetween(attribute: String, start: Any, end: Any) = push(Query.notBetween(attribute, start, end)) + + fun notStartsWith(attribute: String, value: String) = push(Query.notStartsWith(attribute, value)) + + fun notEndsWith(attribute: String, value: String) = push(Query.notEndsWith(attribute, value)) + + fun regex(attribute: String, pattern: String) = push(Query.regex(attribute, pattern)) + + fun exists(attributes: List) = push(Query.exists(attributes)) + + fun notExists(attributes: List) = push(Query.notExists(attributes)) + + fun createdBefore(value: String) = push(Query.createdBefore(value)) + + fun createdAfter(value: String) = push(Query.createdAfter(value)) + + fun createdBetween(start: String, end: String) = push(Query.createdBetween(start, end)) + + fun updatedBefore(value: String) = push(Query.updatedBefore(value)) + + fun updatedAfter(value: String) = push(Query.updatedAfter(value)) + + fun updatedBetween(start: String, end: String) = push(Query.updatedBetween(start, end)) + + fun or(queries: List) = push(Query.or(queries)) + + fun and(queries: List) = push(Query.and(queries)) + + fun elemMatch(attribute: String, queries: List) = push(Query.elemMatch(attribute, queries)) + + fun relationship(attribute: String, queries: List) = push(Query.relationship(attribute, queries)) + + fun distanceEqual(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceEqual(attribute, values, distance, meters)) + + fun distanceNotEqual(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceNotEqual(attribute, values, distance, meters)) + + fun distanceGreaterThan(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceGreaterThan(attribute, values, distance, meters)) + + fun distanceLessThan(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceLessThan(attribute, values, distance, meters)) + + fun intersects(attribute: String, values: List) = push(Query.intersects(attribute, values)) + + fun notIntersects(attribute: String, values: List) = push(Query.notIntersects(attribute, values)) + + fun crosses(attribute: String, values: List) = push(Query.crosses(attribute, values)) + + fun notCrosses(attribute: String, values: List) = push(Query.notCrosses(attribute, values)) + + fun overlaps(attribute: String, values: List) = push(Query.overlaps(attribute, values)) + + fun notOverlaps(attribute: String, values: List) = push(Query.notOverlaps(attribute, values)) + + fun touches(attribute: String, values: List) = push(Query.touches(attribute, values)) + + fun notTouches(attribute: String, values: List) = push(Query.notTouches(attribute, values)) + + fun count(attribute: String = "*", alias: String = "") = push(Query.count(attribute, alias)) + + fun countDistinct(attribute: String, alias: String = "") = push(Query.countDistinct(attribute, alias)) + + fun sum(attribute: String, alias: String = "") = push(Query.sum(attribute, alias)) + + fun avg(attribute: String, alias: String = "") = push(Query.avg(attribute, alias)) + + fun min(attribute: String, alias: String = "") = push(Query.min(attribute, alias)) + + fun max(attribute: String, alias: String = "") = push(Query.max(attribute, alias)) + + fun stddev(attribute: String, alias: String = "") = push(Query.stddev(attribute, alias)) + + fun stddevPop(attribute: String, alias: String = "") = push(Query.stddevPop(attribute, alias)) + + fun stddevSamp(attribute: String, alias: String = "") = push(Query.stddevSamp(attribute, alias)) + + fun variance(attribute: String, alias: String = "") = push(Query.variance(attribute, alias)) + + fun varPop(attribute: String, alias: String = "") = push(Query.varPop(attribute, alias)) + + fun varSamp(attribute: String, alias: String = "") = push(Query.varSamp(attribute, alias)) + + fun bitAnd(attribute: String, alias: String = "") = push(Query.bitAnd(attribute, alias)) + + fun bitOr(attribute: String, alias: String = "") = push(Query.bitOr(attribute, alias)) + + fun bitXor(attribute: String, alias: String = "") = push(Query.bitXor(attribute, alias)) + + fun groupBy(attributes: List) = push(Query.groupBy(attributes)) + + fun having(queries: List) = push(Query.having(queries)) + + fun distinct() = push(Query.distinct()) + + fun join(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.join(table, left, right, operator, alias)) + + fun join(table: String, on: List) = push(Query.join(table, on)) + + fun join(table: String, alias: String, on: List) = push(Query.join(table, alias, on)) + + fun leftJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.leftJoin(table, left, right, operator, alias)) + + fun leftJoin(table: String, on: List) = push(Query.leftJoin(table, on)) + + fun leftJoin(table: String, alias: String, on: List) = push(Query.leftJoin(table, alias, on)) + + fun rightJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.rightJoin(table, left, right, operator, alias)) + + fun rightJoin(table: String, on: List) = push(Query.rightJoin(table, on)) + + fun rightJoin(table: String, alias: String, on: List) = push(Query.rightJoin(table, alias, on)) + + fun fullOuterJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.fullOuterJoin(table, left, right, operator, alias)) + + fun fullOuterJoin(table: String, on: List) = push(Query.fullOuterJoin(table, on)) + + fun fullOuterJoin(table: String, alias: String, on: List) = push(Query.fullOuterJoin(table, alias, on)) + + fun crossJoin(table: String, alias: String = "") = push(Query.crossJoin(table, alias)) + + fun vectorDot(attribute: String, vector: List) = push(Query.vectorDot(attribute, vector)) + + fun vectorCosine(attribute: String, vector: List) = push(Query.vectorCosine(attribute, vector)) + + fun vectorEuclidean(attribute: String, vector: List) = push(Query.vectorEuclidean(attribute, vector)) + + fun covers(attribute: String, values: List) = push(Query.covers(attribute, values)) + + fun notCovers(attribute: String, values: List) = push(Query.notCovers(attribute, values)) + + fun spatialEquals(attribute: String, values: List) = push(Query.spatialEquals(attribute, values)) + + fun notSpatialEquals(attribute: String, values: List) = push(Query.notSpatialEquals(attribute, values)) + + fun page(page: Int, perPage: Int = 25) = push(Query.page(page, perPage)) + + private fun push(query: String) = apply { queries.add(query) } + + private fun push(query: List) = apply { queries.addAll(query) } + } } diff --git a/library/src/main/java/io/appwrite/Topic.kt b/library/src/main/java/io/appwrite/Topic.kt index 8945da0e..614dca39 100644 --- a/library/src/main/java/io/appwrite/Topic.kt +++ b/library/src/main/java/io/appwrite/Topic.kt @@ -2,7 +2,7 @@ package io.appwrite /** * Validate the levels passed to `path()`: a non-empty list of non-empty strings, none holding - * the level separator `/` or the wildcards `+` and `#` (use `any()` and `all()` for those). + * the level separator `/` or the MQTT wildcards `+` and `#`. */ private fun levelsOf(levels: List): List { require(levels.isNotEmpty()) { "path() needs at least one level" } @@ -12,10 +12,9 @@ private fun levelsOf(levels: List): List { val parts = level.split("/").joinToString(", ") { "'$it'" } "\"$level\" contains \"/\", split it: path([$parts])" } - require(level != "+") { "\"+\" is reserved, use any()" } - require(!level.contains("+")) { "\"$level\" contains \"+\", use any() for wildcards" } - require(level != "#") { "\"#\" is reserved, use all()" } - require(!level.contains("#")) { "\"$level\" contains \"#\", use all() for wildcards" } + require(!level.contains("+")) { "\"$level\" contains \"+\", not allowed in a topic" } + require(!level.contains("#")) { "\"$level\" contains \"#\", not allowed in a topic" } + require(!level.contains('\u0000')) { "level at index $index contains a NUL character" } } return levels.toList() } @@ -25,13 +24,9 @@ private fun levelsOf(levels: List): List { * * ``` * Topic.path(listOf("user", userId, "notification")) // "user/123/notification" - * Topic.path(listOf("user")).any().path(listOf("notification")) // "user/+/notification" - * Topic.path(listOf("org")).any().path(listOf("logs")).all() // "org/+/logs/#" + * Topic.path(listOf("org", orgId)).path(listOf("logs")) // "org/42/logs" * ``` * - * `any()` matches exactly one level and `all()` everything below, at any depth (including the - * parent itself). `all()` ends the topic, so it returns a [ResolvedTopic]. - * * @throws IllegalArgumentException from `path()` when a level is empty or holds `/`, `+` or `#`. */ class Topic private constructor( @@ -40,29 +35,10 @@ class Topic private constructor( /** Append the given levels. */ fun path(levels: List): Topic = Topic(this.levels + levelsOf(levels)) - /** Append a single-level wildcard (`+`): matches exactly one level. */ - fun any(): Topic = Topic(levels + "+") - - /** Append the multi-level wildcard (`#`) and end the topic. */ - fun all(): ResolvedTopic = ResolvedTopic(levels + "#") - override fun toString(): String = levels.joinToString("/") companion object { /** Start a topic with the given levels, e.g. `Topic.path(listOf("user", userId))`. */ fun path(levels: List): Topic = Topic(levelsOf(levels)) - - /** Start a topic with a single-level wildcard (`+`). */ - fun any(): Topic = Topic(listOf("+")) - - /** The multi-level wildcard alone (`#`): matches every topic. */ - fun all(): ResolvedTopic = ResolvedTopic(listOf("#")) } } - -/** A topic ended by `all()`: nothing can be appended after the `#` wildcard. */ -class ResolvedTopic internal constructor( - private val levels: List, -) { - override fun toString(): String = levels.joinToString("/") -} diff --git a/library/src/main/java/io/appwrite/enums/ExecutionTrigger.kt b/library/src/main/java/io/appwrite/enums/ExecutionTrigger.kt index 3ba84bb5..5cb01125 100644 --- a/library/src/main/java/io/appwrite/enums/ExecutionTrigger.kt +++ b/library/src/main/java/io/appwrite/enums/ExecutionTrigger.kt @@ -8,7 +8,9 @@ enum class ExecutionTrigger(val value: String) { @SerializedName("schedule") SCHEDULE("schedule"), @SerializedName("event") - EVENT("event"); + EVENT("event"), + @SerializedName("email") + EMAIL("email"); override fun toString() = value } diff --git a/library/src/main/java/io/appwrite/models/Account.kt b/library/src/main/java/io/appwrite/models/Account.kt index 38865ff8..2d461144 100644 --- a/library/src/main/java/io/appwrite/models/Account.kt +++ b/library/src/main/java/io/appwrite/models/Account.kt @@ -237,28 +237,28 @@ data class Account( map: Map, nestedType: Class ) = Account( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - name = map["name"] as String, - registration = map["registration"] as String, - status = map["status"] as Boolean, - labels = map["labels"] as List, - passwordUpdate = map["passwordUpdate"] as String, - email = map["email"] as String, - phone = map["phone"] as String, - emailVerification = map["emailVerification"] as Boolean, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + registration = (if (map.containsKey("registration")) map["registration"] else "") as String, + status = (if (map.containsKey("status")) map["status"] else true) as Boolean, + labels = (if (map.containsKey("labels")) map["labels"] else listOf()) as List, + passwordUpdate = (if (map.containsKey("passwordUpdate")) map["passwordUpdate"] else "") as String, + email = (if (map.containsKey("email")) map["email"] else "") as String, + phone = (if (map.containsKey("phone")) map["phone"] else "") as String, + emailVerification = (if (map.containsKey("emailVerification")) map["emailVerification"] else false) as Boolean, emailCanonical = map["emailCanonical"] as? String, emailIsFree = map["emailIsFree"] as? Boolean, emailIsDisposable = map["emailIsDisposable"] as? Boolean, emailIsCorporate = map["emailIsCorporate"] as? Boolean, emailIsCanonical = map["emailIsCanonical"] as? Boolean, passwordPwned = map["passwordPwned"] as? Boolean, - phoneVerification = map["phoneVerification"] as Boolean, - mfa = map["mfa"] as Boolean, + phoneVerification = (if (map.containsKey("phoneVerification")) map["phoneVerification"] else false) as Boolean, + mfa = (if (map.containsKey("mfa")) map["mfa"] else false) as Boolean, prefs = Preferences.from(map = map["prefs"] as Map, nestedType), - targets = (map["targets"] as List>).map { Target.from(map = it) }, - accessedAt = map["accessedAt"] as String, + targets = ((if (map.containsKey("targets")) map["targets"] else listOf()) as List>).map { Target.from(map = it) }, + accessedAt = (if (map.containsKey("accessedAt")) map["accessedAt"] else "") as String, impersonator = map["impersonator"] as? Boolean, impersonatorUserId = map["impersonatorUserId"] as? String, ) diff --git a/library/src/main/java/io/appwrite/models/AlgoArgon2.kt b/library/src/main/java/io/appwrite/models/AlgoArgon2.kt index 39773328..1d26dfd3 100644 --- a/library/src/main/java/io/appwrite/models/AlgoArgon2.kt +++ b/library/src/main/java/io/appwrite/models/AlgoArgon2.kt @@ -44,10 +44,10 @@ data class AlgoArgon2( fun from( map: Map, ) = AlgoArgon2( - type = map["type"] as String, - memoryCost = (map["memoryCost"] as Number).toLong(), - timeCost = (map["timeCost"] as Number).toLong(), - threads = (map["threads"] as Number).toLong(), + type = (if (map.containsKey("type")) map["type"] else "argon2") as String, + memoryCost = ((if (map.containsKey("memoryCost")) map["memoryCost"] else 0) as Number).toLong(), + timeCost = ((if (map.containsKey("timeCost")) map["timeCost"] else 0) as Number).toLong(), + threads = ((if (map.containsKey("threads")) map["threads"] else 0) as Number).toLong(), ) } } diff --git a/library/src/main/java/io/appwrite/models/AlgoBcrypt.kt b/library/src/main/java/io/appwrite/models/AlgoBcrypt.kt index 38317807..2a99dad7 100644 --- a/library/src/main/java/io/appwrite/models/AlgoBcrypt.kt +++ b/library/src/main/java/io/appwrite/models/AlgoBcrypt.kt @@ -23,7 +23,7 @@ data class AlgoBcrypt( fun from( map: Map, ) = AlgoBcrypt( - type = map["type"] as String, + type = (if (map.containsKey("type")) map["type"] else "bcrypt") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/AlgoMd5.kt b/library/src/main/java/io/appwrite/models/AlgoMd5.kt index e46eb99b..d683ea70 100644 --- a/library/src/main/java/io/appwrite/models/AlgoMd5.kt +++ b/library/src/main/java/io/appwrite/models/AlgoMd5.kt @@ -23,7 +23,7 @@ data class AlgoMd5( fun from( map: Map, ) = AlgoMd5( - type = map["type"] as String, + type = (if (map.containsKey("type")) map["type"] else "md5") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/AlgoPhpass.kt b/library/src/main/java/io/appwrite/models/AlgoPhpass.kt index d457a81d..3eba6cad 100644 --- a/library/src/main/java/io/appwrite/models/AlgoPhpass.kt +++ b/library/src/main/java/io/appwrite/models/AlgoPhpass.kt @@ -23,7 +23,7 @@ data class AlgoPhpass( fun from( map: Map, ) = AlgoPhpass( - type = map["type"] as String, + type = (if (map.containsKey("type")) map["type"] else "phpass") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/AlgoScrypt.kt b/library/src/main/java/io/appwrite/models/AlgoScrypt.kt index 92147046..789061fe 100644 --- a/library/src/main/java/io/appwrite/models/AlgoScrypt.kt +++ b/library/src/main/java/io/appwrite/models/AlgoScrypt.kt @@ -51,11 +51,11 @@ data class AlgoScrypt( fun from( map: Map, ) = AlgoScrypt( - type = map["type"] as String, - costCpu = (map["costCpu"] as Number).toLong(), - costMemory = (map["costMemory"] as Number).toLong(), - costParallel = (map["costParallel"] as Number).toLong(), - length = (map["length"] as Number).toLong(), + type = (if (map.containsKey("type")) map["type"] else "scrypt") as String, + costCpu = ((if (map.containsKey("costCpu")) map["costCpu"] else 8) as Number).toLong(), + costMemory = ((if (map.containsKey("costMemory")) map["costMemory"] else 14) as Number).toLong(), + costParallel = ((if (map.containsKey("costParallel")) map["costParallel"] else 1) as Number).toLong(), + length = ((if (map.containsKey("length")) map["length"] else 64) as Number).toLong(), ) } } diff --git a/library/src/main/java/io/appwrite/models/AlgoScryptModified.kt b/library/src/main/java/io/appwrite/models/AlgoScryptModified.kt index 539828ac..ff306fa4 100644 --- a/library/src/main/java/io/appwrite/models/AlgoScryptModified.kt +++ b/library/src/main/java/io/appwrite/models/AlgoScryptModified.kt @@ -44,10 +44,10 @@ data class AlgoScryptModified( fun from( map: Map, ) = AlgoScryptModified( - type = map["type"] as String, - salt = map["salt"] as String, - saltSeparator = map["saltSeparator"] as String, - signerKey = map["signerKey"] as String, + type = (if (map.containsKey("type")) map["type"] else "scryptMod") as String, + salt = (if (map.containsKey("salt")) map["salt"] else "") as String, + saltSeparator = (if (map.containsKey("saltSeparator")) map["saltSeparator"] else "") as String, + signerKey = (if (map.containsKey("signerKey")) map["signerKey"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/AlgoSha.kt b/library/src/main/java/io/appwrite/models/AlgoSha.kt index 45fb0104..5c52f087 100644 --- a/library/src/main/java/io/appwrite/models/AlgoSha.kt +++ b/library/src/main/java/io/appwrite/models/AlgoSha.kt @@ -23,7 +23,7 @@ data class AlgoSha( fun from( map: Map, ) = AlgoSha( - type = map["type"] as String, + type = (if (map.containsKey("type")) map["type"] else "sha") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/App.kt b/library/src/main/java/io/appwrite/models/App.kt index f4a715c8..9125cdcf 100644 --- a/library/src/main/java/io/appwrite/models/App.kt +++ b/library/src/main/java/io/appwrite/models/App.kt @@ -198,32 +198,32 @@ data class App( fun from( map: Map, ) = App( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - name = map["name"] as String, - description = map["description"] as String, - clientUri = map["clientUri"] as String, - logoUri = map["logoUri"] as String, - privacyPolicyUrl = map["privacyPolicyUrl"] as String, - termsUrl = map["termsUrl"] as String, - contacts = map["contacts"] as List, - tagline = map["tagline"] as String, - tags = map["tags"] as List, - labels = map["labels"] as List, - images = map["images"] as List, - supportUrl = map["supportUrl"] as String, - dataDeletionUrl = map["dataDeletionUrl"] as String, - redirectUris = map["redirectUris"] as List, - postLogoutRedirectUris = map["postLogoutRedirectUris"] as List, - enabled = map["enabled"] as Boolean, - type = map["type"] as String, - deviceFlow = map["deviceFlow"] as Boolean, - teamId = map["teamId"] as String, - userId = map["userId"] as String, - installationScopes = map["installationScopes"] as List, - installationRedirectUrl = map["installationRedirectUrl"] as String, - secrets = (map["secrets"] as List>).map { AppSecret.from(map = it) }, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + description = (if (map.containsKey("description")) map["description"] else "") as String, + clientUri = (if (map.containsKey("clientUri")) map["clientUri"] else "") as String, + logoUri = (if (map.containsKey("logoUri")) map["logoUri"] else "") as String, + privacyPolicyUrl = (if (map.containsKey("privacyPolicyUrl")) map["privacyPolicyUrl"] else "") as String, + termsUrl = (if (map.containsKey("termsUrl")) map["termsUrl"] else "") as String, + contacts = (if (map.containsKey("contacts")) map["contacts"] else listOf()) as List, + tagline = (if (map.containsKey("tagline")) map["tagline"] else "") as String, + tags = (if (map.containsKey("tags")) map["tags"] else listOf()) as List, + labels = (if (map.containsKey("labels")) map["labels"] else listOf()) as List, + images = (if (map.containsKey("images")) map["images"] else listOf()) as List, + supportUrl = (if (map.containsKey("supportUrl")) map["supportUrl"] else "") as String, + dataDeletionUrl = (if (map.containsKey("dataDeletionUrl")) map["dataDeletionUrl"] else "") as String, + redirectUris = (if (map.containsKey("redirectUris")) map["redirectUris"] else listOf()) as List, + postLogoutRedirectUris = (if (map.containsKey("postLogoutRedirectUris")) map["postLogoutRedirectUris"] else listOf()) as List, + enabled = (if (map.containsKey("enabled")) map["enabled"] else true) as Boolean, + type = (if (map.containsKey("type")) map["type"] else "confidential") as String, + deviceFlow = (if (map.containsKey("deviceFlow")) map["deviceFlow"] else false) as Boolean, + teamId = (if (map.containsKey("teamId")) map["teamId"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + installationScopes = (if (map.containsKey("installationScopes")) map["installationScopes"] else listOf()) as List, + installationRedirectUrl = (if (map.containsKey("installationRedirectUrl")) map["installationRedirectUrl"] else "") as String, + secrets = ((if (map.containsKey("secrets")) map["secrets"] else listOf()) as List>).map { AppSecret.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/AppInstallation.kt b/library/src/main/java/io/appwrite/models/AppInstallation.kt index 46bf9075..3c6d1f68 100644 --- a/library/src/main/java/io/appwrite/models/AppInstallation.kt +++ b/library/src/main/java/io/appwrite/models/AppInstallation.kt @@ -86,15 +86,15 @@ data class AppInstallation( fun from( map: Map, ) = AppInstallation( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - appId = map["appId"] as String, - teamId = map["teamId"] as String, - scopes = map["scopes"] as List, - authorizationDetails = map["authorizationDetails"] as List>, - createdById = map["createdById"] as String, - createdByName = map["createdByName"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + teamId = (if (map.containsKey("teamId")) map["teamId"] else "") as String, + scopes = (if (map.containsKey("scopes")) map["scopes"] else listOf()) as List, + authorizationDetails = (if (map.containsKey("authorizationDetails")) map["authorizationDetails"] else listOf()) as List>, + createdById = (if (map.containsKey("createdById")) map["createdById"] else "") as String, + createdByName = (if (map.containsKey("createdByName")) map["createdByName"] else "") as String, lastAccessedAt = map["lastAccessedAt"] as? String, ) } diff --git a/library/src/main/java/io/appwrite/models/AppInstallationList.kt b/library/src/main/java/io/appwrite/models/AppInstallationList.kt index f7434cf2..8006d98c 100644 --- a/library/src/main/java/io/appwrite/models/AppInstallationList.kt +++ b/library/src/main/java/io/appwrite/models/AppInstallationList.kt @@ -30,8 +30,8 @@ data class AppInstallationList( fun from( map: Map, ) = AppInstallationList( - total = (map["total"] as Number).toLong(), - installations = (map["installations"] as List>).map { AppInstallation.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + installations = ((if (map.containsKey("installations")) map["installations"] else listOf()) as List>).map { AppInstallation.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/AppKey.kt b/library/src/main/java/io/appwrite/models/AppKey.kt index f59e8d50..7348cd3f 100644 --- a/library/src/main/java/io/appwrite/models/AppKey.kt +++ b/library/src/main/java/io/appwrite/models/AppKey.kt @@ -79,14 +79,14 @@ data class AppKey( fun from( map: Map, ) = AppKey( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - appId = map["appId"] as String, - secret = map["secret"] as String, - hint = map["hint"] as String, - createdById = map["createdById"] as String, - createdByName = map["createdByName"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + hint = (if (map.containsKey("hint")) map["hint"] else "") as String, + createdById = (if (map.containsKey("createdById")) map["createdById"] else "") as String, + createdByName = (if (map.containsKey("createdByName")) map["createdByName"] else "") as String, lastAccessedAt = map["lastAccessedAt"] as? String, ) } diff --git a/library/src/main/java/io/appwrite/models/AppKeyList.kt b/library/src/main/java/io/appwrite/models/AppKeyList.kt index ea4b1cca..854af040 100644 --- a/library/src/main/java/io/appwrite/models/AppKeyList.kt +++ b/library/src/main/java/io/appwrite/models/AppKeyList.kt @@ -30,8 +30,8 @@ data class AppKeyList( fun from( map: Map, ) = AppKeyList( - total = (map["total"] as Number).toLong(), - keys = (map["keys"] as List>).map { AppKey.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + keys = ((if (map.containsKey("keys")) map["keys"] else listOf()) as List>).map { AppKey.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/AppScope.kt b/library/src/main/java/io/appwrite/models/AppScope.kt index 4cc7fe95..dbacafc7 100644 --- a/library/src/main/java/io/appwrite/models/AppScope.kt +++ b/library/src/main/java/io/appwrite/models/AppScope.kt @@ -51,11 +51,11 @@ data class AppScope( fun from( map: Map, ) = AppScope( - value = map["value"] as String, - description = map["description"] as String, - type = map["type"] as String, - category = map["category"] as String, - deprecated = map["deprecated"] as Boolean, + value = (if (map.containsKey("value")) map["value"] else "") as String, + description = (if (map.containsKey("description")) map["description"] else "") as String, + type = (if (map.containsKey("type")) map["type"] else "") as String, + category = (if (map.containsKey("category")) map["category"] else "") as String, + deprecated = (if (map.containsKey("deprecated")) map["deprecated"] else false) as Boolean, ) } } diff --git a/library/src/main/java/io/appwrite/models/AppScopeList.kt b/library/src/main/java/io/appwrite/models/AppScopeList.kt index cbcf91fb..76bc7369 100644 --- a/library/src/main/java/io/appwrite/models/AppScopeList.kt +++ b/library/src/main/java/io/appwrite/models/AppScopeList.kt @@ -30,8 +30,8 @@ data class AppScopeList( fun from( map: Map, ) = AppScopeList( - total = (map["total"] as Number).toLong(), - scopes = (map["scopes"] as List>).map { AppScope.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + scopes = ((if (map.containsKey("scopes")) map["scopes"] else listOf()) as List>).map { AppScope.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/AppSecret.kt b/library/src/main/java/io/appwrite/models/AppSecret.kt index 0349ffbd..b7d70922 100644 --- a/library/src/main/java/io/appwrite/models/AppSecret.kt +++ b/library/src/main/java/io/appwrite/models/AppSecret.kt @@ -79,14 +79,14 @@ data class AppSecret( fun from( map: Map, ) = AppSecret( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - appId = map["appId"] as String, - secret = map["secret"] as String, - hint = map["hint"] as String, - createdById = map["createdById"] as String, - createdByName = map["createdByName"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + hint = (if (map.containsKey("hint")) map["hint"] else "") as String, + createdById = (if (map.containsKey("createdById")) map["createdById"] else "") as String, + createdByName = (if (map.containsKey("createdByName")) map["createdByName"] else "") as String, lastAccessedAt = map["lastAccessedAt"] as? String, ) } diff --git a/library/src/main/java/io/appwrite/models/AppSecretList.kt b/library/src/main/java/io/appwrite/models/AppSecretList.kt index 04d2ebb7..1f2946c4 100644 --- a/library/src/main/java/io/appwrite/models/AppSecretList.kt +++ b/library/src/main/java/io/appwrite/models/AppSecretList.kt @@ -30,8 +30,8 @@ data class AppSecretList( fun from( map: Map, ) = AppSecretList( - total = (map["total"] as Number).toLong(), - secrets = (map["secrets"] as List>).map { AppSecret.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + secrets = ((if (map.containsKey("secrets")) map["secrets"] else listOf()) as List>).map { AppSecret.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/AppSecretPlaintext.kt b/library/src/main/java/io/appwrite/models/AppSecretPlaintext.kt index 098b7f19..7464598c 100644 --- a/library/src/main/java/io/appwrite/models/AppSecretPlaintext.kt +++ b/library/src/main/java/io/appwrite/models/AppSecretPlaintext.kt @@ -79,14 +79,14 @@ data class AppSecretPlaintext( fun from( map: Map, ) = AppSecretPlaintext( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - appId = map["appId"] as String, - secret = map["secret"] as String, - hint = map["hint"] as String, - createdById = map["createdById"] as String, - createdByName = map["createdByName"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + hint = (if (map.containsKey("hint")) map["hint"] else "") as String, + createdById = (if (map.containsKey("createdById")) map["createdById"] else "") as String, + createdByName = (if (map.containsKey("createdByName")) map["createdByName"] else "") as String, lastAccessedAt = map["lastAccessedAt"] as? String, ) } diff --git a/library/src/main/java/io/appwrite/models/AppsList.kt b/library/src/main/java/io/appwrite/models/AppsList.kt index 5dcff821..0ad546f1 100644 --- a/library/src/main/java/io/appwrite/models/AppsList.kt +++ b/library/src/main/java/io/appwrite/models/AppsList.kt @@ -30,8 +30,8 @@ data class AppsList( fun from( map: Map, ) = AppsList( - total = (map["total"] as Number).toLong(), - apps = (map["apps"] as List>).map { App.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + apps = ((if (map.containsKey("apps")) map["apps"] else listOf()) as List>).map { App.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Continent.kt b/library/src/main/java/io/appwrite/models/Continent.kt index 0adbb5fa..e8599a03 100644 --- a/library/src/main/java/io/appwrite/models/Continent.kt +++ b/library/src/main/java/io/appwrite/models/Continent.kt @@ -30,8 +30,8 @@ data class Continent( fun from( map: Map, ) = Continent( - name = map["name"] as String, - code = map["code"] as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + code = (if (map.containsKey("code")) map["code"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/ContinentList.kt b/library/src/main/java/io/appwrite/models/ContinentList.kt index 53f95f2a..0a1f4f34 100644 --- a/library/src/main/java/io/appwrite/models/ContinentList.kt +++ b/library/src/main/java/io/appwrite/models/ContinentList.kt @@ -30,8 +30,8 @@ data class ContinentList( fun from( map: Map, ) = ContinentList( - total = (map["total"] as Number).toLong(), - continents = (map["continents"] as List>).map { Continent.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + continents = ((if (map.containsKey("continents")) map["continents"] else listOf()) as List>).map { Continent.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Country.kt b/library/src/main/java/io/appwrite/models/Country.kt index 7b104f0b..1c3065ea 100644 --- a/library/src/main/java/io/appwrite/models/Country.kt +++ b/library/src/main/java/io/appwrite/models/Country.kt @@ -30,8 +30,8 @@ data class Country( fun from( map: Map, ) = Country( - name = map["name"] as String, - code = map["code"] as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + code = (if (map.containsKey("code")) map["code"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/CountryList.kt b/library/src/main/java/io/appwrite/models/CountryList.kt index 322cdaf8..629bae23 100644 --- a/library/src/main/java/io/appwrite/models/CountryList.kt +++ b/library/src/main/java/io/appwrite/models/CountryList.kt @@ -30,8 +30,8 @@ data class CountryList( fun from( map: Map, ) = CountryList( - total = (map["total"] as Number).toLong(), - countries = (map["countries"] as List>).map { Country.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + countries = ((if (map.containsKey("countries")) map["countries"] else listOf()) as List>).map { Country.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Currency.kt b/library/src/main/java/io/appwrite/models/Currency.kt index 95ff13ac..d78a90ca 100644 --- a/library/src/main/java/io/appwrite/models/Currency.kt +++ b/library/src/main/java/io/appwrite/models/Currency.kt @@ -65,13 +65,13 @@ data class Currency( fun from( map: Map, ) = Currency( - symbol = map["symbol"] as String, - name = map["name"] as String, - symbolNative = map["symbolNative"] as String, - decimalDigits = (map["decimalDigits"] as Number).toLong(), - rounding = (map["rounding"] as Number).toDouble(), - code = map["code"] as String, - namePlural = map["namePlural"] as String, + symbol = (if (map.containsKey("symbol")) map["symbol"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + symbolNative = (if (map.containsKey("symbolNative")) map["symbolNative"] else "") as String, + decimalDigits = ((if (map.containsKey("decimalDigits")) map["decimalDigits"] else 0) as Number).toLong(), + rounding = ((if (map.containsKey("rounding")) map["rounding"] else 0.0) as Number).toDouble(), + code = (if (map.containsKey("code")) map["code"] else "") as String, + namePlural = (if (map.containsKey("namePlural")) map["namePlural"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/CurrencyList.kt b/library/src/main/java/io/appwrite/models/CurrencyList.kt index 8082a2f1..5928c9a1 100644 --- a/library/src/main/java/io/appwrite/models/CurrencyList.kt +++ b/library/src/main/java/io/appwrite/models/CurrencyList.kt @@ -30,8 +30,8 @@ data class CurrencyList( fun from( map: Map, ) = CurrencyList( - total = (map["total"] as Number).toLong(), - currencies = (map["currencies"] as List>).map { Currency.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + currencies = ((if (map.containsKey("currencies")) map["currencies"] else listOf()) as List>).map { Currency.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Document.kt b/library/src/main/java/io/appwrite/models/Document.kt index 20e803e1..a19dd7fa 100644 --- a/library/src/main/java/io/appwrite/models/Document.kt +++ b/library/src/main/java/io/appwrite/models/Document.kt @@ -92,13 +92,13 @@ data class Document( map: Map, nestedType: Class ) = Document( - id = map["\$id"] as String, - sequence = map["\$sequence"] as String, - collectionId = map["\$collectionId"] as String, - databaseId = map["\$databaseId"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - permissions = map["\$permissions"] as List, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + sequence = (if (map.containsKey("\$sequence")) map["\$sequence"] else "") as String, + collectionId = (if (map.containsKey("\$collectionId")) map["\$collectionId"] else "") as String, + databaseId = (if (map.containsKey("\$databaseId")) map["\$databaseId"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + permissions = (if (map.containsKey("\$permissions")) map["\$permissions"] else listOf()) as List, data = map["data"]?.jsonCast(to = nestedType) ?: map.jsonCast(to = nestedType) ) } diff --git a/library/src/main/java/io/appwrite/models/DocumentList.kt b/library/src/main/java/io/appwrite/models/DocumentList.kt index d8741eab..59203167 100644 --- a/library/src/main/java/io/appwrite/models/DocumentList.kt +++ b/library/src/main/java/io/appwrite/models/DocumentList.kt @@ -39,8 +39,8 @@ data class DocumentList( map: Map, nestedType: Class ) = DocumentList( - total = (map["total"] as Number).toLong(), - documents = (map["documents"] as List>).map { Document.from(map = it, nestedType) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + documents = ((if (map.containsKey("documents")) map["documents"] else listOf()) as List>).map { Document.from(map = it, nestedType) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Execution.kt b/library/src/main/java/io/appwrite/models/Execution.kt index c698d9c6..8b364141 100644 --- a/library/src/main/java/io/appwrite/models/Execution.kt +++ b/library/src/main/java/io/appwrite/models/Execution.kt @@ -53,7 +53,7 @@ data class Execution( val deploymentId: String, /** - * The trigger that caused the resource to execute. Possible values can be: `http`, `schedule`, or `event`. + * The trigger that caused the resource to execute. Possible values can be: `http`, `schedule`, `event`, or `email`. */ @SerializedName("trigger") val trigger: ExecutionTrigger, @@ -152,24 +152,24 @@ data class Execution( fun from( map: Map, ) = Execution( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - permissions = map["\$permissions"] as List, - resourceId = map["resourceId"] as String, - resourceType = ExecutionResourceType.values().find { it.value == map["resourceType"] as String }!!, - deploymentId = map["deploymentId"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + permissions = (if (map.containsKey("\$permissions")) map["\$permissions"] else listOf()) as List, + resourceId = (if (map.containsKey("resourceId")) map["resourceId"] else "") as String, + resourceType = ExecutionResourceType.values().find { it.value == (if (map.containsKey("resourceType")) map["resourceType"] else "functions") as String }!!, + deploymentId = (if (map.containsKey("deploymentId")) map["deploymentId"] else "") as String, trigger = ExecutionTrigger.values().find { it.value == map["trigger"] as String }!!, status = ExecutionStatus.values().find { it.value == map["status"] as String }!!, - requestMethod = map["requestMethod"] as String, - requestPath = map["requestPath"] as String, - requestHeaders = (map["requestHeaders"] as List>).map { Headers.from(map = it) }, - responseStatusCode = (map["responseStatusCode"] as Number).toLong(), - responseBody = map["responseBody"] as String, - responseHeaders = (map["responseHeaders"] as List>).map { Headers.from(map = it) }, - logs = map["logs"] as String, - errors = map["errors"] as String, - duration = (map["duration"] as Number).toDouble(), + requestMethod = (if (map.containsKey("requestMethod")) map["requestMethod"] else "") as String, + requestPath = (if (map.containsKey("requestPath")) map["requestPath"] else "") as String, + requestHeaders = ((if (map.containsKey("requestHeaders")) map["requestHeaders"] else listOf()) as List>).map { Headers.from(map = it) }, + responseStatusCode = ((if (map.containsKey("responseStatusCode")) map["responseStatusCode"] else 0) as Number).toLong(), + responseBody = (if (map.containsKey("responseBody")) map["responseBody"] else "") as String, + responseHeaders = ((if (map.containsKey("responseHeaders")) map["responseHeaders"] else listOf()) as List>).map { Headers.from(map = it) }, + logs = (if (map.containsKey("logs")) map["logs"] else "") as String, + errors = (if (map.containsKey("errors")) map["errors"] else "") as String, + duration = ((if (map.containsKey("duration")) map["duration"] else 0.0) as Number).toDouble(), scheduledAt = map["scheduledAt"] as? String, ) } diff --git a/library/src/main/java/io/appwrite/models/ExecutionList.kt b/library/src/main/java/io/appwrite/models/ExecutionList.kt index 4eac1d89..0eba2816 100644 --- a/library/src/main/java/io/appwrite/models/ExecutionList.kt +++ b/library/src/main/java/io/appwrite/models/ExecutionList.kt @@ -30,8 +30,8 @@ data class ExecutionList( fun from( map: Map, ) = ExecutionList( - total = (map["total"] as Number).toLong(), - executions = (map["executions"] as List>).map { Execution.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + executions = ((if (map.containsKey("executions")) map["executions"] else listOf()) as List>).map { Execution.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/File.kt b/library/src/main/java/io/appwrite/models/File.kt index d15a55b1..049bbae7 100644 --- a/library/src/main/java/io/appwrite/models/File.kt +++ b/library/src/main/java/io/appwrite/models/File.kt @@ -128,22 +128,22 @@ data class File( fun from( map: Map, ) = File( - id = map["\$id"] as String, - bucketId = map["bucketId"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - permissions = map["\$permissions"] as List, - name = map["name"] as String, - folder = map["folder"] as String, - key = map["key"] as String, - signature = map["signature"] as String, - mimeType = map["mimeType"] as String, - sizeOriginal = (map["sizeOriginal"] as Number).toLong(), - sizeActual = (map["sizeActual"] as Number).toLong(), - chunksTotal = (map["chunksTotal"] as Number).toLong(), - chunksUploaded = (map["chunksUploaded"] as Number).toLong(), - encryption = map["encryption"] as Boolean, - compression = map["compression"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + bucketId = (if (map.containsKey("bucketId")) map["bucketId"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + permissions = (if (map.containsKey("\$permissions")) map["\$permissions"] else listOf()) as List, + name = (if (map.containsKey("name")) map["name"] else "") as String, + folder = (if (map.containsKey("folder")) map["folder"] else "") as String, + key = (if (map.containsKey("key")) map["key"] else "") as String, + signature = (if (map.containsKey("signature")) map["signature"] else "") as String, + mimeType = (if (map.containsKey("mimeType")) map["mimeType"] else "") as String, + sizeOriginal = ((if (map.containsKey("sizeOriginal")) map["sizeOriginal"] else 0) as Number).toLong(), + sizeActual = ((if (map.containsKey("sizeActual")) map["sizeActual"] else 0) as Number).toLong(), + chunksTotal = ((if (map.containsKey("chunksTotal")) map["chunksTotal"] else 0) as Number).toLong(), + chunksUploaded = ((if (map.containsKey("chunksUploaded")) map["chunksUploaded"] else 0) as Number).toLong(), + encryption = (if (map.containsKey("encryption")) map["encryption"] else false) as Boolean, + compression = (if (map.containsKey("compression")) map["compression"] else "none") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/FileList.kt b/library/src/main/java/io/appwrite/models/FileList.kt index 7a462dc7..1a84a460 100644 --- a/library/src/main/java/io/appwrite/models/FileList.kt +++ b/library/src/main/java/io/appwrite/models/FileList.kt @@ -30,8 +30,8 @@ data class FileList( fun from( map: Map, ) = FileList( - total = (map["total"] as Number).toLong(), - files = (map["files"] as List>).map { File.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + files = ((if (map.containsKey("files")) map["files"] else listOf()) as List>).map { File.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Headers.kt b/library/src/main/java/io/appwrite/models/Headers.kt index 4fece1e4..d2c27770 100644 --- a/library/src/main/java/io/appwrite/models/Headers.kt +++ b/library/src/main/java/io/appwrite/models/Headers.kt @@ -30,8 +30,8 @@ data class Headers( fun from( map: Map, ) = Headers( - name = map["name"] as String, - value = map["value"] as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + value = (if (map.containsKey("value")) map["value"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Identity.kt b/library/src/main/java/io/appwrite/models/Identity.kt index 011c32e4..0269400f 100644 --- a/library/src/main/java/io/appwrite/models/Identity.kt +++ b/library/src/main/java/io/appwrite/models/Identity.kt @@ -93,17 +93,17 @@ data class Identity( fun from( map: Map, ) = Identity( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - userId = map["userId"] as String, - provider = map["provider"] as String, - providerUid = map["providerUid"] as String, - providerEmail = map["providerEmail"] as String, - providerAccessToken = map["providerAccessToken"] as String, - providerAccessTokenExpiry = map["providerAccessTokenExpiry"] as String, - providerRefreshToken = map["providerRefreshToken"] as String, - providerIdToken = map["providerIdToken"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + provider = (if (map.containsKey("provider")) map["provider"] else "") as String, + providerUid = (if (map.containsKey("providerUid")) map["providerUid"] else "") as String, + providerEmail = (if (map.containsKey("providerEmail")) map["providerEmail"] else "") as String, + providerAccessToken = (if (map.containsKey("providerAccessToken")) map["providerAccessToken"] else "") as String, + providerAccessTokenExpiry = (if (map.containsKey("providerAccessTokenExpiry")) map["providerAccessTokenExpiry"] else "") as String, + providerRefreshToken = (if (map.containsKey("providerRefreshToken")) map["providerRefreshToken"] else "") as String, + providerIdToken = (if (map.containsKey("providerIdToken")) map["providerIdToken"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/IdentityList.kt b/library/src/main/java/io/appwrite/models/IdentityList.kt index b92f4e6a..31e5b0ea 100644 --- a/library/src/main/java/io/appwrite/models/IdentityList.kt +++ b/library/src/main/java/io/appwrite/models/IdentityList.kt @@ -30,8 +30,8 @@ data class IdentityList( fun from( map: Map, ) = IdentityList( - total = (map["total"] as Number).toLong(), - identities = (map["identities"] as List>).map { Identity.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + identities = ((if (map.containsKey("identities")) map["identities"] else listOf()) as List>).map { Identity.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Jwt.kt b/library/src/main/java/io/appwrite/models/Jwt.kt index c188a853..7b27865e 100644 --- a/library/src/main/java/io/appwrite/models/Jwt.kt +++ b/library/src/main/java/io/appwrite/models/Jwt.kt @@ -23,7 +23,7 @@ data class Jwt( fun from( map: Map, ) = Jwt( - jwt = map["jwt"] as String, + jwt = (if (map.containsKey("jwt")) map["jwt"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Language.kt b/library/src/main/java/io/appwrite/models/Language.kt index be1f30e8..01eb45bd 100644 --- a/library/src/main/java/io/appwrite/models/Language.kt +++ b/library/src/main/java/io/appwrite/models/Language.kt @@ -37,9 +37,9 @@ data class Language( fun from( map: Map, ) = Language( - name = map["name"] as String, - code = map["code"] as String, - nativeName = map["nativeName"] as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + code = (if (map.containsKey("code")) map["code"] else "") as String, + nativeName = (if (map.containsKey("nativeName")) map["nativeName"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/LanguageList.kt b/library/src/main/java/io/appwrite/models/LanguageList.kt index e1b0e6e7..86f6aed4 100644 --- a/library/src/main/java/io/appwrite/models/LanguageList.kt +++ b/library/src/main/java/io/appwrite/models/LanguageList.kt @@ -30,8 +30,8 @@ data class LanguageList( fun from( map: Map, ) = LanguageList( - total = (map["total"] as Number).toLong(), - languages = (map["languages"] as List>).map { Language.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + languages = ((if (map.containsKey("languages")) map["languages"] else listOf()) as List>).map { Language.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Locale.kt b/library/src/main/java/io/appwrite/models/Locale.kt index 689de89c..dc6a1ad8 100644 --- a/library/src/main/java/io/appwrite/models/Locale.kt +++ b/library/src/main/java/io/appwrite/models/Locale.kt @@ -142,13 +142,13 @@ data class Locale( fun from( map: Map, ) = Locale( - ip = map["ip"] as String, - countryCode = map["countryCode"] as String, - country = map["country"] as String, - continentCode = map["continentCode"] as String, - continent = map["continent"] as String, - eu = map["eu"] as Boolean, - currency = map["currency"] as String, + ip = (if (map.containsKey("ip")) map["ip"] else "") as String, + countryCode = (if (map.containsKey("countryCode")) map["countryCode"] else "") as String, + country = (if (map.containsKey("country")) map["country"] else "") as String, + continentCode = (if (map.containsKey("continentCode")) map["continentCode"] else "") as String, + continent = (if (map.containsKey("continent")) map["continent"] else "") as String, + eu = (if (map.containsKey("eu")) map["eu"] else false) as Boolean, + currency = (if (map.containsKey("currency")) map["currency"] else "") as String, city = map["city"] as? String, timeZone = map["timeZone"] as? String, postalCode = map["postalCode"] as? String, diff --git a/library/src/main/java/io/appwrite/models/LocaleCode.kt b/library/src/main/java/io/appwrite/models/LocaleCode.kt index 602d9ed9..3bee5288 100644 --- a/library/src/main/java/io/appwrite/models/LocaleCode.kt +++ b/library/src/main/java/io/appwrite/models/LocaleCode.kt @@ -30,8 +30,8 @@ data class LocaleCode( fun from( map: Map, ) = LocaleCode( - code = map["code"] as String, - name = map["name"] as String, + code = (if (map.containsKey("code")) map["code"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/LocaleCodeList.kt b/library/src/main/java/io/appwrite/models/LocaleCodeList.kt index e0c568a7..86cb808f 100644 --- a/library/src/main/java/io/appwrite/models/LocaleCodeList.kt +++ b/library/src/main/java/io/appwrite/models/LocaleCodeList.kt @@ -30,8 +30,8 @@ data class LocaleCodeList( fun from( map: Map, ) = LocaleCodeList( - total = (map["total"] as Number).toLong(), - localeCodes = (map["localeCodes"] as List>).map { LocaleCode.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + localeCodes = ((if (map.containsKey("localeCodes")) map["localeCodes"] else listOf()) as List>).map { LocaleCode.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Membership.kt b/library/src/main/java/io/appwrite/models/Membership.kt index 7d5b963b..3d0c37b9 100644 --- a/library/src/main/java/io/appwrite/models/Membership.kt +++ b/library/src/main/java/io/appwrite/models/Membership.kt @@ -121,21 +121,21 @@ data class Membership( fun from( map: Map, ) = Membership( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - userId = map["userId"] as String, - userName = map["userName"] as String, - userEmail = map["userEmail"] as String, - userPhone = map["userPhone"] as String, - teamId = map["teamId"] as String, - teamName = map["teamName"] as String, - invited = map["invited"] as String, - joined = map["joined"] as String, - confirm = map["confirm"] as Boolean, - mfa = map["mfa"] as Boolean, - userAccessedAt = map["userAccessedAt"] as String, - roles = map["roles"] as List, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + userName = (if (map.containsKey("userName")) map["userName"] else "") as String, + userEmail = (if (map.containsKey("userEmail")) map["userEmail"] else "") as String, + userPhone = (if (map.containsKey("userPhone")) map["userPhone"] else "") as String, + teamId = (if (map.containsKey("teamId")) map["teamId"] else "") as String, + teamName = (if (map.containsKey("teamName")) map["teamName"] else "") as String, + invited = (if (map.containsKey("invited")) map["invited"] else "") as String, + joined = (if (map.containsKey("joined")) map["joined"] else "") as String, + confirm = (if (map.containsKey("confirm")) map["confirm"] else false) as Boolean, + mfa = (if (map.containsKey("mfa")) map["mfa"] else false) as Boolean, + userAccessedAt = (if (map.containsKey("userAccessedAt")) map["userAccessedAt"] else "") as String, + roles = (if (map.containsKey("roles")) map["roles"] else listOf()) as List, ) } } diff --git a/library/src/main/java/io/appwrite/models/MembershipList.kt b/library/src/main/java/io/appwrite/models/MembershipList.kt index 8376bc96..f6f4afe7 100644 --- a/library/src/main/java/io/appwrite/models/MembershipList.kt +++ b/library/src/main/java/io/appwrite/models/MembershipList.kt @@ -30,8 +30,8 @@ data class MembershipList( fun from( map: Map, ) = MembershipList( - total = (map["total"] as Number).toLong(), - memberships = (map["memberships"] as List>).map { Membership.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + memberships = ((if (map.containsKey("memberships")) map["memberships"] else listOf()) as List>).map { Membership.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/MfaChallenge.kt b/library/src/main/java/io/appwrite/models/MfaChallenge.kt index b3e9f6bd..c17e792d 100644 --- a/library/src/main/java/io/appwrite/models/MfaChallenge.kt +++ b/library/src/main/java/io/appwrite/models/MfaChallenge.kt @@ -44,10 +44,10 @@ data class MfaChallenge( fun from( map: Map, ) = MfaChallenge( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - userId = map["userId"] as String, - expire = map["expire"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/MfaFactors.kt b/library/src/main/java/io/appwrite/models/MfaFactors.kt index 20f7bb36..42872fa2 100644 --- a/library/src/main/java/io/appwrite/models/MfaFactors.kt +++ b/library/src/main/java/io/appwrite/models/MfaFactors.kt @@ -51,11 +51,11 @@ data class MfaFactors( fun from( map: Map, ) = MfaFactors( - totp = map["totp"] as Boolean, - phone = map["phone"] as Boolean, - email = map["email"] as Boolean, - recoveryCode = map["recoveryCode"] as Boolean, - custom = map["custom"] as Boolean, + totp = (if (map.containsKey("totp")) map["totp"] else false) as Boolean, + phone = (if (map.containsKey("phone")) map["phone"] else false) as Boolean, + email = (if (map.containsKey("email")) map["email"] else false) as Boolean, + recoveryCode = (if (map.containsKey("recoveryCode")) map["recoveryCode"] else false) as Boolean, + custom = (if (map.containsKey("custom")) map["custom"] else false) as Boolean, ) } } diff --git a/library/src/main/java/io/appwrite/models/MfaRecoveryCodes.kt b/library/src/main/java/io/appwrite/models/MfaRecoveryCodes.kt index a8967496..25e783cd 100644 --- a/library/src/main/java/io/appwrite/models/MfaRecoveryCodes.kt +++ b/library/src/main/java/io/appwrite/models/MfaRecoveryCodes.kt @@ -23,7 +23,7 @@ data class MfaRecoveryCodes( fun from( map: Map, ) = MfaRecoveryCodes( - recoveryCodes = map["recoveryCodes"] as List, + recoveryCodes = (if (map.containsKey("recoveryCodes")) map["recoveryCodes"] else listOf()) as List, ) } } diff --git a/library/src/main/java/io/appwrite/models/MfaType.kt b/library/src/main/java/io/appwrite/models/MfaType.kt index e20b7f66..5b7b9710 100644 --- a/library/src/main/java/io/appwrite/models/MfaType.kt +++ b/library/src/main/java/io/appwrite/models/MfaType.kt @@ -30,8 +30,8 @@ data class MfaType( fun from( map: Map, ) = MfaType( - secret = map["secret"] as String, - uri = map["uri"] as String, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + uri = (if (map.containsKey("uri")) map["uri"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Approve.kt b/library/src/main/java/io/appwrite/models/Oauth2Approve.kt index e09ab9ea..0aabf4de 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Approve.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Approve.kt @@ -23,7 +23,7 @@ data class Oauth2Approve( fun from( map: Map, ) = Oauth2Approve( - redirectUrl = map["redirectUrl"] as String, + redirectUrl = (if (map.containsKey("redirectUrl")) map["redirectUrl"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Authorize.kt b/library/src/main/java/io/appwrite/models/Oauth2Authorize.kt index e0812f7e..fe878e3a 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Authorize.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Authorize.kt @@ -30,8 +30,8 @@ data class Oauth2Authorize( fun from( map: Map, ) = Oauth2Authorize( - grantId = map["grantId"] as String, - redirectUrl = map["redirectUrl"] as String, + grantId = (if (map.containsKey("grantId")) map["grantId"] else "") as String, + redirectUrl = (if (map.containsKey("redirectUrl")) map["redirectUrl"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Consent.kt b/library/src/main/java/io/appwrite/models/Oauth2Consent.kt index 7769e9b1..28cde915 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Consent.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Consent.kt @@ -86,16 +86,16 @@ data class Oauth2Consent( fun from( map: Map, ) = Oauth2Consent( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - userId = map["userId"] as String, - appId = map["appId"] as String, - cimdUrl = map["cimdUrl"] as String, - scopes = map["scopes"] as List, - resources = map["resources"] as List, - authorizationDetails = map["authorizationDetails"] as String, - expire = map["expire"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + cimdUrl = (if (map.containsKey("cimdUrl")) map["cimdUrl"] else "") as String, + scopes = (if (map.containsKey("scopes")) map["scopes"] else listOf()) as List, + resources = (if (map.containsKey("resources")) map["resources"] else listOf()) as List, + authorizationDetails = (if (map.containsKey("authorizationDetails")) map["authorizationDetails"] else "[]") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2ConsentList.kt b/library/src/main/java/io/appwrite/models/Oauth2ConsentList.kt index da6acd38..2a6337bd 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2ConsentList.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2ConsentList.kt @@ -30,8 +30,8 @@ data class Oauth2ConsentList( fun from( map: Map, ) = Oauth2ConsentList( - total = (map["total"] as Number).toLong(), - consents = (map["consents"] as List>).map { Oauth2Consent.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + consents = ((if (map.containsKey("consents")) map["consents"] else listOf()) as List>).map { Oauth2Consent.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2ConsentToken.kt b/library/src/main/java/io/appwrite/models/Oauth2ConsentToken.kt index 31f6249b..1f5c1dfc 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2ConsentToken.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2ConsentToken.kt @@ -93,17 +93,17 @@ data class Oauth2ConsentToken( fun from( map: Map, ) = Oauth2ConsentToken( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - consentId = map["consentId"] as String, - userId = map["userId"] as String, - appId = map["appId"] as String, - cimdUrl = map["cimdUrl"] as String, - scopes = map["scopes"] as List, - resources = map["resources"] as List, - authorizationDetails = map["authorizationDetails"] as String, - expire = map["expire"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + consentId = (if (map.containsKey("consentId")) map["consentId"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + cimdUrl = (if (map.containsKey("cimdUrl")) map["cimdUrl"] else "") as String, + scopes = (if (map.containsKey("scopes")) map["scopes"] else listOf()) as List, + resources = (if (map.containsKey("resources")) map["resources"] else listOf()) as List, + authorizationDetails = (if (map.containsKey("authorizationDetails")) map["authorizationDetails"] else "[]") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2ConsentTokenList.kt b/library/src/main/java/io/appwrite/models/Oauth2ConsentTokenList.kt index 09278f45..b5d4c524 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2ConsentTokenList.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2ConsentTokenList.kt @@ -30,8 +30,8 @@ data class Oauth2ConsentTokenList( fun from( map: Map, ) = Oauth2ConsentTokenList( - total = (map["total"] as Number).toLong(), - tokens = (map["tokens"] as List>).map { Oauth2ConsentToken.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + tokens = ((if (map.containsKey("tokens")) map["tokens"] else listOf()) as List>).map { Oauth2ConsentToken.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2DeviceAuthorization.kt b/library/src/main/java/io/appwrite/models/Oauth2DeviceAuthorization.kt index 58c7e5ee..f4ca6e64 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2DeviceAuthorization.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2DeviceAuthorization.kt @@ -58,12 +58,12 @@ data class Oauth2DeviceAuthorization( fun from( map: Map, ) = Oauth2DeviceAuthorization( - device_code = map["device_code"] as String, - user_code = map["user_code"] as String, - verification_uri = map["verification_uri"] as String, - verification_uri_complete = map["verification_uri_complete"] as String, - expires_in = (map["expires_in"] as Number).toLong(), - interval = (map["interval"] as Number).toLong(), + device_code = (if (map.containsKey("device_code")) map["device_code"] else "") as String, + user_code = (if (map.containsKey("user_code")) map["user_code"] else "") as String, + verification_uri = (if (map.containsKey("verification_uri")) map["verification_uri"] else "") as String, + verification_uri_complete = (if (map.containsKey("verification_uri_complete")) map["verification_uri_complete"] else "") as String, + expires_in = ((if (map.containsKey("expires_in")) map["expires_in"] else 0) as Number).toLong(), + interval = ((if (map.containsKey("interval")) map["interval"] else 0) as Number).toLong(), ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Grant.kt b/library/src/main/java/io/appwrite/models/Oauth2Grant.kt index ef1a7c39..914b6f33 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Grant.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Grant.kt @@ -100,18 +100,18 @@ data class Oauth2Grant( fun from( map: Map, ) = Oauth2Grant( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - userId = map["userId"] as String, - appId = map["appId"] as String, - scopes = map["scopes"] as List, - resources = map["resources"] as List, - authorizationDetails = map["authorizationDetails"] as String, - prompt = map["prompt"] as String, - redirectUri = map["redirectUri"] as String, - authTime = (map["authTime"] as Number).toLong(), - expire = map["expire"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + appId = (if (map.containsKey("appId")) map["appId"] else "") as String, + scopes = (if (map.containsKey("scopes")) map["scopes"] else listOf()) as List, + resources = (if (map.containsKey("resources")) map["resources"] else listOf()) as List, + authorizationDetails = (if (map.containsKey("authorizationDetails")) map["authorizationDetails"] else "[]") as String, + prompt = (if (map.containsKey("prompt")) map["prompt"] else "") as String, + redirectUri = (if (map.containsKey("redirectUri")) map["redirectUri"] else "") as String, + authTime = ((if (map.containsKey("authTime")) map["authTime"] else 0) as Number).toLong(), + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Organization.kt b/library/src/main/java/io/appwrite/models/Oauth2Organization.kt index 605916a0..47b6a8ff 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Organization.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Organization.kt @@ -23,7 +23,7 @@ data class Oauth2Organization( fun from( map: Map, ) = Oauth2Organization( - id = map["\$id"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2OrganizationList.kt b/library/src/main/java/io/appwrite/models/Oauth2OrganizationList.kt index b90ee636..48f45063 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2OrganizationList.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2OrganizationList.kt @@ -30,8 +30,8 @@ data class Oauth2OrganizationList( fun from( map: Map, ) = Oauth2OrganizationList( - total = (map["total"] as Number).toLong(), - organizations = (map["organizations"] as List>).map { Oauth2Organization.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + organizations = ((if (map.containsKey("organizations")) map["organizations"] else listOf()) as List>).map { Oauth2Organization.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2PAR.kt b/library/src/main/java/io/appwrite/models/Oauth2PAR.kt index 94d175ee..1091bdd5 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2PAR.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2PAR.kt @@ -30,8 +30,8 @@ data class Oauth2PAR( fun from( map: Map, ) = Oauth2PAR( - request_uri = map["request_uri"] as String, - expires_in = (map["expires_in"] as Number).toLong(), + request_uri = (if (map.containsKey("request_uri")) map["request_uri"] else "") as String, + expires_in = ((if (map.containsKey("expires_in")) map["expires_in"] else 0) as Number).toLong(), ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Project.kt b/library/src/main/java/io/appwrite/models/Oauth2Project.kt index eb085dce..95c79e0b 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Project.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Project.kt @@ -37,9 +37,9 @@ data class Oauth2Project( fun from( map: Map, ) = Oauth2Project( - id = map["\$id"] as String, - region = map["region"] as String, - endpoint = map["endpoint"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + region = (if (map.containsKey("region")) map["region"] else "") as String, + endpoint = (if (map.containsKey("endpoint")) map["endpoint"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2ProjectList.kt b/library/src/main/java/io/appwrite/models/Oauth2ProjectList.kt index 551b9ee4..5eb46ed5 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2ProjectList.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2ProjectList.kt @@ -30,8 +30,8 @@ data class Oauth2ProjectList( fun from( map: Map, ) = Oauth2ProjectList( - total = (map["total"] as Number).toLong(), - projects = (map["projects"] as List>).map { Oauth2Project.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + projects = ((if (map.containsKey("projects")) map["projects"] else listOf()) as List>).map { Oauth2Project.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Reject.kt b/library/src/main/java/io/appwrite/models/Oauth2Reject.kt index b23f308c..42b3a3a3 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Reject.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Reject.kt @@ -23,7 +23,7 @@ data class Oauth2Reject( fun from( map: Map, ) = Oauth2Reject( - redirectUrl = map["redirectUrl"] as String, + redirectUrl = (if (map.containsKey("redirectUrl")) map["redirectUrl"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Oauth2Token.kt b/library/src/main/java/io/appwrite/models/Oauth2Token.kt index f36f7768..53547c23 100644 --- a/library/src/main/java/io/appwrite/models/Oauth2Token.kt +++ b/library/src/main/java/io/appwrite/models/Oauth2Token.kt @@ -65,11 +65,11 @@ data class Oauth2Token( fun from( map: Map, ) = Oauth2Token( - access_token = map["access_token"] as String, - token_type = map["token_type"] as String, - expires_in = (map["expires_in"] as Number).toLong(), - refresh_token = map["refresh_token"] as String, - scope = map["scope"] as String, + access_token = (if (map.containsKey("access_token")) map["access_token"] else "") as String, + token_type = (if (map.containsKey("token_type")) map["token_type"] else "Bearer") as String, + expires_in = ((if (map.containsKey("expires_in")) map["expires_in"] else 0) as Number).toLong(), + refresh_token = (if (map.containsKey("refresh_token")) map["refresh_token"] else "") as String, + scope = (if (map.containsKey("scope")) map["scope"] else "") as String, authorization_details = map["authorization_details"] as? String, id_token = map["id_token"] as? String, ) diff --git a/library/src/main/java/io/appwrite/models/Passkey.kt b/library/src/main/java/io/appwrite/models/Passkey.kt new file mode 100644 index 00000000..90f0e497 --- /dev/null +++ b/library/src/main/java/io/appwrite/models/Passkey.kt @@ -0,0 +1,61 @@ +package io.appwrite.models + +import com.google.gson.annotations.SerializedName +import io.appwrite.extensions.jsonCast + +/** + * Passkey + */ +data class Passkey( + /** + * Passkey ID. + */ + @SerializedName("\$id") + val id: String, + + /** + * Passkey creation date in ISO 8601 format. + */ + @SerializedName("\$createdAt") + val createdAt: String, + + /** + * Passkey update date in ISO 8601 format. + */ + @SerializedName("\$updatedAt") + val updatedAt: String, + + /** + * Passkey name. + */ + @SerializedName("name") + val name: String, + + /** + * Most recent sign-in with this passkey in ISO 8601 format. Empty until the passkey is used to sign in. + */ + @SerializedName("accessedAt") + val accessedAt: String, + +) { + fun toMap(): Map = mapOf( + "\$id" to id as Any, + "\$createdAt" to createdAt as Any, + "\$updatedAt" to updatedAt as Any, + "name" to name as Any, + "accessedAt" to accessedAt as Any, + ) + + companion object { + @Suppress("UNCHECKED_CAST") + fun from( + map: Map, + ) = Passkey( + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + accessedAt = (if (map.containsKey("accessedAt")) map["accessedAt"] else "") as String, + ) + } +} diff --git a/library/src/main/java/io/appwrite/models/PasskeyChallenge.kt b/library/src/main/java/io/appwrite/models/PasskeyChallenge.kt new file mode 100644 index 00000000..dcdcb771 --- /dev/null +++ b/library/src/main/java/io/appwrite/models/PasskeyChallenge.kt @@ -0,0 +1,61 @@ +package io.appwrite.models + +import com.google.gson.annotations.SerializedName +import io.appwrite.extensions.jsonCast + +/** + * Passkey Challenge + */ +data class PasskeyChallenge( + /** + * Challenge ID. When signing in, pass it back with the credential to receive a token. + */ + @SerializedName("\$id") + val id: String, + + /** + * Challenge creation date in ISO 8601 format. + */ + @SerializedName("\$createdAt") + val createdAt: String, + + /** + * ID of the passkey being registered. Empty for sign-in challenges. + */ + @SerializedName("passkeyId") + val passkeyId: String, + + /** + * Challenge expiration date in ISO 8601 format. + */ + @SerializedName("expire") + val expire: String, + + /** + * WebAuthn options in JSON form. Pass them to `PublicKeyCredential.parseCreationOptionsFromJSON()` when registering, or `PublicKeyCredential.parseRequestOptionsFromJSON()` when signing in. + */ + @SerializedName("publicKey") + val publicKey: Map, + +) { + fun toMap(): Map = mapOf( + "\$id" to id as Any, + "\$createdAt" to createdAt as Any, + "passkeyId" to passkeyId as Any, + "expire" to expire as Any, + "publicKey" to publicKey as Any, + ) + + companion object { + @Suppress("UNCHECKED_CAST") + fun from( + map: Map, + ) = PasskeyChallenge( + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + passkeyId = (if (map.containsKey("passkeyId")) map["passkeyId"] else "") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, + publicKey = (if (map.containsKey("publicKey")) map["publicKey"] else mapOf()) as Map, + ) + } +} diff --git a/library/src/main/java/io/appwrite/models/PasskeyList.kt b/library/src/main/java/io/appwrite/models/PasskeyList.kt new file mode 100644 index 00000000..05692293 --- /dev/null +++ b/library/src/main/java/io/appwrite/models/PasskeyList.kt @@ -0,0 +1,37 @@ +package io.appwrite.models + +import com.google.gson.annotations.SerializedName +import io.appwrite.extensions.jsonCast + +/** + * Passkeys List + */ +data class PasskeyList( + /** + * Total number of passkeys that matched your query. + */ + @SerializedName("total") + val total: Long, + + /** + * List of passkeys. + */ + @SerializedName("passkeys") + val passkeys: List, + +) { + fun toMap(): Map = mapOf( + "total" to total as Any, + "passkeys" to passkeys.map { it.toMap() } as Any, + ) + + companion object { + @Suppress("UNCHECKED_CAST") + fun from( + map: Map, + ) = PasskeyList( + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + passkeys = ((if (map.containsKey("passkeys")) map["passkeys"] else listOf()) as List>).map { Passkey.from(map = it) }, + ) + } +} diff --git a/library/src/main/java/io/appwrite/models/Phone.kt b/library/src/main/java/io/appwrite/models/Phone.kt index 74bfa518..12c8eb63 100644 --- a/library/src/main/java/io/appwrite/models/Phone.kt +++ b/library/src/main/java/io/appwrite/models/Phone.kt @@ -37,9 +37,9 @@ data class Phone( fun from( map: Map, ) = Phone( - code = map["code"] as String, - countryCode = map["countryCode"] as String, - countryName = map["countryName"] as String, + code = (if (map.containsKey("code")) map["code"] else "") as String, + countryCode = (if (map.containsKey("countryCode")) map["countryCode"] else "") as String, + countryName = (if (map.containsKey("countryName")) map["countryName"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/PhoneList.kt b/library/src/main/java/io/appwrite/models/PhoneList.kt index fc0ffa3c..2c3f9cd1 100644 --- a/library/src/main/java/io/appwrite/models/PhoneList.kt +++ b/library/src/main/java/io/appwrite/models/PhoneList.kt @@ -30,8 +30,8 @@ data class PhoneList( fun from( map: Map, ) = PhoneList( - total = (map["total"] as Number).toLong(), - phones = (map["phones"] as List>).map { Phone.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + phones = ((if (map.containsKey("phones")) map["phones"] else listOf()) as List>).map { Phone.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Presence.kt b/library/src/main/java/io/appwrite/models/Presence.kt index c7f60731..b8e27fc0 100644 --- a/library/src/main/java/io/appwrite/models/Presence.kt +++ b/library/src/main/java/io/appwrite/models/Presence.kt @@ -79,13 +79,13 @@ data class Presence( fun from( map: Map, ) = Presence( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - permissions = map["\$permissions"] as List, - userId = map["userId"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + permissions = (if (map.containsKey("\$permissions")) map["\$permissions"] else listOf()) as List, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, status = map["status"] as? String, - source = map["source"] as String, + source = (if (map.containsKey("source")) map["source"] else "") as String, expiresAt = map["expiresAt"] as? String, metadata = map["metadata"] as? Map, ) diff --git a/library/src/main/java/io/appwrite/models/PresenceList.kt b/library/src/main/java/io/appwrite/models/PresenceList.kt index b69d3e13..01a20264 100644 --- a/library/src/main/java/io/appwrite/models/PresenceList.kt +++ b/library/src/main/java/io/appwrite/models/PresenceList.kt @@ -30,8 +30,8 @@ data class PresenceList( fun from( map: Map, ) = PresenceList( - total = (map["total"] as Number).toLong(), - presences = (map["presences"] as List>).map { Presence.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + presences = ((if (map.containsKey("presences")) map["presences"] else listOf()) as List>).map { Presence.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Row.kt b/library/src/main/java/io/appwrite/models/Row.kt index 1f190104..d0968469 100644 --- a/library/src/main/java/io/appwrite/models/Row.kt +++ b/library/src/main/java/io/appwrite/models/Row.kt @@ -92,13 +92,13 @@ data class Row( map: Map, nestedType: Class ) = Row( - id = map["\$id"] as String, - sequence = map["\$sequence"] as String, - tableId = map["\$tableId"] as String, - databaseId = map["\$databaseId"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - permissions = map["\$permissions"] as List, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + sequence = (if (map.containsKey("\$sequence")) map["\$sequence"] else "") as String, + tableId = (if (map.containsKey("\$tableId")) map["\$tableId"] else "") as String, + databaseId = (if (map.containsKey("\$databaseId")) map["\$databaseId"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + permissions = (if (map.containsKey("\$permissions")) map["\$permissions"] else listOf()) as List, data = map["data"]?.jsonCast(to = nestedType) ?: map.jsonCast(to = nestedType) ) } diff --git a/library/src/main/java/io/appwrite/models/RowList.kt b/library/src/main/java/io/appwrite/models/RowList.kt index f9c8f61e..0d52602c 100644 --- a/library/src/main/java/io/appwrite/models/RowList.kt +++ b/library/src/main/java/io/appwrite/models/RowList.kt @@ -39,8 +39,8 @@ data class RowList( map: Map, nestedType: Class ) = RowList( - total = (map["total"] as Number).toLong(), - rows = (map["rows"] as List>).map { Row.from(map = it, nestedType) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + rows = ((if (map.containsKey("rows")) map["rows"] else listOf()) as List>).map { Row.from(map = it, nestedType) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Session.kt b/library/src/main/java/io/appwrite/models/Session.kt index c9c39509..884d5814 100644 --- a/library/src/main/java/io/appwrite/models/Session.kt +++ b/library/src/main/java/io/appwrite/models/Session.kt @@ -219,35 +219,35 @@ data class Session( fun from( map: Map, ) = Session( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - userId = map["userId"] as String, - expire = map["expire"] as String, - provider = map["provider"] as String, - providerUid = map["providerUid"] as String, - providerAccessToken = map["providerAccessToken"] as String, - providerAccessTokenExpiry = map["providerAccessTokenExpiry"] as String, - providerRefreshToken = map["providerRefreshToken"] as String, - ip = map["ip"] as String, - osCode = map["osCode"] as String, - osName = map["osName"] as String, - osVersion = map["osVersion"] as String, - clientType = map["clientType"] as String, - clientCode = map["clientCode"] as String, - clientName = map["clientName"] as String, - clientVersion = map["clientVersion"] as String, - clientEngine = map["clientEngine"] as String, - clientEngineVersion = map["clientEngineVersion"] as String, - deviceName = map["deviceName"] as String, - deviceBrand = map["deviceBrand"] as String, - deviceModel = map["deviceModel"] as String, - countryCode = map["countryCode"] as String, - countryName = map["countryName"] as String, - current = map["current"] as Boolean, - factors = map["factors"] as List, - secret = map["secret"] as String, - mfaUpdatedAt = map["mfaUpdatedAt"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, + provider = (if (map.containsKey("provider")) map["provider"] else "") as String, + providerUid = (if (map.containsKey("providerUid")) map["providerUid"] else "") as String, + providerAccessToken = (if (map.containsKey("providerAccessToken")) map["providerAccessToken"] else "") as String, + providerAccessTokenExpiry = (if (map.containsKey("providerAccessTokenExpiry")) map["providerAccessTokenExpiry"] else "") as String, + providerRefreshToken = (if (map.containsKey("providerRefreshToken")) map["providerRefreshToken"] else "") as String, + ip = (if (map.containsKey("ip")) map["ip"] else "") as String, + osCode = (if (map.containsKey("osCode")) map["osCode"] else "") as String, + osName = (if (map.containsKey("osName")) map["osName"] else "") as String, + osVersion = (if (map.containsKey("osVersion")) map["osVersion"] else "") as String, + clientType = (if (map.containsKey("clientType")) map["clientType"] else "") as String, + clientCode = (if (map.containsKey("clientCode")) map["clientCode"] else "") as String, + clientName = (if (map.containsKey("clientName")) map["clientName"] else "") as String, + clientVersion = (if (map.containsKey("clientVersion")) map["clientVersion"] else "") as String, + clientEngine = (if (map.containsKey("clientEngine")) map["clientEngine"] else "") as String, + clientEngineVersion = (if (map.containsKey("clientEngineVersion")) map["clientEngineVersion"] else "") as String, + deviceName = (if (map.containsKey("deviceName")) map["deviceName"] else "") as String, + deviceBrand = (if (map.containsKey("deviceBrand")) map["deviceBrand"] else "") as String, + deviceModel = (if (map.containsKey("deviceModel")) map["deviceModel"] else "") as String, + countryCode = (if (map.containsKey("countryCode")) map["countryCode"] else "") as String, + countryName = (if (map.containsKey("countryName")) map["countryName"] else "") as String, + current = (if (map.containsKey("current")) map["current"] else false) as Boolean, + factors = (if (map.containsKey("factors")) map["factors"] else listOf()) as List, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + mfaUpdatedAt = (if (map.containsKey("mfaUpdatedAt")) map["mfaUpdatedAt"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/SessionList.kt b/library/src/main/java/io/appwrite/models/SessionList.kt index bda39fca..9be215ba 100644 --- a/library/src/main/java/io/appwrite/models/SessionList.kt +++ b/library/src/main/java/io/appwrite/models/SessionList.kt @@ -30,8 +30,8 @@ data class SessionList( fun from( map: Map, ) = SessionList( - total = (map["total"] as Number).toLong(), - sessions = (map["sessions"] as List>).map { Session.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + sessions = ((if (map.containsKey("sessions")) map["sessions"] else listOf()) as List>).map { Session.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Subscriber.kt b/library/src/main/java/io/appwrite/models/Subscriber.kt index 931b65db..adcd8c53 100644 --- a/library/src/main/java/io/appwrite/models/Subscriber.kt +++ b/library/src/main/java/io/appwrite/models/Subscriber.kt @@ -79,15 +79,15 @@ data class Subscriber( fun from( map: Map, ) = Subscriber( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - targetId = map["targetId"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + targetId = (if (map.containsKey("targetId")) map["targetId"] else "") as String, target = Target.from(map = map["target"] as Map), - userId = map["userId"] as String, - userName = map["userName"] as String, - topicId = map["topicId"] as String, - providerType = map["providerType"] as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + userName = (if (map.containsKey("userName")) map["userName"] else "") as String, + topicId = (if (map.containsKey("topicId")) map["topicId"] else "") as String, + providerType = (if (map.containsKey("providerType")) map["providerType"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/Target.kt b/library/src/main/java/io/appwrite/models/Target.kt index 3e89e08d..d67a716c 100644 --- a/library/src/main/java/io/appwrite/models/Target.kt +++ b/library/src/main/java/io/appwrite/models/Target.kt @@ -79,15 +79,15 @@ data class Target( fun from( map: Map, ) = Target( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - name = map["name"] as String, - userId = map["userId"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, providerId = map["providerId"] as? String, - providerType = map["providerType"] as String, - identifier = map["identifier"] as String, - expired = map["expired"] as Boolean, + providerType = (if (map.containsKey("providerType")) map["providerType"] else "") as String, + identifier = (if (map.containsKey("identifier")) map["identifier"] else "") as String, + expired = (if (map.containsKey("expired")) map["expired"] else false) as Boolean, ) } } diff --git a/library/src/main/java/io/appwrite/models/Team.kt b/library/src/main/java/io/appwrite/models/Team.kt index 6cd7dbc9..dc780c8b 100644 --- a/library/src/main/java/io/appwrite/models/Team.kt +++ b/library/src/main/java/io/appwrite/models/Team.kt @@ -75,11 +75,11 @@ data class Team( map: Map, nestedType: Class ) = Team( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - name = map["name"] as String, - total = (map["total"] as Number).toLong(), + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), prefs = Preferences.from(map = map["prefs"] as Map, nestedType), ) } diff --git a/library/src/main/java/io/appwrite/models/TeamList.kt b/library/src/main/java/io/appwrite/models/TeamList.kt index 8e490f51..db4e1eb2 100644 --- a/library/src/main/java/io/appwrite/models/TeamList.kt +++ b/library/src/main/java/io/appwrite/models/TeamList.kt @@ -39,8 +39,8 @@ data class TeamList( map: Map, nestedType: Class ) = TeamList( - total = (map["total"] as Number).toLong(), - teams = (map["teams"] as List>).map { Team.from(map = it, nestedType) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + teams = ((if (map.containsKey("teams")) map["teams"] else listOf()) as List>).map { Team.from(map = it, nestedType) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/Token.kt b/library/src/main/java/io/appwrite/models/Token.kt index 625e3085..c3762397 100644 --- a/library/src/main/java/io/appwrite/models/Token.kt +++ b/library/src/main/java/io/appwrite/models/Token.kt @@ -58,12 +58,12 @@ data class Token( fun from( map: Map, ) = Token( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - userId = map["userId"] as String, - secret = map["secret"] as String, - expire = map["expire"] as String, - phrase = map["phrase"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, + phrase = (if (map.containsKey("phrase")) map["phrase"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/TokenSecret.kt b/library/src/main/java/io/appwrite/models/TokenSecret.kt new file mode 100644 index 00000000..fe2dd92c --- /dev/null +++ b/library/src/main/java/io/appwrite/models/TokenSecret.kt @@ -0,0 +1,69 @@ +package io.appwrite.models + +import com.google.gson.annotations.SerializedName +import io.appwrite.extensions.jsonCast + +/** + * Token Secret + */ +data class TokenSecret( + /** + * Token ID. + */ + @SerializedName("\$id") + val id: String, + + /** + * Token creation date in ISO 8601 format. + */ + @SerializedName("\$createdAt") + val createdAt: String, + + /** + * User ID. + */ + @SerializedName("userId") + val userId: String, + + /** + * Token secret key. Exchange it for a session with the create session endpoint. + */ + @SerializedName("secret") + val secret: String, + + /** + * Token expiration date in ISO 8601 format. + */ + @SerializedName("expire") + val expire: String, + + /** + * Security phrase of a token. Empty if security phrase was not requested when creating a token. It includes randomly generated phrase which is also sent in the external resource such as email. + */ + @SerializedName("phrase") + val phrase: String, + +) { + fun toMap(): Map = mapOf( + "\$id" to id as Any, + "\$createdAt" to createdAt as Any, + "userId" to userId as Any, + "secret" to secret as Any, + "expire" to expire as Any, + "phrase" to phrase as Any, + ) + + companion object { + @Suppress("UNCHECKED_CAST") + fun from( + map: Map, + ) = TokenSecret( + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + userId = (if (map.containsKey("userId")) map["userId"] else "") as String, + secret = (if (map.containsKey("secret")) map["secret"] else "") as String, + expire = (if (map.containsKey("expire")) map["expire"] else "") as String, + phrase = (if (map.containsKey("phrase")) map["phrase"] else "") as String, + ) + } +} diff --git a/library/src/main/java/io/appwrite/models/Transaction.kt b/library/src/main/java/io/appwrite/models/Transaction.kt index 8f09860e..bb5f39b3 100644 --- a/library/src/main/java/io/appwrite/models/Transaction.kt +++ b/library/src/main/java/io/appwrite/models/Transaction.kt @@ -58,12 +58,12 @@ data class Transaction( fun from( map: Map, ) = Transaction( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - status = map["status"] as String, - operations = (map["operations"] as Number).toLong(), - expiresAt = map["expiresAt"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + status = (if (map.containsKey("status")) map["status"] else "pending") as String, + operations = ((if (map.containsKey("operations")) map["operations"] else 0) as Number).toLong(), + expiresAt = (if (map.containsKey("expiresAt")) map["expiresAt"] else "") as String, ) } } diff --git a/library/src/main/java/io/appwrite/models/TransactionList.kt b/library/src/main/java/io/appwrite/models/TransactionList.kt index c55a7317..963f3732 100644 --- a/library/src/main/java/io/appwrite/models/TransactionList.kt +++ b/library/src/main/java/io/appwrite/models/TransactionList.kt @@ -30,8 +30,8 @@ data class TransactionList( fun from( map: Map, ) = TransactionList( - total = (map["total"] as Number).toLong(), - transactions = (map["transactions"] as List>).map { Transaction.from(map = it) }, + total = ((if (map.containsKey("total")) map["total"] else 0) as Number).toLong(), + transactions = ((if (map.containsKey("transactions")) map["transactions"] else listOf()) as List>).map { Transaction.from(map = it) }, ) } } diff --git a/library/src/main/java/io/appwrite/models/User.kt b/library/src/main/java/io/appwrite/models/User.kt index 0eea3b00..22b4aeb5 100644 --- a/library/src/main/java/io/appwrite/models/User.kt +++ b/library/src/main/java/io/appwrite/models/User.kt @@ -264,31 +264,31 @@ data class User( map: Map, nestedType: Class ) = User( - id = map["\$id"] as String, - createdAt = map["\$createdAt"] as String, - updatedAt = map["\$updatedAt"] as String, - name = map["name"] as String, + id = (if (map.containsKey("\$id")) map["\$id"] else "") as String, + createdAt = (if (map.containsKey("\$createdAt")) map["\$createdAt"] else "") as String, + updatedAt = (if (map.containsKey("\$updatedAt")) map["\$updatedAt"] else "") as String, + name = (if (map.containsKey("name")) map["name"] else "") as String, password = map["password"] as? String, hash = map["hash"] as? String, hashOptions = map["hashOptions"] as? Map, - registration = map["registration"] as String, - status = map["status"] as Boolean, - labels = map["labels"] as List, - passwordUpdate = map["passwordUpdate"] as String, - email = map["email"] as String, - phone = map["phone"] as String, - emailVerification = map["emailVerification"] as Boolean, + registration = (if (map.containsKey("registration")) map["registration"] else "") as String, + status = (if (map.containsKey("status")) map["status"] else true) as Boolean, + labels = (if (map.containsKey("labels")) map["labels"] else listOf()) as List, + passwordUpdate = (if (map.containsKey("passwordUpdate")) map["passwordUpdate"] else "") as String, + email = (if (map.containsKey("email")) map["email"] else "") as String, + phone = (if (map.containsKey("phone")) map["phone"] else "") as String, + emailVerification = (if (map.containsKey("emailVerification")) map["emailVerification"] else false) as Boolean, emailCanonical = map["emailCanonical"] as? String, emailIsFree = map["emailIsFree"] as? Boolean, emailIsDisposable = map["emailIsDisposable"] as? Boolean, emailIsCorporate = map["emailIsCorporate"] as? Boolean, emailIsCanonical = map["emailIsCanonical"] as? Boolean, passwordPwned = map["passwordPwned"] as? Boolean, - phoneVerification = map["phoneVerification"] as Boolean, - mfa = map["mfa"] as Boolean, + phoneVerification = (if (map.containsKey("phoneVerification")) map["phoneVerification"] else false) as Boolean, + mfa = (if (map.containsKey("mfa")) map["mfa"] else false) as Boolean, prefs = Preferences.from(map = map["prefs"] as Map, nestedType), - targets = (map["targets"] as List>).map { Target.from(map = it) }, - accessedAt = map["accessedAt"] as String, + targets = ((if (map.containsKey("targets")) map["targets"] else listOf()) as List>).map { Target.from(map = it) }, + accessedAt = (if (map.containsKey("accessedAt")) map["accessedAt"] else "") as String, impersonator = map["impersonator"] as? Boolean, impersonatorUserId = map["impersonatorUserId"] as? String, ) diff --git a/library/src/main/java/io/appwrite/services/Account.kt b/library/src/main/java/io/appwrite/services/Account.kt index 8df38221..e6201c46 100644 --- a/library/src/main/java/io/appwrite/services/Account.kt +++ b/library/src/main/java/io/appwrite/services/Account.kt @@ -1200,6 +1200,219 @@ class Account(client: Client) : Service(client) { nestedType = classOf(), ) + /** + * Get the list of verified passkeys registered by the currently logged in user. + * + * @param queries Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter and order on the following attributes: $id, $createdAt, $updatedAt, name, accessedAt + * @param total When set to false, the total count returned will be 0 and will not be calculated. + * @return [io.appwrite.models.PasskeyList] + */ + @JvmOverloads + suspend fun listPasskeys( + queries: List? = null, + total: Boolean? = null, + ): io.appwrite.models.PasskeyList { + val apiPath = "/account/passkeys" + val apiParams = mutableMapOf( + "queries" to queries, + "total" to total, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.PasskeyList = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.PasskeyList.from(map = it as Map) + } + return client.call( + "GET", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.PasskeyList::class.java, + converter, + ) + } + + /** + * Start registering a passkey for the currently logged in user. The session must have signed in or completed an MFA challenge within the last 10 minutes. Pass the returned `publicKey` options to `navigator.credentials.create()`, then complete the registration with [Update passkey verification](/docs/references/cloud/client-web/account#updatePasskeyVerification). The passkey stays pending until verified, and the challenge expires after 5 minutes. + * + * @param passkeyId Passkey ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars. + * @param name Passkey name, shown when listing passkeys. Max length: 128 chars. + * @return [io.appwrite.models.PasskeyChallenge] + */ + @JvmOverloads + suspend fun createPasskey( + passkeyId: String? = null, + name: String? = null, + ): io.appwrite.models.PasskeyChallenge { + val apiPath = "/account/passkeys" + val apiParams = mutableMapOf( + "passkeyId" to passkeyId, + "name" to name, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.PasskeyChallenge = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.PasskeyChallenge.from(map = it as Map) + } + return client.call( + "POST", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.PasskeyChallenge::class.java, + converter, + ) + } + + /** + * Get a verified passkey of the currently logged in user by its unique ID. + * + * @param passkeyId Passkey ID. + * @return [io.appwrite.models.Passkey] + */ + suspend fun getPasskey( + passkeyId: String, + ): io.appwrite.models.Passkey { + if (passkeyId.isEmpty()) { + throw AppwriteException("Missing required parameter: \"passkeyId\"") + } + val apiPath = ("/account/passkeys/{passkeyId}" + .replace("{passkeyId}", passkeyId) + ) + val apiParams = mutableMapOf() + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.Passkey = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.Passkey.from(map = it as Map) + } + return client.call( + "GET", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.Passkey::class.java, + converter, + ) + } + + /** + * Rename a passkey of the currently logged in user. + * + * @param passkeyId Passkey ID. + * @param name Passkey name. Max length: 128 chars. + * @return [io.appwrite.models.Passkey] + */ + suspend fun updatePasskey( + passkeyId: String, + name: String, + ): io.appwrite.models.Passkey { + if (passkeyId.isEmpty()) { + throw AppwriteException("Missing required parameter: \"passkeyId\"") + } + val apiPath = ("/account/passkeys/{passkeyId}" + .replace("{passkeyId}", passkeyId) + ) + val apiParams = mutableMapOf( + "name" to name, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.Passkey = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.Passkey.from(map = it as Map) + } + return client.call( + "PATCH", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.Passkey::class.java, + converter, + ) + } + + /** + * Delete a passkey from the currently logged in user. The passkey can no longer be used to sign in, although it may remain stored on the user's device. The session must have signed in or completed an MFA challenge within the last 10 minutes. + * + * @param passkeyId Passkey ID. + * @return [Any] + */ + suspend fun deletePasskey( + passkeyId: String, + ): Any { + if (passkeyId.isEmpty()) { + throw AppwriteException("Missing required parameter: \"passkeyId\"") + } + val apiPath = ("/account/passkeys/{passkeyId}" + .replace("{passkeyId}", passkeyId) + ) + val apiParams = mutableMapOf() + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + return client.call( + "DELETE", + apiPath, + apiHeaders, + apiParams, + responseType = Any::class.java, + ) + } + + /** + * Complete a passkey registration started with [Create passkey](/docs/references/cloud/client-web/account#createPasskey). Pass the JSON form of the credential returned by `navigator.credentials.create()`, for example `credential.toJSON()`. Each registration can be verified once; a failed attempt requires starting it again. + * + * @param passkeyId Passkey ID. + * @param credential Registration credential returned by the authenticator, in the JSON form produced by `PublicKeyCredential.toJSON()`. + * @return [io.appwrite.models.Passkey] + */ + suspend fun updatePasskeyVerification( + passkeyId: String, + credential: Map, + ): io.appwrite.models.Passkey { + if (passkeyId.isEmpty()) { + throw AppwriteException("Missing required parameter: \"passkeyId\"") + } + val apiPath = ("/account/passkeys/{passkeyId}/verification" + .replace("{passkeyId}", passkeyId) + ) + val apiParams = mutableMapOf( + "credential" to credential, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.Passkey = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.Passkey.from(map = it as Map) + } + return client.call( + "PUT", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.Passkey::class.java, + converter, + ) + } + /** * Update currently logged in user password. For validation, user is required to pass in the new password, and the old password. For users created with OAuth, Team Invites and Magic URL, oldPassword is optional. * @@ -1784,6 +1997,7 @@ class Account(client: Client) : Service(client) { * @param success URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param failure URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param scopes A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of 100 scopes are allowed, each 4096 characters long. + * @param state An opaque value your app generates and keeps, for example in a cookie. It is returned unchanged as the `state` query parameter on the success and failure URLs, so your app can check that the sign-in it receives is one it started. Printable ASCII only (RFC 6749 Appendix A.5). Max length: 256 chars. */ @JvmOverloads suspend fun createOAuth2Session( @@ -1792,6 +2006,7 @@ class Account(client: Client) : Service(client) { success: String? = null, failure: String? = null, scopes: List? = null, + state: String? = null, ) { val apiPath = ("/account/sessions/oauth2/{provider}" .replace("{provider}", provider.value) @@ -1800,6 +2015,7 @@ class Account(client: Client) : Service(client) { "success" to success, "failure" to failure, "scopes" to scopes, + "state" to state, "project" to client.config["project"], ) val apiQuery = mutableListOf() @@ -1810,11 +2026,11 @@ class Account(client: Client) : Service(client) { } is List<*> -> { (it.value as List<*>).forEach { v -> - apiQuery.add("${it.key}[]=${v.toString()}") + apiQuery.add("${Uri.encode(it.key)}[]=${Uri.encode(v.toString())}") } } else -> { - apiQuery.add("${it.key}=${it.value.toString()}") + apiQuery.add("${Uri.encode(it.key)}=${Uri.encode(it.value.toString())}") } } } @@ -2268,6 +2484,7 @@ class Account(client: Client) : Service(client) { * @param success URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param failure URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param scopes A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of 100 scopes are allowed, each 4096 characters long. + * @param state An opaque value your app generates and keeps, for example in a cookie. It is returned unchanged as the `state` query parameter on the success and failure URLs, so your app can check that the sign-in it receives is one it started. Printable ASCII only (RFC 6749 Appendix A.5). Max length: 256 chars. */ @JvmOverloads suspend fun createOAuth2Token( @@ -2276,6 +2493,7 @@ class Account(client: Client) : Service(client) { success: String? = null, failure: String? = null, scopes: List? = null, + state: String? = null, ) { val apiPath = ("/account/tokens/oauth2/{provider}" .replace("{provider}", provider.value) @@ -2284,6 +2502,7 @@ class Account(client: Client) : Service(client) { "success" to success, "failure" to failure, "scopes" to scopes, + "state" to state, "project" to client.config["project"], ) val apiQuery = mutableListOf() @@ -2294,11 +2513,11 @@ class Account(client: Client) : Service(client) { } is List<*> -> { (it.value as List<*>).forEach { v -> - apiQuery.add("${it.key}[]=${v.toString()}") + apiQuery.add("${Uri.encode(it.key)}[]=${Uri.encode(v.toString())}") } } else -> { - apiQuery.add("${it.key}=${it.value.toString()}") + apiQuery.add("${Uri.encode(it.key)}=${Uri.encode(it.value.toString())}") } } } @@ -2332,6 +2551,68 @@ class Account(client: Client) : Service(client) { } } + /** + * Start signing in with a passkey. No email or user ID is needed: pass the returned `publicKey` options to `navigator.credentials.get()` and the user picks one of their passkeys. Then call [Update passkey token](/docs/references/cloud/client-web/account#updatePasskeyToken) with the credential to receive a token, and exchange it with [Create session](/docs/references/cloud/client-web/account#createSession). The challenge expires after 5 minutes. + * + * @return [io.appwrite.models.PasskeyChallenge] + */ + suspend fun createPasskeyToken(): io.appwrite.models.PasskeyChallenge { + val apiPath = "/account/tokens/passkey" + val apiParams = mutableMapOf() + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.PasskeyChallenge = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.PasskeyChallenge.from(map = it as Map) + } + return client.call( + "POST", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.PasskeyChallenge::class.java, + converter, + ) + } + + /** + * Complete a passkey sign-in started with [Create passkey token](/docs/references/cloud/client-web/account#createPasskeyToken). Pass the challenge ID and the JSON form of the credential returned by `navigator.credentials.get()`, for example `credential.toJSON()`. The returned token includes its secret and expires after 1 minute: exchange it for a session with [Create session](/docs/references/cloud/client-web/account#createSession). Passkeys require user verification, so the session also satisfies MFA. Each challenge can only be used once. + * + * @param challengeId Challenge ID returned by createPasskeyToken. + * @param credential Authentication credential returned by the authenticator, in the JSON form produced by `PublicKeyCredential.toJSON()`. + * @return [io.appwrite.models.TokenSecret] + */ + suspend fun updatePasskeyToken( + challengeId: String, + credential: Map, + ): io.appwrite.models.TokenSecret { + val apiPath = "/account/tokens/passkey" + val apiParams = mutableMapOf( + "challengeId" to challengeId, + "credential" to credential, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + val converter: (Any) -> io.appwrite.models.TokenSecret = { + @Suppress("UNCHECKED_CAST") + io.appwrite.models.TokenSecret.from(map = it as Map) + } + return client.call( + "PUT", + apiPath, + apiHeaders, + apiParams, + responseType = io.appwrite.models.TokenSecret::class.java, + converter, + ) + } + /** * Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [POST /v1/account/sessions/token](https://appwrite.io/docs/references/cloud/client-web/account#createSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes. * diff --git a/library/src/main/java/io/appwrite/services/Analytics.kt b/library/src/main/java/io/appwrite/services/Analytics.kt new file mode 100644 index 00000000..1949f76b --- /dev/null +++ b/library/src/main/java/io/appwrite/services/Analytics.kt @@ -0,0 +1,83 @@ +package io.appwrite.services + +import android.net.Uri +import io.appwrite.Client +import io.appwrite.Service +import io.appwrite.exceptions.AppwriteException +import io.appwrite.extensions.classOf +import io.appwrite.models.* +import okhttp3.Cookie +import java.io.File + +/** + * + */ +class Analytics(client: Client) : Service(client) { + /** + * Send a tracking event from a browser, native app, or server-side SDK. + * + * @param propertyId Analytics property ID. + * @param name Event name. "pageview" is just a conventional event name; events are not modeled specially. + * @param url Full page URL or screen identifier. + * @param domain Hostname (e.g. example.com). + * @param referrer Referrer URL. + * @param screenWidth Viewport width in CSS pixels. + * @param sessionHash Optional session hash (provided by SDK). + * @param scrollDepth Scroll depth percentage 0-100. + * @param engagementTime Engagement time in seconds, 0-4294967295. + * @param props Custom string properties as a flat key=value list (max 32 entries, alternating key,value). + * @param userId Override user ID. Requires API-key auth with analytics.write scope. + * @param ip Override IP address. Requires API-key auth with analytics.write scope. + * @param userAgent Override user agent. Requires API-key auth with analytics.write scope. + * @return [Any] + */ + @JvmOverloads + suspend fun createEvent( + propertyId: String, + name: String, + url: String, + domain: String? = null, + referrer: String? = null, + screenWidth: Long? = null, + sessionHash: String? = null, + scrollDepth: Long? = null, + engagementTime: Long? = null, + props: List? = null, + userId: String? = null, + ip: String? = null, + userAgent: String? = null, + ): Any { + if (propertyId.isEmpty()) { + throw AppwriteException("Missing required parameter: \"propertyId\"") + } + val apiPath = ("/analytics/properties/{propertyId}/events" + .replace("{propertyId}", propertyId) + ) + val apiParams = mutableMapOf( + "name" to name, + "url" to url, + "domain" to domain, + "referrer" to referrer, + "screenWidth" to screenWidth, + "sessionHash" to sessionHash, + "scrollDepth" to scrollDepth, + "engagementTime" to engagementTime, + "props" to props, + "userId" to userId, + "ip" to ip, + "userAgent" to userAgent, + ) + val apiHeaders = mutableMapOf( + "X-Appwrite-Project" to client.config["project"].orEmpty(), + "content-type" to "application/json", + "accept" to "application/json", + ) + return client.call( + "POST", + apiPath, + apiHeaders, + apiParams, + responseType = Any::class.java, + ) + } +} diff --git a/library/src/main/java/io/appwrite/services/Oauth2.kt b/library/src/main/java/io/appwrite/services/Oauth2.kt index 030f4cef..f5cbc3cb 100644 --- a/library/src/main/java/io/appwrite/services/Oauth2.kt +++ b/library/src/main/java/io/appwrite/services/Oauth2.kt @@ -85,7 +85,7 @@ class Oauth2(client: Client) : Service(client) { prompt: String? = null, maxAge: Long? = null, authorizationDetails: String? = null, - resource: String? = null, + resource: List? = null, audience: String? = null, requestUri: String? = null, ): io.appwrite.models.Oauth2Authorize { @@ -157,7 +157,7 @@ class Oauth2(client: Client) : Service(client) { prompt: String? = null, maxAge: Long? = null, authorizationDetails: String? = null, - resource: String? = null, + resource: List? = null, audience: String? = null, requestUri: String? = null, ): io.appwrite.models.Oauth2Authorize { @@ -213,7 +213,7 @@ class Oauth2(client: Client) : Service(client) { clientId: String? = null, scope: String? = null, authorizationDetails: String? = null, - resource: String? = null, + resource: List? = null, audience: String? = null, ): io.appwrite.models.Oauth2DeviceAuthorization { val apiPath = ("/oauth2/{project_id}/device_authorization" @@ -468,7 +468,7 @@ class Oauth2(client: Client) : Service(client) { prompt: String? = null, maxAge: Long? = null, authorizationDetails: String? = null, - resource: String? = null, + resource: List? = null, audience: String? = null, ): io.appwrite.models.Oauth2PAR { val apiPath = ("/oauth2/{project_id}/par" @@ -642,7 +642,7 @@ class Oauth2(client: Client) : Service(client) { clientSecret: String? = null, codeVerifier: String? = null, redirectUri: String? = null, - resource: String? = null, + resource: List? = null, audience: String? = null, ): io.appwrite.models.Oauth2Token { val apiPath = ("/oauth2/{project_id}/token" diff --git a/library/src/main/java/io/appwrite/services/Push.kt b/library/src/main/java/io/appwrite/services/Push.kt index 48488c06..21409d73 100644 --- a/library/src/main/java/io/appwrite/services/Push.kt +++ b/library/src/main/java/io/appwrite/services/Push.kt @@ -14,7 +14,6 @@ import androidx.core.content.ContextCompat import com.hivemq.client.mqtt.datatypes.MqttQos import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient import io.appwrite.Client -import io.appwrite.ResolvedTopic import io.appwrite.Service import io.appwrite.Topic import io.appwrite.exceptions.AppwriteException @@ -338,7 +337,7 @@ class Push( /** * Subscribe to a single topic built with [io.appwrite.Topic], e.g. - * `push.subscribe(Topic.path(listOf("user", userId)).any()) { message -> ... }`. + * `push.subscribe(Topic.path(listOf("user", userId))) { message -> ... }`. * Returns a [PushSubscription] handle; see the `List` overload for the options. */ @JvmOverloads @@ -352,28 +351,12 @@ class Push( ): PushSubscription = subscribe(listOf(topic.toString()), background, title, retry, notifyInForeground, callback) - /** - * Subscribe to a single topic ended with `all()`, e.g. - * `push.subscribe(Topic.path(listOf("user", userId)).all()) { message -> ... }`. - * Returns a [PushSubscription] handle; see the `List` overload for the options. - */ - @JvmOverloads - fun subscribe( - topic: ResolvedTopic, - background: Boolean = false, - title: String? = null, - retry: Boolean = true, - notifyInForeground: Boolean = false, - callback: (PushMessage) -> Unit, - ): PushSubscription = - subscribe(listOf(topic.toString()), background, title, retry, notifyInForeground, callback) - /** * Subscribe to a list mixing raw strings and topics built with [io.appwrite.Topic], e.g. - * `push.subscribe(listOf("news", Topic.path(listOf("chat")).any())) { message -> ... }`. - * Each element must be a `String`, `Topic` or `ResolvedTopic` (Kotlin has no common type for - * them, so this is checked at runtime). Returns a [PushSubscription] handle; see the - * `List` overload for the options. + * `push.subscribe(listOf("news", Topic.path(listOf("chat", chatId)))) { message -> ... }`. + * Each element must be a `String` or `Topic` (Kotlin has no common type for them, so this is + * checked at runtime). Returns a [PushSubscription] handle; see the `List` overload + * for the options. */ @JvmOverloads @JvmName("subscribeTopics") @@ -386,8 +369,8 @@ class Push( callback: (PushMessage) -> Unit, ): PushSubscription { val filters = topics.mapIndexed { index, topic -> - require(topic is String || topic is Topic || topic is ResolvedTopic) { - "topics[$index] must be a String, Topic or ResolvedTopic, got ${topic::class.simpleName}" + require(topic is String || topic is Topic) { + "topics[$index] must be a String or Topic, got ${topic::class.simpleName}" } topic.toString() } @@ -397,8 +380,8 @@ class Push( /** * Subscribe to one or more topics. Returns a [PushSubscription] handle. * - * Topics are MQTT filters: pass raw strings, or build them with [io.appwrite.Topic] - * through the `Topic` overloads, e.g. `push.subscribe(Topic.path(listOf("user")).any()) { ... }`. + * Pass raw strings, or build them with [io.appwrite.Topic] + * through the `Topic` overloads, e.g. `push.subscribe(Topic.path(listOf("user", userId))) { ... }`. * * For a foreground subscription this blocks until the broker has acknowledged every * SUBSCRIBE (SUBACK): the broker only starts routing a topic once its subscription is @@ -828,10 +811,14 @@ class Push( const val EXTRA_PAYLOAD = PushBackground.EXTRA_PAYLOAD /** - * What background delivery can rely on: exact alarms, battery-optimisation exemption and - * foreground mode. When [PushBackgroundStatus.bestEffort] is true, wake-ups are inexact and - * messages can arrive late while the app is closed; explain why before asking with - * [requestExactAlarms] or [requestIgnoreBatteryOptimizations]. + * What background delivery can rely on: exact alarms, battery-optimisation exemption, + * foreground mode and restricted background usage. When [PushBackgroundStatus.bestEffort] + * is true, messages can arrive late while the app is closed; explain why before asking with + * [requestExactAlarms] or [requestIgnoreBatteryOptimizations]. When + * [PushBackgroundStatus.backgroundRestricted] is true, neither exact alarms nor foreground + * mode lift it: ask the user to choose Unrestricted in the app's battery settings + * (Settings > Apps > your app > Battery), or for the battery-optimisation exemption, which + * also lets the SDK's alarms and jobs run. */ @JvmStatic fun backgroundStatus(context: Context): PushBackgroundStatus = PushBackground.backgroundStatus(context) diff --git a/library/src/main/java/io/appwrite/services/PushBackground.kt b/library/src/main/java/io/appwrite/services/PushBackground.kt index e06dfabe..73103d5d 100644 --- a/library/src/main/java/io/appwrite/services/PushBackground.kt +++ b/library/src/main/java/io/appwrite/services/PushBackground.kt @@ -52,8 +52,15 @@ import java.util.concurrent.atomic.AtomicInteger private const val LOG_TAG = "AppwritePush" -/** What a message's `notification` block asks a background notification to show; [present] is false without one. */ -internal data class PushNotificationContent(val present: Boolean, val title: String?, val body: String?, val image: String?) +/** + * What a message asks a background notification to show. A [raw] payload, one that is not a JSON + * object, is shown as the body under the subscription's title; an envelope is [visible] only with a + * title or body in its `notification`. + */ +internal data class PushNotificationContent(val raw: Boolean, val title: String?, val body: String?, val image: String?) { + val visible: Boolean + get() = raw || title != null || body != null +} /** A background subscription saved across restarts: its filter, QoS choice and notification title. */ internal data class PushEntry( @@ -887,7 +894,7 @@ internal object PushBackground { // Deliver a message to the matching live callbacks; with none, to the declared PushReceiver // and/or a notification per saved subscription that matches it. [deferAck] hands a listener // that acknowledges messages itself its acknowledgement. - private fun deliverMessage(context: Context, message: PushMessage, deferAck: () -> () -> Unit) { + internal fun deliverMessage(context: Context, message: PushMessage, deferAck: () -> () -> Unit) { val matching = listeners.filter { listener -> listener.topics.any { matchesTopic(it, message.topic) } } // Saved background subscriptions no live one has taken over; they deliver even when a live // foreground-only subscription on an overlapping filter received the message too. @@ -952,16 +959,21 @@ internal object PushBackground { /** * Post a notification for [message] that opens the app, with the message in its extras. It shows - * the server's title, body and image from [content], falling back to [title] and the raw payload. + * the server's title, body and image from [content], falling back to [title], and to the payload + * as the body when it is not a JSON object. An envelope with neither a title nor a body, such as + * a data-only push, posts nothing. */ fun notify(context: Context, message: PushMessage, title: String, content: PushNotificationContent = notificationContent(message)) { + if (!content.visible) { + return + } val manager = NotificationManagerCompat.from(context) if (!manager.areNotificationsEnabled()) { return } createChannels(context) val id = 31 * message.hashCode() + title.hashCode() - val body = content.body ?: message.data.takeIf { !content.present } + val body = content.body ?: message.data.takeIf { content.raw } val builder = NotificationCompat.Builder(context, CHANNEL_ID) .setContentTitle(content.title ?: title) .setSmallIcon(notificationIcon(context)) @@ -992,12 +1004,13 @@ internal object PushBackground { .also { ActivityManager.getMyMemoryState(it) } .importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND - /** The server's `notification` block in [message]: nulls when the payload has none or is not JSON. */ + /** The server's `notification` block in [message], [PushNotificationContent.raw] when the payload is not a JSON object. */ fun notificationContent(message: PushMessage): PushNotificationContent { - val notification = runCatching { JSONObject(message.data) }.getOrNull()?.optJSONObject("notification") - ?: return PushNotificationContent(false, null, null, null) - fun field(name: String) = (notification.opt(name) as? String)?.takeIf { it.isNotEmpty() } - return PushNotificationContent(true, field("title"), field("body"), field("image")) + val envelope = runCatching { JSONObject(message.data) }.getOrNull() + ?: return PushNotificationContent(true, null, null, null) + val notification = envelope.optJSONObject("notification") + fun field(name: String) = (notification?.opt(name) as? String)?.takeIf { it.isNotEmpty() } + return PushNotificationContent(false, field("title"), field("body"), field("image")) } // Downloads notification images, so a slow one is abandoned without holding up delivery. @@ -1201,6 +1214,8 @@ internal object PushBackground { app.getSystemService(AlarmManager::class.java)?.canScheduleExactAlarms() == true, ignoringBatteryOptimizations = app.getSystemService(PowerManager::class.java)?.isIgnoringBatteryOptimizations(app.packageName) == true, foregroundService = PushStore.foreground(app), + backgroundRestricted = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && + app.getSystemService(ActivityManager::class.java)?.isBackgroundRestricted == true, ) } diff --git a/library/src/main/java/io/appwrite/services/PushCore.kt b/library/src/main/java/io/appwrite/services/PushCore.kt index 15f86b6a..bc151295 100644 --- a/library/src/main/java/io/appwrite/services/PushCore.kt +++ b/library/src/main/java/io/appwrite/services/PushCore.kt @@ -54,19 +54,36 @@ data class PushMessage( /** * What background delivery can rely on. Without exact alarms, the scheduled wake-ups are inexact * and Doze can defer them, so delivery is [bestEffort] unless foreground mode keeps the - * connection open. + * connection open. When the user restricted background usage, delivery is [bestEffort] unless the + * app is also exempt from battery optimisation, and foreground mode no longer counts: Android stops + * a restricted app's foreground service once it leaves the screen, exemption or not. */ -data class PushBackgroundStatus( +data class PushBackgroundStatus @JvmOverloads constructor( /** The app may schedule exact alarms (`SCHEDULE_EXACT_ALARM`, granted). */ val exactAlarms: Boolean, /** The app is exempt from battery optimisation. */ val ignoringBatteryOptimizations: Boolean, /** Foreground mode (`setForeground(true)`) keeps the connection open in a service. */ val foregroundService: Boolean, + /** + * The user restricted the app's background usage in its battery settings (Android 9+). Android + * may then hold its alarms, jobs and foreground service while it is closed, so messages can + * wait until the app is opened. The battery-optimisation exemption lets the alarms and jobs + * run again, but foreground mode still stops once the app leaves the screen. + */ + val backgroundRestricted: Boolean = false, ) { - /** Wake-ups may be deferred by Doze, so messages can arrive late while the app is closed. */ + /** + * Messages can arrive late, or not until the app is opened, while the app is closed: background + * usage is restricted without the battery-optimisation exemption, or wake-ups may be deferred + * by Doze. + */ val bestEffort: Boolean - get() = !exactAlarms && !foregroundService + get() { + val held = backgroundRestricted && !ignoringBatteryOptimizations + val foreground = foregroundService && !backgroundRestricted + return held || (!exactAlarms && !foreground) + } } /** Connection parameters shared between the in-process client and the foreground service. */ diff --git a/library/src/main/java/io/appwrite/services/Teams.kt b/library/src/main/java/io/appwrite/services/Teams.kt index 7227eba6..8a4bb87d 100644 --- a/library/src/main/java/io/appwrite/services/Teams.kt +++ b/library/src/main/java/io/appwrite/services/Teams.kt @@ -539,6 +539,7 @@ class Teams(client: Client) : Service(client) { * @param phone Phone number. Format this number with a leading '+' and a country code, e.g., +16175551212. * @param url URL to redirect the user back to your app from the invitation email. This parameter is not required when an API key is supplied. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. * @param name Name of the new team member. Max length: 128 chars. + * @param membershipId Membership ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars. * @return [io.appwrite.models.Membership] */ @JvmOverloads @@ -550,6 +551,7 @@ class Teams(client: Client) : Service(client) { phone: String? = null, url: String? = null, name: String? = null, + membershipId: String? = null, ): io.appwrite.models.Membership { if (teamId.isEmpty()) { throw AppwriteException("Missing required parameter: \"teamId\"") @@ -564,6 +566,7 @@ class Teams(client: Client) : Service(client) { "roles" to roles, "url" to url, "name" to name, + "membershipId" to membershipId, ) val apiHeaders = mutableMapOf( "X-Appwrite-Project" to client.config["project"].orEmpty(), diff --git a/library/src/test/java/io/appwrite/QueryTest.kt b/library/src/test/java/io/appwrite/QueryTest.kt new file mode 100644 index 00000000..a3662b7b --- /dev/null +++ b/library/src/test/java/io/appwrite/QueryTest.kt @@ -0,0 +1,171 @@ +package io.appwrite + +import com.google.gson.JsonParser +import org.junit.Assert.assertEquals +import org.junit.Test + +class QueryTest { + private fun method(json: String): String = + JsonParser.parseString(json).asJsonObject.get("method").asString + + @Test + fun countDistinct() { + assertEquals("countDistinct", method(Query.countDistinct("year", "uniqueYears"))) + } + + @Test + fun sum() { + assertEquals("sum", method(Query.sum("price", "total"))) + } + + @Test + fun avg() { + assertEquals("avg", method(Query.avg("price", "avgPrice"))) + } + + @Test + fun min() { + assertEquals("min", method(Query.min("price", "lowest"))) + } + + @Test + fun max() { + assertEquals("max", method(Query.max("price", "highest"))) + } + + @Test + fun stddev() { + assertEquals("stddev", method(Query.stddev("price", "sd"))) + } + + @Test + fun stddevPop() { + assertEquals("stddevPop", method(Query.stddevPop("price", "sdp"))) + } + + @Test + fun stddevSamp() { + assertEquals("stddevSamp", method(Query.stddevSamp("price", "sds"))) + } + + @Test + fun variance() { + assertEquals("variance", method(Query.variance("price", "var"))) + } + + @Test + fun varPop() { + assertEquals("varPop", method(Query.varPop("price", "vp"))) + } + + @Test + fun varSamp() { + assertEquals("varSamp", method(Query.varSamp("price", "vs"))) + } + + @Test + fun bitAnd() { + assertEquals("bitAnd", method(Query.bitAnd("flags", "band"))) + } + + @Test + fun bitOr() { + assertEquals("bitOr", method(Query.bitOr("flags", "bor"))) + } + + @Test + fun bitXor() { + assertEquals("bitXor", method(Query.bitXor("flags", "bxor"))) + } + + @Test + fun having() { + val json = JsonParser.parseString(Query.having(listOf(Query.greaterThan("total", 1)))).asJsonObject + assertEquals("having", json.get("method").asString) + assertEquals("greaterThan", json.get("values").asJsonArray[0].asJsonObject.get("method").asString) + } + + @Test + fun leftJoin() { + assertEquals("leftJoin", method(Query.leftJoin("orders", "\$id", "customerId", "=", "ord"))) + } + + @Test + fun rightJoin() { + assertEquals("rightJoin", method(Query.rightJoin("orders", "\$id", "customerId"))) + } + + @Test + fun fullOuterJoin() { + assertEquals("fullOuterJoin", method(Query.fullOuterJoin("orders", "\$id", "customerId"))) + } + + @Test + fun crossJoin() { + assertEquals("crossJoin", method(Query.crossJoin("orders", "ord"))) + } + + @Test + fun on() { + assertEquals("on", method(Query.on("\$id", "customerId"))) + } + + @Test + fun relationship() { + val query = Query.relationship("comments", listOf(Query.equal("approved", true), Query.limit(2))) + val json = JsonParser.parseString(query).asJsonObject + assertEquals("relationship", json.get("method").asString) + assertEquals("comments", json.get("attribute").asString) + val values = json.get("values").asJsonArray + assertEquals("equal", values[0].asJsonObject.get("method").asString) + assertEquals("limit", values[1].asJsonObject.get("method").asString) + assertEquals(listOf(query), Query.builder().relationship("comments", listOf(Query.equal("approved", true), Query.limit(2))).build()) + } + + @Test + fun nestedLeftJoin() { + val json = JsonParser.parseString( + Query.leftJoin( + "orders", + "ord", + listOf( + Query.on("\$id", "customerId"), + Query.equal("ord.status", "paid"), + ) + ) + ).asJsonObject + assertEquals("leftJoin", json.get("method").asString) + val values = json.get("values").asJsonArray + assertEquals("ord", values[0].asString) + assertEquals("on", values[1].asJsonObject.get("method").asString) + assertEquals("equal", values[2].asJsonObject.get("method").asString) + } + + @Test + fun notCovers() { + assertEquals("notCovers", method(Query.notCovers("location", listOf(1, 2)))) + } + + @Test + fun spatialEquals() { + assertEquals("spatialEquals", method(Query.spatialEquals("location", listOf(1, 2)))) + } + + @Test + fun notSpatialEquals() { + assertEquals("notSpatialEquals", method(Query.notSpatialEquals("location", listOf(1, 2)))) + } + + @Test + fun page() { + val queries = Query.page(2, 10) + assertEquals(2, queries.size) + assertEquals("limit", method(queries[0])) + assertEquals("offset", method(queries[1])) + } + + @Test(expected = ArithmeticException::class) + fun pageRejectsAnOffsetPastIntMax() { + Query.page(50_000_000, 50) + } +} diff --git a/library/src/test/java/io/appwrite/TopicTest.kt b/library/src/test/java/io/appwrite/TopicTest.kt index 0dd65bf0..3c2a51c4 100644 --- a/library/src/test/java/io/appwrite/TopicTest.kt +++ b/library/src/test/java/io/appwrite/TopicTest.kt @@ -7,7 +7,7 @@ import org.junit.Test class TopicTest { private fun assertPathFails(levels: List) { assertThrows(IllegalArgumentException::class.java) { Topic.path(levels) } - assertThrows(IllegalArgumentException::class.java) { Topic.any().path(levels) } + assertThrows(IllegalArgumentException::class.java) { Topic.path(listOf("user")).path(levels) } } @Test @@ -23,23 +23,6 @@ class TopicTest { ) } - @Test - fun anyAppendsSingleLevelWildcard() { - assertEquals("user/+/notification", Topic.path(listOf("user")).any().path(listOf("notification")).toString()) - assertEquals("chat/+/+/message", Topic.path(listOf("chat")).any().any().path(listOf("message")).toString()) - } - - @Test - fun allEndsTopic() { - assertEquals("org/+/logs/#", Topic.path(listOf("org")).any().path(listOf("logs")).all().toString()) - } - - @Test - fun staticAnyAndAll() { - assertEquals("+/notification", Topic.any().path(listOf("notification")).toString()) - assertEquals("#", Topic.all().toString()) - } - @Test fun pathRejectsInvalidLevels() { assertPathFails(emptyList()) @@ -49,5 +32,6 @@ class TopicTest { assertPathFails(listOf("user", "a+b")) assertPathFails(listOf("user", "#")) assertPathFails(listOf("a#b")) + assertPathFails(listOf("user", "a\u0000b")) } }