Skip to content

Commit

Permalink
Implement mouse cursor sharing in broadcasts
Browse files Browse the repository at this point in the history
  • Loading branch information
slak44 committed Mar 4, 2024
1 parent 10c9740 commit c65cd5e
Show file tree
Hide file tree
Showing 43 changed files with 657 additions and 102 deletions.
3 changes: 2 additions & 1 deletion .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions .idea/modules/ckompiler.internals-explorer-backend.main.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions internals-explorer-backend/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ dependencies {
implementation("org.springframework.security:spring-security-messaging")
implementation("org.springframework.boot:spring-boot-starter-websocket")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
developmentOnly("org.springframework.boot:spring-boot-devtools")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,27 @@ package slak.ckompiler.backend.dto

enum class BroadcastMessageType {
VIEW_STATE,
BROADCAST_METADATA,
SUBSCRIBER_CHANGE,
BROADCAST_CLOSE,
}

/**
* The values are normalized coordinates, not pixels.
*/
data class MousePosition(val x: Double, val y: Double)

sealed class BroadcastMessage(val type: BroadcastMessageType)

data class ViewStateMessage(val viewState: ViewStateNonMetadataDeltaDto) : BroadcastMessage(BroadcastMessageType.VIEW_STATE)
data class ViewStateMessage(
val viewState: ViewStateNonMetadataDeltaDto?,
val pos: MousePosition,
) : BroadcastMessage(BroadcastMessageType.VIEW_STATE) {
constructor(dto: ViewStateMessageDto) : this(dto.state, dto.pos)
}

data class BroadcastMetadataMessage(val broadcasterName: String) : BroadcastMessage(BroadcastMessageType.BROADCAST_METADATA)

data class SubscriberChangeMessage(val subscribers: List<String>) : BroadcastMessage(BroadcastMessageType.SUBSCRIBER_CHANGE)

data object BroadcastCloseMessage : BroadcastMessage(BroadcastMessageType.BROADCAST_CLOSE)
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import slak.ckompiler.backend.entities.UserState
import slak.ckompiler.backend.entities.ViewState
import slak.ckompiler.backend.services.broadcast.ActiveBroadcast

data class UserStateDto(val id: String, val autosaveViewState: ViewState?, val activeBroadcast: ActiveBroadcast? = null) {
constructor(entity: UserState) : this(entity.id, entity.autosaveViewState)
data class UserStateDto(
val id: String,
val autosaveViewState: ViewState?,
val userName: String?,
val activeBroadcast: ActiveBroadcast? = null
) {
constructor(entity: UserState) : this(entity.id, entity.autosaveViewState, entity.userName)
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ data class ViewStateNonMetadataDeltaDto(
val variableRenameViewState: SteppableGraphViewStateDto,
)

data class ViewStateMessageDto(
val state: ViewStateNonMetadataDeltaDto?,
val pos: MousePosition
)

data class ViewStateDto(
val id: String?,
val createdAt: Instant?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import org.springframework.stereotype.Repository
@Entity
data class UserState(
@Id val id: String,
@OneToOne var autosaveViewState: ViewState,
@OneToOne(optional = true) var autosaveViewState: ViewState?,
@Column(columnDefinition = "text")
var userName: String? = null,
)

@Repository
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package slak.ckompiler.backend.presentation

import org.springframework.web.bind.annotation.*
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import slak.ckompiler.backend.dto.UserStateDto
import slak.ckompiler.backend.services.broadcast.BroadcastService
import slak.ckompiler.backend.services.UserStateService
import slak.ckompiler.backend.services.broadcast.BroadcastService
import java.security.Principal

@RestController
Expand All @@ -14,7 +17,7 @@ class UserStateController(
) {
@GetMapping
fun getUserState(principal: Principal): UserStateDto {
return userStateService.findById(principal.name).copy(
return userStateService.findById(principal.name, principal as JwtAuthenticationToken).copy(
activeBroadcast = broadcastService.getBroadcastByPresenterId(principal.name)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package slak.ckompiler.backend.presentation

import org.springframework.security.access.prepost.PostAuthorize
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
import org.springframework.web.bind.annotation.*
import slak.ckompiler.backend.dto.ViewStateDto
import slak.ckompiler.backend.dto.ViewStateListingDto
Expand All @@ -24,7 +25,7 @@ class ViewStateController(
@PreAuthorize("#viewState.owner == null")
fun saveAutosave(@RequestBody viewState: ViewStateDto, principal: Principal): ViewStateDto {
viewState.owner = principal.name
return ViewStateDto(viewStateService.saveAutosave(viewState))
return ViewStateDto(viewStateService.saveAutosave(viewState, principal as JwtAuthenticationToken))
}

@GetMapping("/list")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import org.springframework.web.socket.CloseStatus
import org.springframework.web.socket.messaging.SessionDisconnectEvent
import slak.ckompiler.backend.dto.BroadcastCloseMessage
import slak.ckompiler.backend.dto.ViewStateMessage
import slak.ckompiler.backend.dto.ViewStateNonMetadataDeltaDto
import slak.ckompiler.backend.dto.ViewStateMessageDto
import slak.ckompiler.backend.services.broadcast.BroadcastId
import slak.ckompiler.backend.services.broadcast.BroadcastService
import java.security.Principal
Expand Down Expand Up @@ -51,12 +51,12 @@ class BroadcastController(
@PreAuthorize("@broadcastService.isPresenter(#broadcastId, #principal.name)")
fun broadcastState(
@DestinationVariable broadcastId: String,
@Payload viewState: ViewStateNonMetadataDeltaDto,
@Payload viewStateMessage: ViewStateMessageDto,
principal: Principal,
): ViewStateMessage {
broadcastService.updateCurrentState(broadcastId, viewState)
broadcastService.updateCurrentState(broadcastId, viewStateMessage)

return ViewStateMessage(viewState)
return ViewStateMessage(viewStateMessage)
}

@EventListener
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,87 @@
package slak.ckompiler.backend.services

import com.fasterxml.jackson.databind.JsonNode
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.client.ExchangeFilterFunction
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.bodyToMono
import reactor.core.publisher.Mono
import slak.ckompiler.backend.dto.UserStateDto
import slak.ckompiler.backend.entities.UserState
import slak.ckompiler.backend.entities.UserStateRepository
import slak.ckompiler.backend.entities.ViewState
import slak.ckompiler.backend.entities.ViewStateRepository
import kotlin.jvm.optionals.getOrNull

@Service
class UserStateService(val userStateRepository: UserStateRepository, val viewStateRepository: ViewStateRepository) {
fun findById(id: String): UserStateDto {
return userStateRepository.findById(id).map(::UserStateDto).orElseGet { UserStateDto(id, null) }
class UserStateService(
val userStateRepository: UserStateRepository,
val viewStateRepository: ViewStateRepository,
@Value("\${spring.security.oauth2.resourceserver.jwt.issuer-uri:}") oauthIssuerUri: String,
) {
private val auth0Client = WebClient.builder()
.baseUrl(oauthIssuerUri)
.filters {
it += ExchangeFilterFunction.ofResponseProcessor { response ->
logger.info("Response from ${oauthIssuerUri}: ${response.statusCode()}")
return@ofResponseProcessor Mono.just(response);
}
}
.build()

private fun fetchUserInfo(token: JwtAuthenticationToken): Map<String, JsonNode> {
val rawAuthToken = token.token.tokenValue

return auth0Client.get()
.uri("/userinfo")
.header(HttpHeaders.AUTHORIZATION, "Bearer $rawAuthToken")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono<Map<String, JsonNode>>()
.block() ?: emptyMap()
}

private fun fetchUserName(token: JwtAuthenticationToken): String? {
val userInfo = fetchUserInfo(token)
val name = userInfo["name"] ?: userInfo["nickname"] ?: userInfo["given_name"] ?: userInfo["preferred_username"]
return name?.textValue()
}

fun markAutosaveViewState(viewState: ViewState) {
val userState = userStateRepository.findById(viewState.owner).orElse(null)
if (userState == null) {
userStateRepository.save(UserState(viewState.owner, viewState))
} else {
val oldAutosave = userState.autosaveViewState
userState.autosaveViewState = viewState
userStateRepository.save(userState)
viewStateRepository.delete(oldAutosave)
private fun findEntityById(id: String, token: JwtAuthenticationToken): UserState {
return userStateRepository.findById(id).map {
if (it.userName == null) {
it.copy(userName = fetchUserName(token))
} else {
it
}
}.orElseGet {
userStateRepository.save(UserState(id, null, fetchUserName(token)))
}
}

fun findById(id: String, token: JwtAuthenticationToken): UserStateDto {
return UserStateDto(findEntityById(id, token))
}

fun findNameById(id: String): String {
return userStateRepository.findById(id).map(UserState::userName).orElseThrow() ?: ""
}

fun markAutosaveViewState(viewState: ViewState, token: JwtAuthenticationToken) {
require(viewState.owner == token.name)
val userState = findEntityById(viewState.owner, token)

val oldAutosave = userState.autosaveViewState
userState.autosaveViewState = viewState
userStateRepository.save(userState)
oldAutosave?.let { viewStateRepository.delete(it) }
}

companion object {
private val logger = LoggerFactory.getLogger(UserStateService::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package slak.ckompiler.backend.services

import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
import org.springframework.stereotype.Service
import slak.ckompiler.backend.dto.ViewStateDto
import slak.ckompiler.backend.dto.ViewStateListingDto
Expand All @@ -13,9 +14,9 @@ class ViewStateService(val repository: ViewStateRepository, val userStateService
return repository.save(ViewState(viewStateDto))
}

fun saveAutosave(viewStateDto: ViewStateDto): ViewState {
fun saveAutosave(viewStateDto: ViewStateDto, token: JwtAuthenticationToken): ViewState {
val viewState = repository.save(ViewState(viewStateDto))
userStateService.markAutosaveViewState(viewState)
userStateService.markAutosaveViewState(viewState, token)
return viewState
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package slak.ckompiler.backend.services.broadcast

import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import slak.ckompiler.backend.dto.ViewStateNonMetadataDeltaDto
import slak.ckompiler.backend.dto.ViewStateMessageDto
import java.util.*
import java.util.concurrent.ConcurrentHashMap

Expand All @@ -22,7 +22,7 @@ class BroadcastService {
* This cannot be accessed concurrently because we only have a single presenter. If the presenter sends concurrent messages, that's his
* fault.
*/
private val activeBroadcastState: MutableMap<String, ViewStateNonMetadataDeltaDto> = mutableMapOf()
private val activeBroadcastState: MutableMap<String, ViewStateMessageDto> = mutableMapOf()

fun getBroadcastByPresenterId(presenterUserId: String): ActiveBroadcast? {
return activeBroadcasts.values.find { it.presenterUserId == presenterUserId }
Expand Down Expand Up @@ -54,12 +54,25 @@ class BroadcastService {
}
}

fun updateCurrentState(broadcastId: BroadcastId, delta: ViewStateNonMetadataDeltaDto) {
val sourceCode = activeBroadcastState[broadcastId]?.sourceCode ?: delta.sourceCode
activeBroadcastState[broadcastId] = delta.copy(sourceCode = sourceCode)
fun updateCurrentState(broadcastId: BroadcastId, delta: ViewStateMessageDto) {
val oldState = activeBroadcastState[broadcastId]?.state
val newState = delta.state

if (oldState == null) {
activeBroadcastState[broadcastId] = delta
return
}

if (newState == null) {
activeBroadcastState[broadcastId] = ViewStateMessageDto(oldState, delta.pos)
return
}

val sourceCode = delta.state.sourceCode ?: oldState.sourceCode
activeBroadcastState[broadcastId] = delta.copy(state = newState.copy(sourceCode = sourceCode))
}

fun getCurrentState(broadcastId: BroadcastId): ViewStateNonMetadataDeltaDto {
fun getCurrentState(broadcastId: BroadcastId): ViewStateMessageDto {
return activeBroadcastState[broadcastId] ?: throw IllegalStateException("Broadcast state $broadcastId doesn't exist")
}

Expand All @@ -68,6 +81,11 @@ class BroadcastService {
return broadcast.subscribers - broadcast.presenterUserId
}

fun getPresenterId(broadcastId: BroadcastId): String {
val broadcast = activeBroadcasts[broadcastId] ?: throw IllegalStateException("Broadcast state $broadcastId doesn't exist")
return broadcast.presenterUserId
}

fun markSubscription(broadcastId: BroadcastId, subscriberId: String): Boolean {
val broadcast = activeBroadcasts[broadcastId] ?: return false
activeBroadcasts[broadcastId] = broadcast.copy(subscribers = broadcast.subscribers + subscriberId)
Expand Down
Loading

0 comments on commit c65cd5e

Please sign in to comment.