mirror of
https://github.com/appwrite/sdk-for-android.git
synced 2026-04-07 19:17:49 +00:00
implement realtime feature
This commit is contained in:
@@ -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 }}
|
||||
SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }}
|
||||
SDK_VERSION: ${{ github.event.release.tag_name }}
|
||||
@@ -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:
|
||||
<dependency>
|
||||
<groupId>io.appwrite</groupId>
|
||||
<artifactId>sdk-for-android</artifactId>
|
||||
<version>0.0.1</version>
|
||||
<version>0.1.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
@@ -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)
|
||||
|
||||
+1
-1
@@ -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()
|
||||
|
||||
@@ -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<Object>() {
|
||||
@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());
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<Object>() {
|
||||
@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());
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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<AppwriteException>()
|
||||
} else {
|
||||
Error(bodyString, response.code)
|
||||
AppwriteException(bodyString, response.code)
|
||||
}
|
||||
|
||||
it.cancel(AppwriteException(
|
||||
|
||||
@@ -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 <T> Collection<T>.forEachAsync(
|
||||
callback: suspend (T) -> Unit
|
||||
) = withContext(IO) {
|
||||
map { async { callback.invoke(it) } }.awaitAll()
|
||||
}
|
||||
@@ -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 <T> String.fromJson(clazz: Class<T>): T =
|
||||
Gson().fromJson(this, clazz)
|
||||
}
|
||||
fun <T> String.fromJson(clazz: Class<T>): T =
|
||||
gson.fromJson(this, clazz)
|
||||
|
||||
inline fun <reified T> String.fromJson(): T =
|
||||
gson.fromJson(this, T::class.java)
|
||||
|
||||
fun <T> Any.jsonCast(to: Class<T>): T =
|
||||
toJson().fromJson(to)
|
||||
|
||||
inline fun <reified T> Any.jsonCast(): T =
|
||||
toJson().fromJson(T::class.java)
|
||||
|
||||
fun <T> Any.tryJsonCast(to: Class<T>): T? = try {
|
||||
toJson().fromJson(to)
|
||||
} catch (ex: Exception) {
|
||||
ex.printStackTrace()
|
||||
null
|
||||
}
|
||||
|
||||
inline fun <reified T> Any.tryJsonCast(): T? = try {
|
||||
toJson().fromJson(T::class.java)
|
||||
} catch (ex: Exception) {
|
||||
ex.printStackTrace()
|
||||
null
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package io.appwrite.models
|
||||
|
||||
data class Error(
|
||||
val message: String,
|
||||
val code: Int
|
||||
)
|
||||
@@ -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<T>(
|
||||
val event: String,
|
||||
val channels: Collection<String>,
|
||||
val timestamp: Long,
|
||||
var payload: T
|
||||
)
|
||||
|
||||
enum class RealtimeCode(val value: Int) {
|
||||
POLICY_VIOLATION(1008),
|
||||
UNKNOWN_ERROR(-1)
|
||||
}
|
||||
@@ -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<String, Any?>(
|
||||
"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<String, Any?>(
|
||||
"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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<String, MutableCollection<RealtimeCallback>>()
|
||||
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<Any>) -> Unit,
|
||||
) = subscribe(
|
||||
channels = channels,
|
||||
Any::class.java,
|
||||
callback
|
||||
)
|
||||
|
||||
fun <T> subscribe(
|
||||
vararg channels: String,
|
||||
payloadType: Class<T>,
|
||||
callback: (RealtimeResponseEvent<T>) -> 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<RealtimeResponse>()
|
||||
when (message.type) {
|
||||
TYPE_ERROR -> handleResponseError(message)
|
||||
TYPE_EVENT -> handleResponseEvent(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleResponseError(message: RealtimeResponse) {
|
||||
val error = message.data.jsonCast<AppwriteException>()
|
||||
errorCallbacks.forEach { it.invoke(error) }
|
||||
}
|
||||
|
||||
private suspend fun handleResponseEvent(message: RealtimeResponse) {
|
||||
val event = message.data.jsonCast<RealtimeResponseEvent<Any>>()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user