Kotlin

Sariska provides a Kotlin API and easy-to-integrate SDKs for building real-time features into your android applications. Easily add in-app chats, instant messaging, and other real-time functionalities to your apps.

Key Features:

  • Real-time messaging for in-app chats and instant messaging

  • Easy installation

  • Socket creation and management

  • Channel creation, joining, and leaving

  • Sending messages, poll votes, and message replies

  • Presence management (track: typing, joining and leaving users)

  • History management (fetching chat history and specific messages)


Installation

Step 1 : Install the Phoenix Client

JavaPhoenixClient is hosted on MavenCentral. To install it, you'll need to add the following repositories and dependencies to your project's build configuration:

  • Repositories

repositories {
    mavenCentral()
}

  • Dependencies

dependencies {
  implementation 'com.github.dsrees:JavaPhoenixClient:0.5.0'
}

Step 2 : Create Socket

Establish a WebSocket connection to the Sariska server to join channels, receive events, and send messages.

// Define the authentication token
val token = "{your-auth-token}"
val params = hashMapOf("token" to token)
// Establish a WebSocket connection to the specified URL
val socket = Socket("wss://api.sariska.io/api/v1/messaging/websocket", params)
// Define the topic to subscribe to
val topic = "chat:Chat10Feb"
// Handle socket opening event
socket.onOpen { this.addText("Socket Opened") }
// Handle socket closing event
socket.onClose { this.addText("Socket Closed") }
// Handle socket error events
socket.onError { throwable, response -> Log.e(TAG, "Socket Errored $response", throwable) }

// Set logger to receive log messages from the WebSocket connection
socket.logger = { Log.d("TAG", it) }

// Open connection to the server 
socket.connect()

To disconnect the user from any previous WebSocket connections and tabs before opening a new one, set the disconnect_past_tabs parameter to true in the WebSocket connection.

Disconnect Socket

Close the WebSocket connection to the Sariska server. This will terminate all active channels and prevent further communication with the server.

// Close connection to the server
socket.disconnect()

Step 3 : Create Channel

Channels cannot be created directly; instead, they are created through the socket object by calling socket.channel(topic) with the topic of the channel. The topic is a string that identifies the channel, and it can be any string you like.

Channel Prefix

Each channel name starts with a prefix that determines the message serialization format and persistence behavior.

chat: Use this prefix for persisting messages to the database. This prefix requires a fixed schema and is ideal for chat applications.

 val channel = socket.channel("chat:room123");

rtc: Use this prefix for scenarios where message persistence is not necessary. This prefix allows sending arbitrary JSON data, making it suitable for events in multiplayer games, IoT applications, and others.

val channel = socket.channel("rtc:room123");

sariska: Use this prefix for performance-critical applications that utilize Flatbuffers as the serialization format and do not require message persistence. This prefix provides zero-copy serialization/deserialization, significantly enhancing performance.

val channel = socket.channel("sariska:room123");

Step 4 : Join and Leave Channel

To join a channel, call the join() method on the channel object. The join() method returns a promise that resolves when the client has successfully joined the channel. When sending data, you can utilize the .receive() hook to monitor the status of the outgoing push message.

// Join the Channel
channel.join()
    .receive("ok") { /* Joined the chatroom */ }
    .receive("error") { /* Failed to join the chatroom */ }

// Leave the Channel
channel.leave()

Channel User Joined

// Listen for the "user_joined" event on the channel
channel.on("user_joined") { message: Message ->
    // Extract the payload from the message
    val payload = message.payload
    // Extract user and room information from the payload
    val user = payload["user"]
    val room = payload["room"]
    // Print the user and room details to the console
    println(user)
    println(room)
}
Payload of user containing user details, including:
  • id: The user's ID

  • name: The user's name

  • avatar: The user's avatar URL

  • email: The user's email address

Payload of room containing room details, including:
  • id: The room's ID

  • session_id: The room's session ID

  • created_by: The ID of the user who created the room

  • status: The status of the room (e.g., "active", "inactive")

Channel New Message

// Listen for the "user_message" event on the channel
channel.on("new_message") { message: Message ->
    // Extract the payload from the message
    val payload = message.payload
    // Extract the username from the payload
    val username = payload["user"] as? String
    // Extract the message content from the payload
    val body = payload["content"]
    // Add the message content to the chat interface
    addText("$body")
}
Payload of message containing message details, including:
  • content: The message content

  • id: The sender's user ID

  • name: The sender's name

  • content_type: The message content type (media, emoji, text)

  • is_delivered: Whether the message has been delivered to the recipient

  • created_by: The ID of the user who sent the message

  • created_by_name: The name of the user who sent the message

  • timestamp: The message timestamp

Send Message

Once you've established a connection to a channel, you can start sending messages to other connected clients. To send a message, use the push() method on the channel object.

// Create a map to store the message payload
val payload = mapOf("hi, from nick" to content)
// Send the message payload to the channel
channel?.push("new_message", payload)
    // If the response is "ok", log a success message
    ?.receive("ok") { Log.d(TAG, "success $it") }
    // If the response is "error", log an error message
    ?.receive("error") { Log.d(TAG, "error $it") }
Payload of new_message containing message details, including:
  • content: The message content

  • type: The message content type (media, emoji, text)

Send Message Reply

// Create a payload map containing the message content and message ID
val payload = mapOf(
    "hi, from nick" to content,
    message_id to 1
    )
// Send the message payload to the channel
channel?.push("new_message_reply", payload)
    // If the response is "ok", log a success message
    ?.receive("ok") { Log.d(TAG, "success $it") }
    // If the response is "error", log an error message
    ?.receive("error") { Log.d(TAG, "error $it") }

Channel Poll Vote

// Define the poll question and poll options
val pollQuestion = "What is your favorite color?"
val pollOptions = listOf("Red", "Blue", "Green")

// Build the payload with the poll question, content type, and options
val payload = mapOf(
    "content" to pollQuestion,
    "content_type" to "poll",
    "options" to pollOptions
)

// Send the new_vote event with the payload to the channel
channel?.push("new_vote", payload)
    ?.receive("ok") { response ->
        // Log a message indicating that the poll vote was sent successfully
        Log.d(TAG, "Poll Vote Sent: $response")
    }
    ?.receive("error") { response ->
        // Log an error message with the response details
        Log.d(TAG, "Error: $response")
    }
Payload of vote containing vote details, including:
  • content: The vote question

  • content_type: The vote content type (media, emoji, text)

  • options: An array of vote options

For other polls APIs, please refer to Swagger documentation

Attach Media Files to Chat Messages

Attaching media files to chat messages involves obtaining a presigned URL, uploading the file to the presigned URL, and then sending the file information to the chat server.

Obtain a Presigned URL

To obtain a presigned URL, make a POST request to the API endpoint.

val client = OkHttpClient();
    val formBody = FormBody.Builder().add("apiKey", "{your-api-key}").build()
            // Create a POST request to the API endpoint with the form body and a header specifying JSON response acceptance.
            val request = Request.Builder()
                .url("https://api.sariska.io/api/v1/misc/get-presigned").post(formBody)
                .addHeader("Accept", "application/json")
                .build()
            // Execute the request
            client.newCall(request).execute().use { response ->
                if (!response.isSuccessful) throw IOException("Unexpected code ${response.code()}")
            }

Upload the File

After obtaining the presigned URL, the file can be uploaded to the URL using the PUT method.

// Extract the first file from the target files list
val file = event.target.files[0];
            // Retrieve the pre-signed URL from the response
            val signedURL = response.get("presignedURL");
                // Build a PUT request to upload the file to the pre-signed URL
                Request signedRequest = Request.Builder()
                    .url(signedURL)
                    .PUT(file)
                    .addHeader("ACL", "public-read")
                    .addHeader("Content-Disposition","attachment")
                    .build();
                // Execute the request
                client.newCall(signedRequest).execute();

Use presignedUrl to upload your files

Chat History

Retrieve the chat history using two methods:

  1. By Subscribing to Events

  • Subscribe to the archived_message event to receive the last 1000 messages in reverse chronological order.

channel.on("archived_message") { payload ->
    // Check if the payload contains a map of messages
    if (payload["messages"] is Map<*, *>) {
        // Cast the "messages" property to a map
        val messages = payload["messages"] as Map<*, *>
        // Print the received messages
        println("Received archived messages: $messages")
    }
}

  • Subscribe to the archived_message_count event to get the total number of messages in the chat history.

channel.on("archived_message_count") { payload ->
    // Check if the payload contains a "page" object as a map
    if (payload["page"] is Map<*, *>) {
        // Cast the "page" to a map
        val page = payload["page"] as Map<*, *>
        // Check if the "page" map contains a "count" key with an integer value
        if (page["count"] is Int) {
            val count = page["count"] as Int
            // Print the received message count
            println("Total message count: $count")
        }
    }
}

  • To retrieve a list of messages from the chat history, trigger the archived_message event to obtain the messages. Specify the size parameter to determine the number of messages you wish to fetch, and set the offset parameter as the starting index of the messages.

channel.push("archived_message", mapOf("page" to mapOf("offset" to 20, "size" to 20)))

  • To receive the total count of messages at any given time, initiate the archived_message_count trigger and subscribe to the corresponding event by listening for archived_message_count

channel.push("archived_message_count")

  1. By Using the Messages API

Make a GET request to the API endpoint to fetch the chat history for a specific room.

// Import libraries for URL processing, data reading, and stream manipulation
import java.net.URL
import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.stream.Collectors
import java.net.HttpURLConnection

// Fetch chat history from Sariska.io API endpoint
fun fetchChatHistory(completion: (Result<Map<String, Any>, Exception>) -> Unit) {
    // Define the API endpoint URL with room name placeholder
    val url = URL("https://api.sariska.io/api/v1/messaging/rooms/{room_name}/messages")
    // Open an HTTP connection to the API endpoint
    val connection = url.openConnection() as HttpURLConnection
    // Specify GET request method
    connection.requestMethod = "GET"
    // Set authorization header with your Sariska.io API token
    connection.setRequestProperty("Authorization", "Bearer your-token")

    // Send the request and handle the response
    try {
        // Get the server response code
        val responseCode = connection.responseCode
        // Check for successful response
        if (responseCode == HttpURLConnection.HTTP_OK) {
            // Create a reader for the response body
            val reader = BufferedReader(InputStreamReader(connection.inputStream))
            // Collect all lines of response data into a single string
            val responseData = reader.lines().collect(Collectors.joining())
            // Close the reader to free up resources
            reader.close()

            // Parse the response data as JSON, ignoring empty responses
            val json = responseData.takeIf { it.isNotBlank() }?.let {
                try {
                    it
                } catch (e: Exception) {
                    throw e
                }
            }
            
            // Pass successful result with parsed chat history data (or empty map for empty responses)
            completion(Result.success(json?.toMap() ?: emptyMap()))
        } else {
            // Handle errors with specific status code information
            val errorMessage = "Failed with status code $responseCode"
            val exception = Exception(errorMessage)
            completion(Result.failure(exception))
        }
    // Catch any unexpected exceptions during processing
    } catch (e: Exception) {
        completion(Result.failure(e))
    } finally {
        // Close the connection even after success or failure
        connection.disconnect()
    }
}

Example usage demonstration
fetchChatHistory { result ->
    when (result) {
        is Result.Success -> {
            // Access the chat history data
            val chatHistory = result.value
            // Print the retrieved chat history
            println("Chat history: $chatHistory")
        }
        is Result.Failure -> {
            // Get the exception associated with failure
            val error = result.exception
            // Print the error message
            println("Error fetching chat history: $error")
        }
    }
}

Fetch Specific Message

Retrieve any specific message from a room. It takes the room ID and message ID as parameters and sends a GET request to the Sariska API to fetch the specified message.

// Import libraries for URL processing, data reading, and stream manipulation
import java.net.URL
import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.stream.Collectors
import java.net.HttpURLConnection

// Fetch chat history from Sariska.io API endpoint
fun fetchSpecificMessage(messageID: String, completion: (Result<Map<String, Any>, Exception>) -> Unit) {
    // Build the request URL with the message ID
    val urlString = "https://api.sariska.io/api/v1/messaging/rooms/{room_name}/messages/$messageID"
    try {
        val url = URL(urlString)

        // Open an HTTP connection to the API endpoint
        val connection = url.openConnection() as HttpURLConnection
        // Specify GET request method
        connection.requestMethod = "GET"
        // Set authorization header with your Sariska.io API token
        connection.setRequestProperty("Authorization", "Bearer your-token")

        // Send the request and handle the response
        val responseCode = connection.responseCode
        // Check for successful response
        if (responseCode == HttpURLConnection.HTTP_OK) {
            // Create a reader for the response body
            val reader = BufferedReader(InputStreamReader(connection.inputStream))
            // Collect all lines of response data into a single string
            val responseData = reader.lines().collect(Collectors.joining())
            // Close the reader to free up resources
            reader.close()

            // Parse the response data as JSON, ignoring empty responses
            val json = responseData.takeIf { it.isNotBlank() }?.let {
                try {
                    it
                } catch (e: Exception) {
                    throw e
                }
            }
            // Pass successful result with parsed chat history data (or empty map for empty responses)
            completion(Result.success(json?.toMap() ?: emptyMap()))
        } else {
            // Handle errors with specific status code information
            val errorMessage = "Failed with status code $responseCode"
            val exception = Exception(errorMessage)
            completion(Result.failure(exception))
        }
    // Catch any unexpected exceptions during processing
    } catch (e: Exception) {
        completion(Result.failure(e))
    } finally {
        // Close the connection even after success or failure
        connection.disconnect()
    }
}

Example usage demonstration
val messageId = "your-message-id"
fetchSpecificMessage(messageId) { result ->
    when (result) {
        is Result.Success -> {
            // Access the specific message
            val specificMessage = result.value
            // Print the retrieved specific message
            println("Specific message: $specificMessage")
        }
        is Result.Failure -> {
            // Get the exception associated with failure
            val error = result.exception
            // Print the error message
            println("Error fetching specific message: $error")
        }
    }
}

Delete Chat History

Delete chat history for a specific room.

Delete Single or Multiple Messages

import java.net.URL
import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.nio.charset.StandardCharsets

fun emptyChat(messageIDs: List<String>, roomName: String, completion: (Result<Map<String, Any>, Exception>) -> Unit) {
    // Construct the request URL
    val urlString = "https://api.sariska.io/api/v1/messaging/rooms/$roomName/messages"
    try {
        val url = URL(urlString)

        // Construct the request payload
        val payload = mapOf(
            "method" to "DELETE",
            "headers" to mapOf("Authorization" to "Bearer your-token"),
            "body" to mapOf(
                "message_ids" to messageIDs,
                "is_empty" to false
            )
        )

        // Convert payload to JSON
        val jsonData = payload.toString().toByteArray(StandardCharsets.UTF_8)

        // Construct the request
        val connection = url.openConnection() as HttpURLConnection
        connection.requestMethod = "DELETE"
        connection.setRequestProperty("Authorization", "Bearer your-token")
        connection.doOutput = true

        // Send the DELETE request to the API endpoint
        val outputStream = OutputStreamWriter(connection.outputStream)
        outputStream.write(String(jsonData))
        outputStream.flush()

        // Check for successful response
        val responseCode = connection.responseCode
        if (responseCode == HttpURLConnection.HTTP_OK) {
            val reader = BufferedReader(InputStreamReader(connection.inputStream))
            val responseData = reader.lines().collect(Collectors.joining())
            reader.close()

            // Parse the response data
            val json = responseData.takeIf { it.isNotBlank() }?.let {
                try {
                    it
                } catch (e: Exception) {
                    throw e
                }
            }

            completion(Result.success(json?.toMap() ?: emptyMap()))
        } else {
            val errorMessage = "Failed with status code $responseCode"
            val exception = Exception(errorMessage)
            completion(Result.failure(exception))
        }
    } catch (e: Exception) {
        completion(Result.failure(e))
    } finally {
        // Close the connection even after success or failure
        connection.disconnect()
    }
}

Example usage demonstration
// Define a list of message IDs to be deleted
val messageIDs = listOf("message_id_1", "message_id_2")
// Specify the target chat room
val roomName = "your-room-name"
// Empty the chat
emptyChat(messageIDs, roomName) { result ->
    when (result) {
        is Result.Success -> {
            // If successful, extract the response
            val response = result.value
            // Print the response
            println("Empty chat response: $response")
        }
        is Result.Failure -> {
            // If an error occurred, extract the exception
            val error = result.exception
            // Print the error
            println("Error emptying chat: $error")
        }
    }
}

Delete All Chats

import java.net.URL
import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.nio.charset.StandardCharsets

fun emptyChat(roomName: String, completion: (Result<Map<String, Any>, Exception>) -> Unit) {
    // Construct the request URL
    val urlString = "https://api.sariska.io/api/v1/messaging/rooms/$roomName/messages"
    try {
        val url = URL(urlString)

        // Construct the request payload
        val payload = mapOf(
            "method" to "DELETE",
            "headers" to mapOf("Authorization" to "Bearer your-token"),
            "body" to mapOf(
                "message_ids" to emptyList<String>(),
                "is_empty" to true
            )
        )

        // Convert payload to JSON
        val jsonData = payload.toString().toByteArray(StandardCharsets.UTF_8)

        // Construct the request
        val connection = url.openConnection() as HttpURLConnection
        connection.requestMethod = "DELETE"
        connection.setRequestProperty("Authorization", "Bearer your-token")
        connection.doOutput = true

        // Send the DELETE request to the API endpoint
        val outputStream = OutputStreamWriter(connection.outputStream)
        outputStream.write(String(jsonData))
        outputStream.flush()

        // Check for successful response
        val responseCode = connection.responseCode
        if (responseCode == HttpURLConnection.HTTP_OK) {
            val reader = connection.inputStream.reader()
            val responseData = reader.readText()

            // Parse the response data
            val json = responseData.takeIf { it.isNotBlank() }?.let {
                try {
                    it
                } catch (e: Exception) {
                    throw e
                }
            }

            completion(Result.success(json?.toMap() ?: emptyMap()))
        } else {
            val errorMessage = "Failed with status code $responseCode"
            val exception = Exception(errorMessage)
            completion(Result.failure(exception))
        }
    } catch (e: Exception) {
        completion(Result.failure(e))
    } finally {
        // Close the connection even after success or failure
        connection.disconnect()
    }
}

Example usage demonstration
// Define the room name to empty the chat of
val roomName = "your-room-name"
// Empty the chat
emptyChat(roomName) { result ->
    when (result) {
        is Result.Success -> {
            // If successful, extract the response
            val response = result.value
            // Print the response
            println("Empty chat response: $response")
        }
        is Result.Failure -> {
            // If an error occurred, extract exception details
            val error = result.exception
            // Print the error
            println("Error emptying chat: $error")
        }
    }
}

Presence

The Presence object facilitates real-time synchronization of presence information between the server and the client, enabling the detection of user join and leave events.

Create a Presence Instance

To establish presence synchronization, instantiate a Presence object and provide the channel to track presence lifecycle events:

val channel = socket.channel("chat:room123")
val presence = Presence(channel)

State Synchronization

Utilize the presence.onSync callback to respond to state changes initiated by the server. For instance, to dynamically render the user list upon every list modification, implement the following:

presence.onSync {
    println(presence.list());
})

Handle Individual Join and Leave Events

The presence.onJoin and presence.onLeave callbacks allow for handling individual user join and leave events. Here is an instance:

val presence = Presence(channel = lobbyChannel);

// Detect if the user has joined for the first time or from another tab/device
presence.onJoin {id, current, leftPres ->
      if (current != null) {
        if(current.isEmpty()){
          println("user has entered for the first time")
        } else {
          println("user additional presence")
        }
      }
    }

// Detect if the user has left all tabs/devices, or is still present
presence.onLeave {id, current, leftPres -> 
  if(current.length == 0){
    println("user has left from all devices")
  } else {
    println("user left from a device")
  }
}

// Receive presence data from server
presence.onSync {
    // List of users
    presence.list()
}

Retrieve Presence Information

The presence.list(by:) method retrieves a list of presence information based on the local metadata state. By default, it returns the entire presence metadata. Alternatively, a listBy function can be provided to filter the metadata for each presence.

For instance, if a user is online from multiple devices, each with a metadata status of "online," but their status is set to "away" on another device, the application might prefer to display the "away" status in the UI.

The following example defines a listBy function that prioritizes the first metadata registered for each user, representing the first tab or device they used to connect:

val onlineUsers = presence.list(listBy)

User Typing

Send information about a user who is typing. This information can be used to update the chat interface, such as displaying a "user is typing" indicator next to the user's name.

Send User Typing Event

When a user starts typing, the following code sends a typing event to other peers.

val payload = mapOf(true to typing)
channel?.push("user_typing", payload)
    // Log the success message
    ?.receive("ok") { Log.d(TAG, "success $it") }
    // Log the error message
    ?.receive("error") { Log.d(TAG, "error $it") }

Receive User Typing Event

Other peers in the chat can listen for the typing event on the same channel.

channel.on("user_typing") { typing_response: TypingMessage ->
    println(typing_response)
}

For more detailed information on the APIs, check out the Github repository.

For detailed real-time messaging API's, refer to Phoenix documentation.

For detailed management of chat and room APIs, refer to Sariska Swagger documentation

Last updated