diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index b777478..20b880d 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -50,4 +50,5 @@ jobs:
SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }}
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }}
- SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }}
\ No newline at end of file
+ SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }}
+ SDK_VERSION: ${{ github.event.release.tag_name }}
\ No newline at end of file
diff --git a/README.md b/README.md
index 66da83e..174b5d0 100644
--- a/README.md
+++ b/README.md
@@ -2,12 +2,12 @@


-
+
[](https://travis-ci.com/appwrite/sdk-generator)
[](https://twitter.com/appwrite_io)
[](https://appwrite.io/discord)
-**This SDK is compatible with Appwrite server version 0.9.x. For older versions, please check [previous releases](https://github.com/appwrite/sdk-for-android/releases).**
+**This SDK is compatible with Appwrite server version 0.10.x. For older versions, please check [previous releases](https://github.com/appwrite/sdk-for-android/releases).**
Appwrite is an open-source backend as a service server that abstract and simplify 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:0.0.1")
+implementation("io.appwrite:sdk-for-android:0.1.0")
```
### Maven
@@ -49,7 +49,7 @@ Add this to your project's `pom.xml` file:
io.appwrite
sdk-for-android
- 0.0.1
+ 0.1.0
```
@@ -144,7 +144,7 @@ try {
```
### Learn more
-You can use following resources to learn more and get help
+You can use the following resources to learn more and get help
- 🚀 [Getting Started Tutorial](https://appwrite.io/docs/getting-started-for-android)
- 📜 [Appwrite Docs](https://appwrite.io/docs)
- 💬 [Discord Community](https://appwrite.io/discord)
diff --git a/build.gradle b/build.gradle
index ee004cf..a387c9f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -3,7 +3,7 @@ apply plugin: 'io.github.gradle-nexus.publish-plugin'
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.4.31"
- version '0.0.1'
+ version '0.1.0'
repositories {
maven { url "https://plugins.gradle.org/m2/" }
google()
diff --git a/docs/examples/java/account/create-magic-u-r-l-session.md b/docs/examples/java/account/create-magic-u-r-l-session.md
new file mode 100644
index 0000000..36fc58a
--- /dev/null
+++ b/docs/examples/java/account/create-magic-u-r-l-session.md
@@ -0,0 +1,48 @@
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import io.appwrite.Client
+import io.appwrite.services.Account
+
+public class MainActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ Client client = new Client(getApplicationContext())
+ .setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
+ .setProject("5df5acd0d48c2"); // Your project ID
+
+ Account account = new Account(client);
+
+ account.createMagicURLSession(
+ "email@example.com",
+ new Continuation() {
+ @NotNull
+ @Override
+ public CoroutineContext getContext() {
+ return EmptyCoroutineContext.INSTANCE;
+ }
+
+ @Override
+ public void resumeWith(@NotNull Object o) {
+ String json = "";
+ try {
+ if (o instanceof Result.Failure) {
+ Result.Failure failure = (Result.Failure) o;
+ throw failure.exception;
+ } else {
+ Response response = (Response) o;
+ json = response.body().string();
+ }
+ } catch (Throwable th) {
+ Log.e("ERROR", th.toString());
+ }
+ }
+ }
+ );
+ }
+}
\ No newline at end of file
diff --git a/docs/examples/java/account/update-magic-u-r-l-session.md b/docs/examples/java/account/update-magic-u-r-l-session.md
new file mode 100644
index 0000000..d0734e1
--- /dev/null
+++ b/docs/examples/java/account/update-magic-u-r-l-session.md
@@ -0,0 +1,49 @@
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import io.appwrite.Client
+import io.appwrite.services.Account
+
+public class MainActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ Client client = new Client(getApplicationContext())
+ .setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
+ .setProject("5df5acd0d48c2"); // Your project ID
+
+ Account account = new Account(client);
+
+ account.updateMagicURLSession(
+ "[USER_ID]",
+ "[SECRET]"
+ new Continuation() {
+ @NotNull
+ @Override
+ public CoroutineContext getContext() {
+ return EmptyCoroutineContext.INSTANCE;
+ }
+
+ @Override
+ public void resumeWith(@NotNull Object o) {
+ String json = "";
+ try {
+ if (o instanceof Result.Failure) {
+ Result.Failure failure = (Result.Failure) o;
+ throw failure.exception;
+ } else {
+ Response response = (Response) o;
+ json = response.body().string();
+ }
+ } catch (Throwable th) {
+ Log.e("ERROR", th.toString());
+ }
+ }
+ }
+ );
+ }
+}
\ No newline at end of file
diff --git a/docs/examples/kotlin/account/create-magic-u-r-l-session.md b/docs/examples/kotlin/account/create-magic-u-r-l-session.md
new file mode 100644
index 0000000..c803d1a
--- /dev/null
+++ b/docs/examples/kotlin/account/create-magic-u-r-l-session.md
@@ -0,0 +1,26 @@
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import io.appwrite.Client
+import io.appwrite.services.Account
+
+class MainActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ val client = Client(applicationContext)
+ .setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
+ .setProject("5df5acd0d48c2") // Your project ID
+
+ val account = Account(client)
+
+ GlobalScope.launch {
+ val response = account.createMagicURLSession(
+ email = "email@example.com",
+ )
+ val json = response.body?.string()
+ }
+ }
+}
\ No newline at end of file
diff --git a/docs/examples/kotlin/account/update-magic-u-r-l-session.md b/docs/examples/kotlin/account/update-magic-u-r-l-session.md
new file mode 100644
index 0000000..3fe1b7c
--- /dev/null
+++ b/docs/examples/kotlin/account/update-magic-u-r-l-session.md
@@ -0,0 +1,27 @@
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import io.appwrite.Client
+import io.appwrite.services.Account
+
+class MainActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ val client = Client(applicationContext)
+ .setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
+ .setProject("5df5acd0d48c2") // Your project ID
+
+ val account = Account(client)
+
+ GlobalScope.launch {
+ val response = account.updateMagicURLSession(
+ userId = "[USER_ID]",
+ secret = "[SECRET]"
+ )
+ val json = response.body?.string()
+ }
+ }
+}
\ No newline at end of file
diff --git a/library/build.gradle b/library/build.gradle
index d5e9a7c..9ae290e 100644
--- a/library/build.gradle
+++ b/library/build.gradle
@@ -7,7 +7,7 @@ plugins {
ext {
PUBLISH_GROUP_ID = 'io.appwrite'
PUBLISH_ARTIFACT_ID = 'sdk-for-android'
- PUBLISH_VERSION = '0.0.1'
+ PUBLISH_VERSION = System.getenv('SDK_VERSION')
POM_URL = 'https://github.com/appwrite/sdk-for-android'
POM_SCM_URL = 'https://github.com/appwrite/sdk-for-android'
POM_ISSUE_URL = 'https://github.com/appwrite/sdk-for-android/issues'
@@ -53,8 +53,8 @@ android {
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${KotlinCompilerVersion.VERSION}")
- api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3")
- api("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3")
+ api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1")
+ api("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0")
api(platform("com.squareup.okhttp3:okhttp-bom:4.9.0"))
api("com.squareup.okhttp3:okhttp")
@@ -65,15 +65,16 @@ dependencies {
implementation("net.gotev:cookie-store:1.3.5")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1")
implementation("androidx.lifecycle:lifecycle-common-java8:2.3.1")
- implementation("androidx.appcompat:appcompat:1.2.0")
- implementation("androidx.fragment:fragment-ktx:1.3.2")
- implementation("androidx.activity:activity-ktx:1.2.2")
+ implementation("androidx.appcompat:appcompat:1.3.1")
+ implementation("androidx.fragment:fragment-ktx:1.3.6")
+ implementation("androidx.activity:activity-ktx:1.3.1")
implementation("androidx.browser:browser:1.3.0")
testImplementation 'junit:junit:4.+'
- testImplementation "androidx.test.ext:junit-ktx:1.1.2"
- testImplementation "androidx.test:core-ktx:1.3.0"
+ testImplementation "androidx.test.ext:junit-ktx:1.1.3"
+ testImplementation "androidx.test:core-ktx:1.4.0"
testImplementation "org.robolectric:robolectric:4.5.1"
+ testApi("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1")
}
apply from: "${rootProject.projectDir}/scripts/publish-module.gradle"
\ No newline at end of file
diff --git a/library/src/main/java/io/appwrite/Client.kt b/library/src/main/java/io/appwrite/Client.kt
index e15f2a5..0e55ec7 100644
--- a/library/src/main/java/io/appwrite/Client.kt
+++ b/library/src/main/java/io/appwrite/Client.kt
@@ -5,8 +5,7 @@ import android.content.pm.PackageManager
import com.google.gson.Gson
import io.appwrite.appwrite.BuildConfig
import io.appwrite.exceptions.AppwriteException
-import io.appwrite.extensions.JsonExtensions.fromJson
-import io.appwrite.models.Error
+import io.appwrite.extensions.fromJson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -36,6 +35,7 @@ import kotlin.coroutines.resume
class Client @JvmOverloads constructor(
context: Context,
var endPoint: String = "https://appwrite.io/v1",
+ var endPointRealtime: String? = null,
private var selfSigned: Boolean = false
) : CoroutineScope {
@@ -71,7 +71,7 @@ class Client @JvmOverloads constructor(
"origin" to "appwrite-android://${context.packageName}",
"user-agent" to "${context.packageName}/${appVersion}, ${System.getProperty("http.agent")}",
"x-sdk-version" to "appwrite:android:${BuildConfig.SDK_VERSION}",
- "x-appwrite-response-format" to "0.9.0"
+ "x-appwrite-response-format" to "0.10.0"
)
config = mutableMapOf()
@@ -171,7 +171,7 @@ class Client @JvmOverloads constructor(
}
/**
- * Set endpoint
+ * Set endpoint and realtime endpoint.
*
* @param endpoint
*
@@ -179,6 +179,23 @@ class Client @JvmOverloads constructor(
*/
fun setEndpoint(endPoint: String): Client {
this.endPoint = endPoint
+
+ if (this.endPointRealtime == null && endPoint.startsWith("http")) {
+ this.endPointRealtime = endPoint.replaceFirst("http", "ws")
+ }
+
+ return this
+ }
+
+ /**
+ * Set realtime endpoint
+ *
+ * @param endpoint
+ *
+ * @return this
+ */
+ fun setEndpointRealtime(endPoint: String): Client {
+ this.endPointRealtime = endPoint
return this
}
@@ -317,9 +334,9 @@ class Client @JvmOverloads constructor(
val contentType: String = response.headers["content-type"] ?: ""
val error = if (contentType.contains("application/json", ignoreCase = true)) {
- bodyString.fromJson(Error::class.java)
+ bodyString.fromJson()
} else {
- Error(bodyString, response.code)
+ AppwriteException(bodyString, response.code)
}
it.cancel(AppwriteException(
diff --git a/library/src/main/java/io/appwrite/extensions/CollectionExtensions.kt b/library/src/main/java/io/appwrite/extensions/CollectionExtensions.kt
new file mode 100644
index 0000000..a92704f
--- /dev/null
+++ b/library/src/main/java/io/appwrite/extensions/CollectionExtensions.kt
@@ -0,0 +1,12 @@
+package io.appwrite.extensions
+
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.withContext
+
+suspend fun Collection.forEachAsync(
+ callback: suspend (T) -> Unit
+) = withContext(IO) {
+ map { async { callback.invoke(it) } }.awaitAll()
+}
\ No newline at end of file
diff --git a/library/src/main/java/io/appwrite/extensions/JsonExtensions.kt b/library/src/main/java/io/appwrite/extensions/JsonExtensions.kt
index 4cf0f4f..444a77d 100644
--- a/library/src/main/java/io/appwrite/extensions/JsonExtensions.kt
+++ b/library/src/main/java/io/appwrite/extensions/JsonExtensions.kt
@@ -2,11 +2,33 @@ package io.appwrite.extensions
import com.google.gson.Gson
-object JsonExtensions {
+val gson = Gson()
- fun Any.toJson(): String =
- Gson().toJson(this)
+fun Any.toJson(): String =
+ gson.toJson(this)
- fun String.fromJson(clazz: Class): T =
- Gson().fromJson(this, clazz)
-}
\ No newline at end of file
+fun String.fromJson(clazz: Class): T =
+ gson.fromJson(this, clazz)
+
+inline fun String.fromJson(): T =
+ gson.fromJson(this, T::class.java)
+
+fun Any.jsonCast(to: Class): T =
+ toJson().fromJson(to)
+
+inline fun Any.jsonCast(): T =
+ toJson().fromJson(T::class.java)
+
+fun Any.tryJsonCast(to: Class): T? = try {
+ toJson().fromJson(to)
+} catch (ex: Exception) {
+ ex.printStackTrace()
+ null
+}
+
+inline fun Any.tryJsonCast(): T? = try {
+ toJson().fromJson(T::class.java)
+} catch (ex: Exception) {
+ ex.printStackTrace()
+ null
+}
diff --git a/library/src/main/java/io/appwrite/models/Error.kt b/library/src/main/java/io/appwrite/models/Error.kt
deleted file mode 100644
index f058eb5..0000000
--- a/library/src/main/java/io/appwrite/models/Error.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package io.appwrite.models
-
-data class Error(
- val message: String,
- val code: Int
-)
\ No newline at end of file
diff --git a/library/src/main/java/io/appwrite/models/RealtimeModels.kt b/library/src/main/java/io/appwrite/models/RealtimeModels.kt
new file mode 100644
index 0000000..b4990c8
--- /dev/null
+++ b/library/src/main/java/io/appwrite/models/RealtimeModels.kt
@@ -0,0 +1,31 @@
+package io.appwrite.models
+
+import java.io.Closeable
+
+data class RealtimeSubscription(
+ private val close: () -> Unit
+) : Closeable {
+ override fun close() = close.invoke()
+}
+
+data class RealtimeCallback(
+ val payloadClass: Class<*>,
+ val callback: (RealtimeResponseEvent<*>) -> Unit
+)
+
+open class RealtimeResponse(
+ val type: String,
+ val data: Any
+)
+
+data class RealtimeResponseEvent(
+ val event: String,
+ val channels: Collection,
+ val timestamp: Long,
+ var payload: T
+)
+
+enum class RealtimeCode(val value: Int) {
+ POLICY_VIOLATION(1008),
+ UNKNOWN_ERROR(-1)
+}
\ No newline at end of file
diff --git a/library/src/main/java/io/appwrite/services/Account.kt b/library/src/main/java/io/appwrite/services/Account.kt
index bad36e5..89206d3 100644
--- a/library/src/main/java/io/appwrite/services/Account.kt
+++ b/library/src/main/java/io/appwrite/services/Account.kt
@@ -11,7 +11,7 @@ import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import java.io.File
-class Account(private val client: Client) : BaseService(client) {
+class Account(client: Client) : Service(client) {
/**
* Get Account
@@ -313,7 +313,7 @@ class Account(private val client: Client) : BaseService(client) {
}
/**
- * Complete Password Recovery
+ * Create Password Recovery (confirmation)
*
* Use this endpoint to complete the user account password reset. Both the
* **userId** and **secret** arguments will be passed as query parameters to
@@ -453,6 +453,81 @@ class Account(private val client: Client) : BaseService(client) {
return client.call("POST", path, headers, params)
}
+ /**
+ * Create Magic URL session
+ *
+ * Sends the user an email with a secret key for creating a session. When the
+ * user clicks the link in the email, the user is redirected back to the URL
+ * you provided with the secret key and userId values attached to the URL
+ * query string. Use the query string parameters to submit a request to the
+ * [PUT
+ * /account/sessions/magic-url](/docs/client/account#accountUpdateMagicURLSession)
+ * endpoint to complete the login process. The link sent to the user's email
+ * address is valid for 1 hour. If you are on a mobile device you can leave
+ * the URL parameter empty, so that the login completion will be handled by
+ * your Appwrite instance by default.
+ *
+ * @param email
+ * @param url
+ * @return [Response]
+ */
+ @JvmOverloads
+ @Throws(AppwriteException::class)
+ suspend fun createMagicURLSession(
+ email: String,
+ url: String? = null
+ ): Response {
+ val path = "/account/sessions/magic-url"
+ val params = mapOf(
+ "email" to email,
+ "url" to url
+ )
+
+ val headers = mapOf(
+ "content-type" to "application/json"
+ )
+
+ return client.call("POST", path, headers, params)
+ }
+
+ /**
+ * Create Magic URL session (confirmation)
+ *
+ * Use this endpoint to complete creating the session with the Magic URL. Both
+ * the **userId** and **secret** arguments will be passed as query parameters
+ * to the redirect URL you have provided when sending your request to the
+ * [POST
+ * /account/sessions/magic-url](/docs/client/account#accountCreateMagicURLSession)
+ * endpoint.
+ *
+ * Please note that in order to avoid a [Redirect
+ * Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md)
+ * the only valid redirect URLs are the ones from domains you have set when
+ * adding your platforms in the console interface.
+ *
+ * @param userId
+ * @param secret
+ * @return [Response]
+ */
+ @JvmOverloads
+ @Throws(AppwriteException::class)
+ suspend fun updateMagicURLSession(
+ userId: String,
+ secret: String
+ ): Response {
+ val path = "/account/sessions/magic-url"
+ val params = mapOf(
+ "userId" to userId,
+ "secret" to secret
+ )
+
+ val headers = mapOf(
+ "content-type" to "application/json"
+ )
+
+ return client.call("PUT", path, headers, params)
+ }
+
/**
* Create Account Session with OAuth2
*
@@ -460,6 +535,14 @@ class Account(private val client: Client) : BaseService(client) {
* choice. Each OAuth2 provider should be enabled from the Appwrite console
* first. Use the success and failure arguments to provide a redirect URL's
* back to your app when login is completed.
+ *
+ * If there is already an active session, the new session will be attached to
+ * the logged-in account. If there are no active sessions, the server will
+ * attempt to look for a user with the same email address as the email
+ * received from the OAuth2 provider and attach the new session to the
+ * existing user. If no matching user is found - the server will create a new
+ * user..
+ *
*
* @param provider
* @param success
@@ -620,7 +703,7 @@ class Account(private val client: Client) : BaseService(client) {
}
/**
- * Complete Email Verification
+ * Create Email Verification (confirmation)
*
* Use this endpoint to complete the user email verification process. Use both
* the **userId** and **secret** parameters that were attached to your app URL
diff --git a/library/src/main/java/io/appwrite/services/Avatars.kt b/library/src/main/java/io/appwrite/services/Avatars.kt
index b828eb0..3e0432f 100644
--- a/library/src/main/java/io/appwrite/services/Avatars.kt
+++ b/library/src/main/java/io/appwrite/services/Avatars.kt
@@ -9,7 +9,7 @@ import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import java.io.File
-class Avatars(private val client: Client) : BaseService(client) {
+class Avatars(client: Client) : Service(client) {
/**
* Get Browser Icon
diff --git a/library/src/main/java/io/appwrite/services/Database.kt b/library/src/main/java/io/appwrite/services/Database.kt
index 81bf0c6..6333532 100644
--- a/library/src/main/java/io/appwrite/services/Database.kt
+++ b/library/src/main/java/io/appwrite/services/Database.kt
@@ -7,7 +7,7 @@ import okhttp3.Cookie
import okhttp3.Response
import java.io.File
-class Database(private val client: Client) : BaseService(client) {
+class Database(client: Client) : Service(client) {
/**
* List Documents
diff --git a/library/src/main/java/io/appwrite/services/Functions.kt b/library/src/main/java/io/appwrite/services/Functions.kt
index 95c203c..95a915f 100644
--- a/library/src/main/java/io/appwrite/services/Functions.kt
+++ b/library/src/main/java/io/appwrite/services/Functions.kt
@@ -7,7 +7,7 @@ import okhttp3.Cookie
import okhttp3.Response
import java.io.File
-class Functions(private val client: Client) : BaseService(client) {
+class Functions(client: Client) : Service(client) {
/**
* List Executions
diff --git a/library/src/main/java/io/appwrite/services/Locale.kt b/library/src/main/java/io/appwrite/services/Locale.kt
index 36a3fce..415df78 100644
--- a/library/src/main/java/io/appwrite/services/Locale.kt
+++ b/library/src/main/java/io/appwrite/services/Locale.kt
@@ -7,7 +7,7 @@ import okhttp3.Cookie
import okhttp3.Response
import java.io.File
-class Locale(private val client: Client) : BaseService(client) {
+class Locale(client: Client) : Service(client) {
/**
* Get User Locale
diff --git a/library/src/main/java/io/appwrite/services/Realtime.kt b/library/src/main/java/io/appwrite/services/Realtime.kt
new file mode 100644
index 0000000..cec1dad
--- /dev/null
+++ b/library/src/main/java/io/appwrite/services/Realtime.kt
@@ -0,0 +1,174 @@
+package io.appwrite.services
+
+import io.appwrite.Client
+import io.appwrite.exceptions.AppwriteException
+import io.appwrite.extensions.forEachAsync
+import io.appwrite.extensions.fromJson
+import io.appwrite.extensions.jsonCast
+import io.appwrite.models.*
+import kotlinx.coroutines.*
+import kotlinx.coroutines.Dispatchers.IO
+import okhttp3.Request
+import okhttp3.Response
+import okhttp3.WebSocket
+import okhttp3.WebSocketListener
+import okhttp3.internal.concurrent.TaskRunner
+import okhttp3.internal.ws.RealWebSocket
+import java.util.*
+import kotlin.coroutines.CoroutineContext
+
+class Realtime(client: Client) : Service(client), CoroutineScope {
+
+ private val job = Job()
+
+ override val coroutineContext: CoroutineContext
+ get() = Dispatchers.Main + job
+
+ private companion object {
+ private const val TYPE_ERROR = "error"
+ private const val TYPE_EVENT = "event"
+
+ private const val DEBOUNCE_MILLIS = 1L
+
+ private var socket: RealWebSocket? = null
+ private var channelCallbacks = mutableMapOf>()
+ private var errorCallbacks = mutableSetOf<(AppwriteException) -> Unit>()
+
+ private var subCallDepth = 0
+ }
+
+ private fun createSocket() {
+ val queryParamBuilder = StringBuilder()
+ .append("project=${client.config["project"]}")
+
+ channelCallbacks.keys.forEach {
+ queryParamBuilder
+ .append("&channels[]=$it")
+ }
+
+ val request = Request.Builder()
+ .url("${client.endPointRealtime}/realtime?$queryParamBuilder")
+ .build()
+
+ if (socket != null) {
+ closeSocket()
+ }
+
+ socket = RealWebSocket(
+ taskRunner = TaskRunner.INSTANCE,
+ originalRequest = request,
+ listener = AppwriteWebSocketListener(),
+ random = Random(),
+ pingIntervalMillis = client.http.pingIntervalMillis.toLong(),
+ extensions = null,
+ minimumDeflateSize = client.http.minWebSocketMessageToCompress
+ )
+
+ socket!!.connect(client.http)
+ }
+
+ private fun closeSocket() {
+ socket?.close(RealtimeCode.POLICY_VIOLATION.value, null)
+ }
+
+ fun subscribe(
+ vararg channels: String,
+ callback: (RealtimeResponseEvent) -> Unit,
+ ) = subscribe(
+ channels = channels,
+ Any::class.java,
+ callback
+ )
+
+ fun subscribe(
+ vararg channels: String,
+ payloadType: Class,
+ callback: (RealtimeResponseEvent) -> Unit,
+ ): RealtimeSubscription {
+ channels.forEach {
+ if (!channelCallbacks.containsKey(it)) {
+ channelCallbacks[it] = mutableListOf(
+ RealtimeCallback(
+ payloadType,
+ callback as (RealtimeResponseEvent<*>) -> Unit
+ )
+ )
+ return@forEach
+ }
+ channelCallbacks[it]?.add(
+ RealtimeCallback(payloadType, callback as (RealtimeResponseEvent<*>) -> Unit)
+ )
+ }
+
+ launch {
+ subCallDepth++
+ delay(DEBOUNCE_MILLIS)
+ if (subCallDepth == 1) {
+ createSocket()
+ }
+ subCallDepth--
+ }
+
+ return RealtimeSubscription { unsubscribe(*channels) }
+ }
+
+ fun unsubscribe(vararg channels: String) {
+ channels.forEach {
+ channelCallbacks[it] = mutableListOf()
+ }
+ if (channelCallbacks.all { it.value.isEmpty() }) {
+ errorCallbacks = mutableSetOf()
+ closeSocket()
+ }
+ }
+
+ fun doOnError(callback: (AppwriteException) -> Unit) {
+ errorCallbacks.add(callback)
+ }
+
+ private inner class AppwriteWebSocketListener : WebSocketListener() {
+
+ override fun onMessage(webSocket: WebSocket, text: String) {
+ super.onMessage(webSocket, text)
+
+ launch(IO) {
+ val message = text.fromJson()
+ when (message.type) {
+ TYPE_ERROR -> handleResponseError(message)
+ TYPE_EVENT -> handleResponseEvent(message)
+ }
+ }
+ }
+
+ private fun handleResponseError(message: RealtimeResponse) {
+ val error = message.data.jsonCast()
+ errorCallbacks.forEach { it.invoke(error) }
+ }
+
+ private suspend fun handleResponseEvent(message: RealtimeResponse) {
+ val event = message.data.jsonCast>()
+ event.channels.forEachAsync { channel ->
+ channelCallbacks[channel]?.forEachAsync { callbackWrapper ->
+ event.payload = event.payload.jsonCast(callbackWrapper.payloadClass)
+ callbackWrapper.callback.invoke(event)
+ }
+ }
+ }
+
+ override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
+ super.onClosing(webSocket, code, reason)
+ if (code == RealtimeCode.POLICY_VIOLATION.value) {
+ return
+ }
+ launch {
+ delay(1000)
+ createSocket()
+ }
+ }
+
+ override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
+ super.onFailure(webSocket, t, response)
+ t.printStackTrace()
+ }
+ }
+}
\ No newline at end of file
diff --git a/library/src/main/java/io/appwrite/services/BaseService.kt b/library/src/main/java/io/appwrite/services/Service.kt
similarity index 50%
rename from library/src/main/java/io/appwrite/services/BaseService.kt
rename to library/src/main/java/io/appwrite/services/Service.kt
index 1d6df97..6a00fb1 100644
--- a/library/src/main/java/io/appwrite/services/BaseService.kt
+++ b/library/src/main/java/io/appwrite/services/Service.kt
@@ -2,4 +2,4 @@ package io.appwrite.services
import io.appwrite.Client
-abstract class BaseService(private val client: Client)
+abstract class Service(val client: Client)
diff --git a/library/src/main/java/io/appwrite/services/Storage.kt b/library/src/main/java/io/appwrite/services/Storage.kt
index 6b053d0..777b759 100644
--- a/library/src/main/java/io/appwrite/services/Storage.kt
+++ b/library/src/main/java/io/appwrite/services/Storage.kt
@@ -9,7 +9,7 @@ import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import java.io.File
-class Storage(private val client: Client) : BaseService(client) {
+class Storage(client: Client) : Service(client) {
/**
* List Files
diff --git a/library/src/main/java/io/appwrite/services/Teams.kt b/library/src/main/java/io/appwrite/services/Teams.kt
index 8e14663..b003bd9 100644
--- a/library/src/main/java/io/appwrite/services/Teams.kt
+++ b/library/src/main/java/io/appwrite/services/Teams.kt
@@ -7,7 +7,7 @@ import okhttp3.Cookie
import okhttp3.Response
import java.io.File
-class Teams(private val client: Client) : BaseService(client) {
+class Teams(client: Client) : Service(client) {
/**
* List Teams
@@ -195,14 +195,17 @@ class Teams(private val client: Client) : BaseService(client) {
/**
* Create Team Membership
*
- * Use this endpoint to invite a new member to join your team. An email with a
- * link to join the team will be sent to the new member email address if the
- * member doesn't exist in the project it will be created automatically.
+ * Use this endpoint to invite a new member to join your team. If initiated
+ * from Client SDK, an email with a link to join the team will be sent to the
+ * new member's email address if the member doesn't exist in the project it
+ * will be created automatically. If initiated from server side SDKs, new
+ * member will automatically be added to the team.
*
* Use the 'URL' parameter to redirect the user from the invitation email back
* to your app. When the user is redirected, use the [Update Team Membership
* Status](/docs/client/teams#teamsUpdateMembershipStatus) endpoint to allow
- * the user to accept the invitation to the team.
+ * the user to accept the invitation to the team. While calling from side
+ * SDKs the redirect url can be empty string.
*
* Please note that in order to avoid a [Redirect
* Attacks](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md)