C# (Unity Engine)


Sariska provides a C# API and easy-to-integrate SDKs for Unity and Blazor to add real-time features in your applications.

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 NuGetForUnity Package Manager

  • Download the package manager from GitHub

  • In your Unity project, navigate to Assets -> Import Package -> Custom Package

  • Select the downloaded "NugetForUnity.3.0.4.unitypackage" and click Import All

Step 2 : Install Phoenix Client Package

  • Access NuGet Package Manager: Click NuGet (at the top of your Unity Editor) -> Manage NuGet Packages

  • Search for "io.sariska.sariskacsharp"

  • Click Install to add the Phoenix Client package to your project.

Step 3 : Import PhoenixSharp

using Phoenix;
using PhoenixTests.WebSocketImpl;

Step 4 : Create Socket

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

// Create a new WebSocket instance, specifying the server URL and authorization token
var socket = new Socket("wss://api.sariska.io/api/v1/messaging/websocket", {token: "your-token"});

// Track the number of open connections
var onOpenCount = 0;
var onOpenCount = 0;

// Handles socket opening event
void OnOpenCallback() => onOpenCount++;
socket.OnOpen += OnOpenCallback;

// Handles socket closing event
void OnClosedCallback() => onOpenCount--;
socket.OnClose += OnClosedCallback;

// Initiate the 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 5 : 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.

var 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.

var 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.

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

Handle Errors

When an error occurs on the channel, the ERROR callback is triggered. The callback receives the error information in payload, if available.

void OnErrorCallback(string message)
  {
    onErrorData.Add(message);
  }

socket.OnError += OnErrorCallback;

Close Channel

When the channel is closed, the CLOSE callback is invoked. This signifies that the communication on the channel has ended and no further data can be exchanged.

socket.OnClose += OnClosedCallback;

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
Reply? joinOkReply = null;
channel.join()
    .receive("ok", r => joinOkReply = r)
    .receive("error", r => joinErrorReply = r)
    .receive("timeout", () => Debug.log("Time out"))

// Leave the channel
channel.leave();

Channel User Joined

channel.on("user_joined", (payload)=>{ 
    var {user, room} = payload;
    });
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

Message? afterJoinMessage = null;
channel.on("new_message", (message)=> {
    afterJoinMessage = message;
});
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.

// Send the new_vote event with the payload
channel
    .Push("new_vote", payload)
    .Receive("ok", response =>
    {
        Console.WriteLine($"Poll Vote Sent: {response}");
    })
    .Receive("error", response =>
    {
        Console.WriteLine($"Error: {response}");
    });
    

Send Message Reply

var payload = new
{
    content = "hi",
    message_id = 1
};

channel
    .Push("new_message_reply", payload)
    .Receive("ok", response =>
    {
        Console.WriteLine($"Poll Vote Sent: {response}");
    })
    .Receive("error", response =>
    {
        Console.WriteLine($"Error: {response}");
    });

Channel Poll Vote

var pollQuestion = "What is your favorite color?";
var pollOptions = new List<string> { "Red", "Blue", "Green" };

// Build the payload with the poll question, content type, and options
var payload = new
{
    content = pollQuestion,
    content_type = "poll",
    options = pollOptions
};

// Send the new_vote event with the payload
channel
    .Push("new_vote", payload)
    .Receive("ok", response =>
    {
        Console.WriteLine($"Poll Vote Sent: {response}");
    })
    .Receive("error", response =>
    {
        Console.WriteLine($"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

Unity is a cross-platform game engine developed by Unity Technologies.

The (C#)Phoenix Channel client can be used to build real time messaging apps for:

  • Unity

  • Blazor

Installation

The first step is to install NuGetForUnity Package manager in your Unity project. The Package Manager is used to import Phoenix Client into your project.

  1. The package manager can be downloaded by clicking on the following link. https://github.com/GlitchEnzo/NuGetForUnity/releases/download/v3.0.4/NugetForUnity.3.0.4.unitypackage

  2. In your unity project, go to Assets -> Import-Package -> Custom Package

  3. Select "NugetForUnity.3.0.4.unitypackage" from your download folder and import all.

  4. Once imported, click on NuGet (top of your Unity Editor) -> Manage Nuget Packages.

  5. Search for "io.sariska.sariskacsharp" and install.

Socket

Importing PhoenixSharp into your script

using Phoenix;
using PhoenixTests.WebSocketImpl;

Creating a Socket

First, you need to create the socket that will join channels, receive events, and send messages. A single connection is established to the server and channels are multiplexed over the connection.

var socket = new Socket("wss://api.sariska.io/api/v1/messaging/websocket", {token: "your-token"});

// In the WebSocket connection, set params["disconnect_past_tabs"] to true if you want the user to disconnect from past WebSocket connections and tabs before establishing a new WebSocket connection.
var onOpenCount = 0;
var onOpenCount = 0;

void OnOpenCallback() => onOpenCount++;
socket.OnOpen += OnOpenCallback;

void OnClosedCallback() => onOpenCount--;
socket.OnClose += OnClosedCallback;

// Opens connection to the server

socket.connect()    

Disconnect Socket

// Closes connection to the server
socket.disconnect();

Channels

Creating a Channel

Once your socket is created, you can join channel topics that listen for specific events and allow for sending data to do a topic. Whenever sending data, you can use the .receive() hook to get the status of the outbound push message. Channels cannot be created directly but instead are created through the socket object by calling socket.channel(topic) with the topic of the channel.

// Use prefix chat in your channel name if you want to persist messages to the database 
// here schema is fixed, only fixed fields are allowed to send, use this for chatting applications

var channel = socket.Channel("tester:phoenix-sharp", _channelParams);




// Use prefix rtc in your channel name if you don't need any persistent 
// here you can any json object to other peers, this is useful for events in multiplayer games..etc 

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



// Use prefix sariska in your channel name if want to use flatbuffers as serializer and ou don't need any persistent
// here you are able to send any schema as per flatbuffers standard, use this for performance critical applications , gaming etc
var channel = socket.channel("sariska:room123");


/// Called when errors occur on the channel

void OnErrorCallback(string message)
  {
    onErrorData.Add(message);
  }

socket.OnError += OnErrorCallback;

/// Called when the channel is closed
socket.OnClose += OnClosedCallback;

// channel user joined the event

channel.on("user_joined", (payload)=>{ 
    var {user, room} = payload;
    //  { //user details
    //    id: "1",  
    //    name: "nick",
    //    avatar: "https://test.sariska.io/nick/profile.png",
    //    email:  "nick@gmail.com ",
    // }
    // { //room details
    //    id: "1",
    //    session_id: "room123",
    //    created_by: "123456",
    //    status: "active"  
    // } 
    //
});

// channel message received event
Message? afterJoinMessage = null;
channel.on("new_message", (message)=> {
    afterJoinMessage = message;
    // {  //message details
    //    content: "hello from rick",
    //    id: '2', // sender user id
    //    name: 'rick' // sender name
    //    content_type: "media", // media, emoji, text etc
    //    is_delivered: true,
    //    timestamp:   
    // } 
    //
});

// Join the Channel
Reply? joinOkReply = null;
channel.join()
    .receive("ok", r => joinOkReply = r)
    .receive("error", r => joinErrorReply = r)
    .receive("timeout", () => Debug.log("Time out"))

// Leave the channel

channel.leave();

Sending Messages

// Send the new_vote event with the payload
channel
    .Push("new_vote", payload)
    .Receive("ok", response =>
    {
        Console.WriteLine($"Poll Vote Sent: {response}");
    })
    .Receive("error", response =>
    {
        Console.WriteLine($"Error: {response}");
    });
    

Sending Poll Votes

// Assuming you have a channel instance
// Assuming you have a poll question and options
var pollQuestion = "What is your favorite color?";
var pollOptions = new List<string> { "Red", "Blue", "Green" };

// Build the payload with the poll question, content type, and options
var payload = new
{
    content = pollQuestion,
    content_type = "poll",
    options = pollOptions
};

// Send the new_vote event with the payload
channel
    .Push("new_vote", payload)
    .Receive("ok", response =>
    {
        Console.WriteLine($"Poll Vote Sent: {response}");
    })
    .Receive("error", response =>
    {
        Console.WriteLine($"Error: {response}");
    });

For other polls apis checkout swagger

Sending Message Reply

// Assuming you have a channel instance
// Send the new_vote event with the payload
var payload = new
{
    content = "hi",
    message_id = 1
};

channel
    .Push("new_message_reply", payload)
    .Receive("ok", response =>
    {
        Console.WriteLine($"Poll Vote Sent: {response}");
    })
    .Receive("error", response =>
    {
        Console.WriteLine($"Error: {response}");
    });

User Typing

Sending user typing event to other peers

// Send the new_vote event with the payload
var payload = new
{
    user_typing = true,
};

channel
    .Push("user_typing", payload)
    .Receive("ok", response =>
    {
        Console.WriteLine($" Sent: {response}");
    })
    .Receive("error", response =>
    {
        Console.WriteLine($"Error: {response}");
    });

other peers now can listen for message typing event

channel.on("user_typing", (payload)=>{ 
    //  { //payload
});

Presence

The Presence object provides features for syncing presence information from the server with the client and handling presences of joining and leaving.

Creating Presence

To sync presence state from the server, first instantiate an object and pass your channel in to track lifecycle events:

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

Syncing State

Next, use the presence.onSync callback to react to state changes from the server. For example, to render the list of users every time the list changes, you could write:

presense.onSync(() => {
    myRenderUsersFunction(presence.list());
});

Listing Presences

presence.list(by:) is used to return a list of presence information based on the local state of metadata. By default, all presence metadata is returned, but a listBy function can be supplied to allow the client to select which metadata to use for a given presence. For example, you may have a user online from different devices with a metadata status of "online", but they have set themselves to "away" on another device. In this case, the app may choose to use the "away" status for what appears on the UI. The example below defines a listBy function that prioritizes the first metadata which was registered for each user. This could be the first tab they opened or the first device they came online from:

private static object ListByFirst(
            KeyValuePair<string, Presence.MetadataContainer> container
        )
        {
            return container.Value.Metas.First();
        }

var presenceList = presence.List(ListByFirst)
                .Select(o => o as Dictionary<string, object>)
                .ToList();

Handling individual join and leave events

The presence.onJoin and presence.onLeave callbacks can be used to react to individual presences joining and leaving the app.

var presence = new Presence(channel);

var usersJoined = new List<string>();

var usersLeft = new List<string>();

presence.OnJoin += (userId, _, _) => usersJoined.Add(userId);

presence.OnLeave += (userId, _, _) => usersLeft.Add(userId);

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 =>
{
    if (payload.TryGetValue("messages", out var messages) && messages is Dictionary<string, object>)
    {
        // Process the received messages
        Console.WriteLine($"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 =>
{
    if (payload.TryGetValue("page", out var page) && page is Dictionary<string, object> &&
        ((Dictionary<string, object>)page).TryGetValue("count", out var count) && count is int)
    {
        // Process the received message count
        Console.WriteLine($"Total message count: {count}");
    }
});
  • To retrieve a list of messages in 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 and group_by_day to group messages by date.

var payload = new Dictionary<string, object>
{
    ["page"] = new Dictionary<string, object>
    {
        ["offset"] = 20,
        ["size"] = 20
        ["group_by_day"] = false
    }
};

channel.Push("archived_message", payload);
  • If you desire 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."

void OnArchivedMessageCount(dynamic payload)
{
    if (payload["page"] is Dictionary<string, dynamic> page)
    {
        if (page["count"] is int count)
        {
            // Process the received message count
            Console.WriteLine($"Total message count: {count}");
        }
    }
}

// Example usage:
channel.On("archived_message_count", OnArchivedMessageCount);
  1. Using the Messages API

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

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        await FetchChatHistoryAsync();
    }

    static async Task FetchChatHistoryAsync()
    {
        string url = "https://api.sariska.io/api/v1/messaging/rooms/{room_name}/messages";

        try
        {
            using (var httpClient = new HttpClient())
            using (var request = new HttpRequestMessage(HttpMethod.Get, url))
            {
                request.Headers.Add("Authorization", "Bearer your-token");

                using (var response = await httpClient.SendAsync(request))
                {
                    // Check for successful response
                    if (response.IsSuccessStatusCode)
                    {
                        var responseBody = await response.Content.ReadAsStringAsync();
                        Console.WriteLine($"Chat history: {responseBody}");
                    }
                    else
                    {
                        var statusCode = (int)response.StatusCode;
                        var errorMessage = $"Failed with status code {statusCode}";
                        throw new Exception(errorMessage);
                    }
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error fetching chat history: {ex.Message}");
        }
    }
}

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.

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        string messageId = "your-message-id";
        await FetchSpecificMessageAsync(messageId);
    }

    static async Task FetchSpecificMessageAsync(string messageId)
    {
        string url = $"https://api.sariska.io/api/v1/messaging/rooms/{{room_name}}/messages/{messageId}";

        try
        {
            using (var httpClient = new HttpClient())
            using (var response = await httpClient.GetAsync(url))
            {
                // Check for successful response
                if (response.IsSuccessStatusCode)
                {
                    var responseBody = await response.Content.ReadAsStringAsync();
                    Console.WriteLine($"Specific message: {responseBody}");
                }
                else
                {
                    var statusCode = (int)response.StatusCode;
                    var errorMessage = $"Failed with status code {statusCode}";
                    throw new Exception(errorMessage);
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error fetching specific message: {ex.Message}");
        }
    }
}

Delete Chat History

Delete chat history for a specific room.

Delete Single or Multiple Messages

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        string[] messageIDs = { "message_id_1", "message_id_2" };
        string roomName = "your-room-name";
        await EmptyChatAsync(messageIDs, roomName);
    }

    static async Task EmptyChatAsync(string[] messageIDs, string roomName)
    {
        string url = $"https://api.sariska.io/api/v1/messaging/rooms/{roomName}/messages";

        try
        {
            // Construct the request payload
            var payload = new Dictionary<string, object>
            {
                { "method", "DELETE" },
                { "headers", new Dictionary<string, string> { { "Authorization", "Bearer your-token" } } },
                { "body", new Dictionary<string, object> { { "message_ids", messageIDs }, { "is_empty", false } } }
            };

            // Convert payload to JSON
            string jsonPayload = Newtonsoft.Json.JsonConvert.SerializeObject(payload);

            // Construct the HTTP request
            using (var httpClient = new HttpClient())
            using (var request = new HttpRequestMessage(HttpMethod.Delete, url))
            {
                request.Headers.Add("Authorization", "Bearer your-token");
                request.Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");

                // Send the DELETE request to the API endpoint
                var response = await httpClient.SendAsync(request);

                // Check for successful response
                if (response.IsSuccessStatusCode)
                {
                    var responseBody = await response.Content.ReadAsStringAsync();
                    Console.WriteLine($"Empty chat response: {responseBody}");
                }
                else
                {
                    var statusCode = (int)response.StatusCode;
                    var errorMessage = $"Failed with status code {statusCode}";
                    throw new Exception(errorMessage);
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error emptying chat: {ex.Message}");
        }
    }
}

Delete All Chats

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        string roomName = "your-room-name";
        await EmptyChatAsync(roomName);
    }

    static async Task EmptyChatAsync(string roomName)
    {
        string url = $"https://api.sariska.io/api/v1/messaging/rooms/{roomName}/messages";

        try
        {
            // Construct the request payload
            var payload = new Dictionary<string, object>
            {
                { "method", "DELETE" },
                { "headers", new Dictionary<string, string> { { "Authorization", "Bearer your-token" } } },
                { "body", new Dictionary<string, object> { { "message_ids", new string[] { } }, { "is_empty", true } } }
            };

            // Convert payload to JSON
            string jsonPayload = Newtonsoft.Json.JsonConvert.SerializeObject(payload);

            // Construct the HTTP request
            using (var httpClient = new HttpClient())
            using (var request = new HttpRequestMessage(HttpMethod.Delete, url))
            {
                request.Headers.Add("Authorization", "Bearer your-token");
                request.Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");

                // Send the DELETE request to the API endpoint
                var response = await httpClient.SendAsync(request);

                // Check for successful response
                if (response.IsSuccessStatusCode)
                {
                    var responseBody = await response.Content.ReadAsStringAsync();
                    Console.WriteLine($"Empty chat response: {responseBody}");
                }
                else
                {
                    var statusCode = (int)response.StatusCode;
                    var errorMessage = $"Failed with status code {statusCode}";
                    throw new Exception(errorMessage);
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error emptying chat: {ex.Message}");
        }
    }
}

Attachments

You can attach media files to chat as follows:

private async Task<string> FetchPresignedURL(string fileName, string fileType)
{
    using (var client = new HttpClient())
    {
        client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", AccessToken);

        var requestBody = new
        {
            fileName,
            fileType
        };

        var content = new StringContent(JsonConvert.SerializeObject(requestBody));
        content.Headers.Clear();
        content.Headers.Add("Content-Type", "application/json");

        var response = await client.PostAsync(ApiBaseUrl, content);

        if (response.IsSuccessStatusCode)
        {
            var responseBody = await response.Content.ReadAsStringAsync();
            dynamic result = JsonConvert.DeserializeObject(responseBody);
            return result.presignedUrl;
        }
        else
        {
            throw new Exception($"Failed to fetch presigned URL. Status Code: {response.StatusCode}");
        }
    }
}

private async Task<string> UploadFile(string filePath, string presignedUrl)
{
    using (var client = new HttpClient())
    using (var fileStream = File.OpenRead(filePath))
    {
        var content = new StreamContent(fileStream);
        content.Headers.Clear();
        content.Headers.Add("ACL", "public-read");
        content.Headers.Add("Content-Disposition", "attachment");

        var response = await client.PutAsync(presignedUrl, content);

        if (response.IsSuccessStatusCode)
        {
            var responseBody = await response.Content.ReadAsStringAsync();
            dynamic result = JsonConvert.DeserializeObject(responseBody);
            return result.presignedUrl;
        }
        else
        {
            throw new Exception($"Failed to upload file. Status Code: {response.StatusCode}");
        }
    }
}

public async Task<string> UploadFile()
{
    var filePicker = new YourFilePicker(); // Implement your file picker logic
    var file = await filePicker.PickFile();

    var presignedUrl = await FetchPresignedURL(file.Name, file.Extension);
    var uploadedFileUrl = await UploadFile(file.FilePath, presignedUrl);

    return uploadedFileUrl;
}

For more detailed information on the APIs, check out the Phoenix Demo for Unity.

For detailed real-time messaging API's , follow Phoenix documentation. For detailed management of chat and room APIs, refer to Sariska Swagger Documentation

Last updated