Skip to content

Commit c9e7121

Browse files
committed
feat: add ClientBroadcast message
Clients can broadcast arbitrary strings to do whatever using this new message type.
1 parent d020d2a commit c9e7121

8 files changed

Lines changed: 98 additions & 10 deletions

File tree

src/main/kotlin/pp/api/Rooms.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import jakarta.websocket.CloseReason.CloseCodes.VIOLATED_POLICY
88
import jakarta.websocket.Session
99
import pp.api.data.ChangeName
1010
import pp.api.data.ChatMessage
11+
import pp.api.data.ClientBroadcast
1112
import pp.api.data.GamePhase
1213
import pp.api.data.GamePhase.CARDS_REVEALED
1314
import pp.api.data.GamePhase.PLAYING
@@ -120,6 +121,7 @@ class Rooms {
120121
is ChatMessage -> chatMessage(session, request.message)
121122
is RevealCards -> changeGamePhase(session, CARDS_REVEALED)
122123
is StartNewRound -> changeGamePhase(session, PLAYING)
124+
is ClientBroadcast -> doClientBroadcast(session, request.payload)
123125
else -> {
124126
// spotlessApply keeps generating this else if it doesn't exist
125127
}
@@ -191,7 +193,7 @@ class Rooms {
191193
room withInfo "${user.username} tried to play card with illegal value: $cardValue"
192194
} else {
193195
user.cardValue = cardValue
194-
room
196+
room.copy()
195197
}
196198
} else {
197199
room withInfo "${user.username} tried to play card while no round was in progress"
@@ -207,6 +209,12 @@ class Rooms {
207209
}
208210
}
209211

212+
private fun doClientBroadcast(session: Session, payload: String) {
213+
withUser(session) { room, user ->
214+
room withBroadcast payload
215+
}
216+
}
217+
210218
private fun changeGamePhase(session: Session, newGamePhase: GamePhase) {
211219
withUser(session) { room, user ->
212220
val canRevealCards = room.gamePhase == PLAYING && newGamePhase == CARDS_REVEALED

src/main/kotlin/pp/api/data/LogLevel.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,11 @@ enum class LogLevel {
2222
* Clients should consider simply reconnecting on every error
2323
*/
2424
ERROR,
25+
26+
/**
27+
* A broadcast message sent by a client
28+
*/
29+
CLIENT_BROADCAST,
30+
2531
;
2632
}

src/main/kotlin/pp/api/data/Room.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package pp.api.data
33
import io.quarkus.logging.Log
44
import jakarta.websocket.Session
55
import pp.api.data.GamePhase.PLAYING
6+
import pp.api.data.LogLevel.CLIENT_BROADCAST
67
import pp.api.data.UserType.PARTICIPANT
78

89
/**
@@ -24,7 +25,7 @@ import pp.api.data.UserType.PARTICIPANT
2425
@Suppress("TooManyFunctions", "What can we do about this?")
2526
class Room(
2627
val roomId: String,
27-
val version: ULong = 1u,
28+
val version: Long = 1,
2829
val users: List<User> = listOf(),
2930
val deck: List<String> = listOf("1", "2", "3", "5", "8", "13", "☕"),
3031
val gamePhase: GamePhase = PLAYING,
@@ -56,7 +57,7 @@ class Room(
5657
fun copy(
5758
roomId: String = this.roomId,
5859
users: List<User> = this.users,
59-
version: ULong = this.version + 1u,
60+
version: Long = this.version + 1,
6061
deck: List<String> = this.deck,
6162
gamePhase: GamePhase = this.gamePhase,
6263
log: List<LogEntry> = this.log,
@@ -134,6 +135,14 @@ class Room(
134135
*/
135136
infix fun withChatMessage(message: String): Room = withLogEntry(chat(message))
136137

138+
/**
139+
* Crate a copy of this room, with the given message added as [LogEntry] with level [LogLevel.CLIENT_BROADCAST]
140+
*
141+
* @param payload the chat message to add
142+
* @return a copy of this room, with the message added as [LogEntry] with [LogLevel] [LogLevel.CLIENT_BROADCAST]
143+
*/
144+
infix fun withBroadcast(payload: String): Room = withLogEntry(LogEntry(CLIENT_BROADCAST, payload))
145+
137146
private infix fun withLogEntry(entry: LogEntry): Room = copy(
138147
log = log + entry,
139148
)

src/main/kotlin/pp/api/data/UserRequest.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY
1010
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME
1111
import pp.api.data.RequestType.CHANGE_NAME
1212
import pp.api.data.RequestType.CHAT_MESSAGE
13+
import pp.api.data.RequestType.CLIENT_BROADCAST
1314
import pp.api.data.RequestType.PLAY_CARD
1415
import pp.api.data.RequestType.REVEAL_CARDS
1516
import pp.api.data.RequestType.START_NEW_ROUND
@@ -45,6 +46,11 @@ enum class RequestType {
4546
* User wants to start the next round
4647
*/
4748
START_NEW_ROUND,
49+
50+
/**
51+
* Client wants to broadcast a message
52+
*/
53+
CLIENT_BROADCAST,
4854
;
4955
}
5056

@@ -117,3 +123,29 @@ class StartNewRound : UserRequest(START_NEW_ROUND) {
117123

118124
override fun hashCode(): Int = "StartNewRound".hashCode()
119125
}
126+
127+
/**
128+
* Message a user (or better: the users' client) sends if it wants to broadcast
129+
* "something" to other clients.
130+
*
131+
* @property payload the broadcast payload
132+
*/
133+
class ClientBroadcast(
134+
val payload: String,
135+
) : UserRequest(CLIENT_BROADCAST) {
136+
init {
137+
require(payload.isNotBlank()) {
138+
"Payload cannot be blank"
139+
}
140+
require(payload.length <= MAX_PAYLOAD_LENGTH) {
141+
"Payload cannot be longer than $MAX_PAYLOAD_LENGTH"
142+
}
143+
}
144+
145+
companion object {
146+
/**
147+
* Maximum length of a broadcast payload
148+
*/
149+
const val MAX_PAYLOAD_LENGTH = 10_000
150+
}
151+
}

src/main/kotlin/pp/api/dto/RoomDto.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ import java.util.Locale.US
2020
* @property average represents the average of the card values played. Will only show real data if [gamePhase] is
2121
* [CARDS_REVEALED]
2222
* @property log list of [LogEntry]s for this rooms
23-
* @property version
23+
* @property version current version of the room
2424
*/
2525
// see https://quarkus.io/guides/writing-native-applications-tips#registerForReflection
2626
@RegisterForReflection(registerFullHierarchy = true)
2727
data class RoomDto(
2828
val roomId: String,
29-
val version: ULong,
29+
val version: Long,
3030
val deck: List<String>,
3131
val gamePhase: GamePhase,
3232
val users: List<UserDto>,

src/test/kotlin/pp/api/RoomTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ class RoomTest {
118118

119119
@Test
120120
fun versionAscends() {
121-
val room = Room(roomId = "nice-id", version = 9u)
121+
val room = Room(roomId = "nice-id", version = 9)
122122
val updated = room.copy()
123123
assertEquals(10U.toInt(), updated.version.toInt())
124124
val room2 = Room(roomId = "nice-id")

src/test/kotlin/pp/api/RoomsTest.kt

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ import org.mockito.Mockito.mock
1414
import org.mockito.kotlin.whenever
1515
import pp.api.data.ChangeName
1616
import pp.api.data.ChatMessage
17+
import pp.api.data.ClientBroadcast
1718
import pp.api.data.GamePhase.CARDS_REVEALED
1819
import pp.api.data.GamePhase.PLAYING
1920
import pp.api.data.LogEntry
2021
import pp.api.data.LogLevel.CHAT
22+
import pp.api.data.LogLevel.CLIENT_BROADCAST
2123
import pp.api.data.LogLevel.INFO
2224
import pp.api.data.PlayCard
2325
import pp.api.data.RevealCards
@@ -376,8 +378,9 @@ class RoomsTest {
376378
3, rooms.getRooms().first().log
377379
.size
378380
)
379-
assertTrue(rooms.getRooms().first().log
380-
.any { it.level == INFO && "tried to change game phase" in it.message })
381+
assertTrue(
382+
rooms.getRooms().first().log
383+
.any { it.level == INFO && "tried to change game phase" in it.message })
381384
}
382385

383386
@Test
@@ -444,8 +447,27 @@ class RoomsTest {
444447
2, rooms.getRooms().first().log
445448
.size
446449
)
447-
assertTrue(rooms.getRooms().first().log
448-
.any { it.level == INFO && "tried to change game phase" in it.message })
450+
assertTrue(
451+
rooms.getRooms().first().log
452+
.any { it.level == INFO && "tried to change game phase" in it.message })
453+
}
454+
455+
@Test
456+
fun canBroadcastMessages() {
457+
val rooms = Rooms()
458+
val remote = mock(Async::class.java)
459+
val session = mock(Session::class.java)
460+
whenever(session.asyncRemote).thenReturn(remote)
461+
whenever(remote.sendObject(any())).thenReturn(constantFuture(null))
462+
val user = User("username", SPECTATOR, "7", session)
463+
whenever(session.id).thenReturn("new-session-id")
464+
rooms.ensureRoomContainsUser("nice-id", user)
465+
rooms.submitUserRequest(ClientBroadcast("nice broadcast message"), session)
466+
assertEquals(
467+
LogEntry(level = CLIENT_BROADCAST, message = "nice broadcast message"),
468+
rooms.getRooms().first().log
469+
.last()
470+
)
449471
}
450472

451473
@Test

src/test/kotlin/pp/api/data/UserRequestTest.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package pp.api.data
22

33
import org.junit.jupiter.api.Assertions.assertEquals
44
import org.junit.jupiter.api.Test
5+
import org.junit.jupiter.api.assertThrows
56

67
class UserRequestTest {
78
@Test
@@ -13,4 +14,14 @@ class UserRequestTest {
1314
fun hashStartNewRound() {
1415
assertEquals(StartNewRound().hashCode(), StartNewRound().hashCode())
1516
}
17+
18+
@Test
19+
fun `Cannot make empty ClientBroadcast`() {
20+
assertThrows<IllegalArgumentException> { ClientBroadcast("") }
21+
}
22+
23+
@Test
24+
fun `Cannot make ClientBroadcast with payload to large`() {
25+
assertThrows<IllegalArgumentException> { ClientBroadcast("abracadabra".repeat(1_000)) }
26+
}
1627
}

0 commit comments

Comments
 (0)