Skip to content

Commit

Permalink
Merge pull request #86 from MobileNativeFoundation/chat/websockets
Browse files Browse the repository at this point in the history
Set Up Backend For Chat
  • Loading branch information
matt-ramotar authored Nov 10, 2024
2 parents 5255c29 + 4f361a0 commit 2f96f12
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 0 deletions.
1 change: 1 addition & 0 deletions backend/server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ dependencies {
implementation(libs.jdbc.driver)
implementation(libs.postgresql)
implementation(libs.hikaricp)
implementation(libs.ktor.server.websockets)
}

sqldelight {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.ktor.server.application.*
import io.ktor.server.netty.*
import org.mobilenativefoundation.trails.backend.server.plugins.configureRouting
import org.mobilenativefoundation.trails.backend.server.plugins.configureSerialization
import org.mobilenativefoundation.trails.backend.server.plugins.configureWebSockets


fun main(args: Array<String>): Unit =
Expand All @@ -13,6 +14,7 @@ fun Application.module() {
val database = createTrailsDatabase()

configureSerialization()
configureWebSockets()
configureRouting(database)
}

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.mobilenativefoundation.trails.backend.server.plugins
import io.ktor.server.application.*
import io.ktor.server.routing.*
import org.mobilenativefoundation.trails.backend.server.TrailsDatabase
import org.mobilenativefoundation.trails.backend.server.routes.ChatRoutes
import org.mobilenativefoundation.trails.backend.server.routes.CreatorRoutes
import org.mobilenativefoundation.trails.backend.server.routes.PostRoutes

Expand All @@ -12,6 +13,7 @@ fun Application.configureRouting(

val postRoutes = PostRoutes(database)
val creatorRoutes = CreatorRoutes(database)
val chatRoutes = ChatRoutes(database)

routing {
with(postRoutes) {
Expand All @@ -23,5 +25,9 @@ fun Application.configureRouting(
with(creatorRoutes) {
getCreatorById()
}

with(chatRoutes) {
chat()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.mobilenativefoundation.trails.backend.server.plugins

import io.ktor.server.application.*
import io.ktor.server.websocket.*

fun Application.configureWebSockets() {
install(WebSockets)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package org.mobilenativefoundation.trails.backend.server.routes

import io.ktor.http.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.websocket.*
import io.ktor.websocket.*
import kotlinx.coroutines.channels.consumeEach
import kotlinx.datetime.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.mobilenativefoundation.trails.backend.server.TrailsDatabase
import java.util.concurrent.ConcurrentHashMap

class ChatRoutes(private val trailsDatabase: TrailsDatabase) {

private val chatSessionConnections = ConcurrentHashMap<Int, ConcurrentHashMap<Int, DefaultWebSocketServerSession>>()

fun Route.chat() {
webSocket("/chat/{chatSessionId}/{userId}") {
val chatSessionId = call.parameters["chatSessionId"]?.toIntOrNull()
val userId = call.parameters["userId"]?.toIntOrNull()

if (userId == null) {
call.respond(HttpStatusCode.BadRequest, "Invalid or missing user ID.")
return@webSocket
}

if (chatSessionId == null) {
call.respond(HttpStatusCode.BadRequest, "Invalid or missing chat session ID.")
return@webSocket
}

val chatSessionParticipantIds =
trailsDatabase.chatQueries.getParticipantsByChatSessionId(chatSessionId).executeAsList()
val isParticipant = userId in chatSessionParticipantIds

if (!isParticipant) {
close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, "Unauthorized"))
return@webSocket
}

val connections = chatSessionConnections.getOrPut(chatSessionId) {
ConcurrentHashMap()
}

connections[userId] = this

try {
incoming.consumeEach { frame ->
if (frame is Frame.Text) {
val receivedText = frame.readText()
val chatMessage = Json.decodeFromString<ChatMessage>(receivedText)

// Store the message in the database
trailsDatabase.chatQueries.insertChatMessage(
chat_session_id = chatSessionId,
from_user_id = userId,
content = chatMessage.content,
timestamp = chatMessage.timestamp.toJavaLocalDateTime()
)

// Send the message to all active participants in the chat session
connections.forEach { (participantId, session) ->
if (participantId != userId) {
session.send(
Frame.Text(
Json.encodeToString(chatMessage)
)
)
}
}
}
}
} catch (e: Exception) {
println("Error for user $userId: ${e.localizedMessage}.")
} finally {
print("User disconnected: $userId.")
connections.remove(userId)
if (connections.isEmpty()) {
chatSessionConnections.remove(chatSessionId)
}
}

}
}
}

@Serializable
data class ChatMessage(
val chatSessionId: Int,
val fromUserId: Int,
val content: String,
val timestamp: LocalDateTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
CREATE TABLE chatSession(
id SERIAL PRIMARY KEY
);

CREATE TABLE chatSessionParticipant(
chat_session_id INT NOT NULL REFERENCES chatSession(id) ON DELETE CASCADE,
user_id INT NOT NULL REFERENCES user(id),
PRIMARY KEY (chat_session_id, user_id)
);

CREATE TABLE chatMessage(
id SERIAL PRIMARY KEY,
chat_session_id INT NOT NULL REFERENCES chatSession(id) ON DELETE CASCADE,
from_user_id INT NOT NULL REFERENCES user(id),
content TEXT NOT NULL,
timestamp TIMESTAMP NOT NULL
);

-- Queries

getMessagesByChatSessionId:
SELECT * FROM chatMessage
WHERE chat_session_id = ?
ORDER BY timestamp ASC;

insertChatMessage:
INSERT INTO chatMessage (chat_session_id, from_user_id, content, timestamp) VALUES (?, ?, ?, ?);

insertChatSessionParticipant:
INSERT INTO chatSessionParticipant (chat_session_id, user_id) VALUES (?, ?);

getParticipantsByChatSessionId:
SELECT user_id FROM chatSessionParticipant
WHERE chat_session_id = ?;
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ ktor-client-js = {module = "io.ktor:ktor-client-js", version.ref = "ktor"}
ktor-client-apache5 = { module = "io.ktor:ktor-client-apache5", version.ref = "ktor" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
ktor-client-okhttp-jvm = { module = "io.ktor:ktor-client-okhttp-jvm", version.ref = "ktor" }
ktor-server-websockets = { module = "io.ktor:ktor-server-websockets", version.ref = "ktor" }
native-driver = { module = "app.cash.sqldelight:native-driver", version.ref = "sqldelight" }
sqlite-driver = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqldelight" }
sqljs-driver = { module = "app.cash.sqldelight:sqljs-driver", version.ref = "sqldelight" }
Expand Down

0 comments on commit 2f96f12

Please sign in to comment.