From 91573a4048020d61f83b302266b25fe85c4dfce5 Mon Sep 17 00:00:00 2001 From: dalbodeule <11470513+dalbodeule@users.noreply.github.com> Date: Sun, 4 Aug 2024 14:09:11 +0900 Subject: [PATCH] add Chisu playlist functions - add Websocket - add API - version up to 1.2.0 --- .../chatbot/discord/commands/AlertCommand.kt | 2 +- common/build.gradle.kts | 3 + gradle.properties | 2 +- .../space/mori/chzzk_bot/webserver/Main.kt | 4 +- .../webserver/routes/ApiSongRoutes.kt | 29 +++++ .../webserver/routes/WSSongRoutes.kt | 107 ++++++++++++++++++ .../webserver/routes/WSTimerRoutes.kt | 6 +- 7 files changed, 146 insertions(+), 7 deletions(-) create mode 100644 webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/ApiSongRoutes.kt create mode 100644 webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/WSSongRoutes.kt diff --git a/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/discord/commands/AlertCommand.kt b/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/discord/commands/AlertCommand.kt index 25fa024..801586e 100644 --- a/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/discord/commands/AlertCommand.kt +++ b/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/discord/commands/AlertCommand.kt @@ -16,7 +16,7 @@ import space.mori.chzzk_bot.common.services.UserService object AlertCommand : CommandInterface { private val logger = LoggerFactory.getLogger(this::class.java) override val name: String = "alert" - override val command = Commands.slash(name, "명령어를 추가합니다.") + override val command = Commands.slash(name, "방송알람 채널을 설정합니다. / 알람 취소도 이 명령어를 이용하세요!") .addOptions(OptionData(OptionType.CHANNEL, "channel", "알림을 보낼 채널을 입력하세요.")) .addOptions(OptionData(OptionType.STRING, "content", "표시될 텍스트를 입력하세요. 비워두면 알람이 취소됩니다.")) diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 06d04af..11a0957 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -34,6 +34,9 @@ dependencies { // https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp implementation("com.squareup.okhttp3:okhttp:4.12.0") + // https://mvnrepository.com/artifact/com.google.code.gson/gson + implementation("com.google.code.gson:gson:2.11.0") + testImplementation(kotlin("test")) } diff --git a/gradle.properties b/gradle.properties index d7b3d85..3261d22 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ kotlin.code.style=official group = space.mori -version = 1.1.2 +version = 1.2.0 org.gradle.jvmargs=-Dfile.encoding=UTF-8 org.gradle.console=plain diff --git a/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/Main.kt b/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/Main.kt index 28aa444..fcc3ab0 100644 --- a/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/Main.kt +++ b/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/Main.kt @@ -13,6 +13,7 @@ import io.ktor.server.routing.* import io.ktor.server.websocket.* import kotlinx.serialization.json.Json import space.mori.chzzk_bot.webserver.routes.apiRoutes +import space.mori.chzzk_bot.webserver.routes.wsSongRoutes import space.mori.chzzk_bot.webserver.routes.wsTimerRoutes import java.time.Duration @@ -38,9 +39,10 @@ val server = embeddedServer(Netty, port = 8080) { routing { apiRoutes() wsTimerRoutes() + wsSongRoutes() swaggerUI("swagger-ui/index.html", "openapi/documentation.yaml") { options { - version = "1.1.0" + version = "1.2.0" } } } diff --git a/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/ApiSongRoutes.kt b/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/ApiSongRoutes.kt new file mode 100644 index 0000000..c00e8ab --- /dev/null +++ b/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/ApiSongRoutes.kt @@ -0,0 +1,29 @@ +package space.mori.chzzk_bot.webserver.routes + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import space.mori.chzzk_bot.common.services.SongListService +import space.mori.chzzk_bot.common.services.UserService + +fun Routing.songRoutes() { + route("/songs/{uid}") { + get { + val uid = call.parameters["uid"] + val user = uid?.let { it1 -> UserService.getUser(it1) } + if (user == null) { + call.respondText("No user found", status = HttpStatusCode.NotFound) + return@get + } + + val songs = SongListService.getSong(user) + call.respond(songs) + } + } + route("/songs") { + get { + call.respondText("Require UID", status= HttpStatusCode.BadRequest) + } + } +} \ No newline at end of file diff --git a/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/WSSongRoutes.kt b/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/WSSongRoutes.kt new file mode 100644 index 0000000..b5d6053 --- /dev/null +++ b/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/WSSongRoutes.kt @@ -0,0 +1,107 @@ +package space.mori.chzzk_bot.webserver.routes + +import io.ktor.server.routing.* +import io.ktor.server.websocket.* +import io.ktor.websocket.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.ClosedReceiveChannelException +import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable +import org.koin.java.KoinJavaComponent.inject +import org.slf4j.LoggerFactory +import space.mori.chzzk_bot.common.events.* +import space.mori.chzzk_bot.common.services.UserService +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentLinkedQueue + +fun Routing.wsSongRoutes() { + val sessions = ConcurrentHashMap>() + val status = ConcurrentHashMap() + val logger = LoggerFactory.getLogger(this.javaClass.name) + + fun addSession(uid: String, session: WebSocketServerSession) { + sessions.computeIfAbsent(uid) { ConcurrentLinkedQueue() }.add(session) + } + + fun removeSession(uid: String, session: WebSocketServerSession) { + sessions[uid]?.remove(session) + if(sessions[uid]?.isEmpty() == true) { + sessions.remove(uid) + } + } + + webSocket("/song/{uid}") { + val uid = call.parameters["uid"] + val user = uid?.let { UserService.getUser(it) } + if (uid == null) { + close(CloseReason(CloseReason.Codes.CANNOT_ACCEPT, "Invalid UID")) + return@webSocket + } + if (user == null) { + close(CloseReason(CloseReason.Codes.CANNOT_ACCEPT, "Invalid UID")) + return@webSocket + } + + addSession(uid, this) + + if(status[uid] == TimerType.STREAM_OFF) { + CoroutineScope(Dispatchers.Default).launch { + sendSerialized(SongResponse( + SongType.STREAM_OFF.value, + uid, + null, + null, + null, + null + )) + } + } + + try { + for (frame in incoming) { + when(frame) { + is Frame.Text -> { + + } + is Frame.Ping -> send(Frame.Pong(frame.data)) + else -> { + + } + } + } + } catch(e: ClosedReceiveChannelException) { + logger.error("Error in WebSocket: ${e.message}") + } finally { + removeSession(uid, this) + } + } + + val dispatcher: CoroutinesEventBus by inject(CoroutinesEventBus::class.java) + + dispatcher.subscribe(SongEvent::class) { + logger.debug("SongEvent: {} / {} {}", it.uid, it.type, it.name) + CoroutineScope(Dispatchers.Default).launch { + sessions[it.uid]?.forEach { ws -> + ws.sendSerialized(SongResponse( + it.type.value, + it.uid, + it.req_uid, + it.name, + it.author, + it.time + )) + } + } + } +} + +@Serializable +data class SongResponse( + val type: Int, + val uid: String, + val reqUid: String?, + val name: String?, + val author: String?, + val time: Int? +) diff --git a/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/WSTimerRoutes.kt b/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/WSTimerRoutes.kt index 215df6a..9e0a567 100644 --- a/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/WSTimerRoutes.kt +++ b/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/WSTimerRoutes.kt @@ -9,18 +9,16 @@ import kotlinx.coroutines.channels.ClosedReceiveChannelException import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import org.koin.java.KoinJavaComponent.inject -import org.slf4j.Logger import org.slf4j.LoggerFactory import space.mori.chzzk_bot.common.events.* import space.mori.chzzk_bot.common.services.UserService import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentLinkedQueue -val logger: Logger = LoggerFactory.getLogger("WSTimerRoutes") - fun Routing.wsTimerRoutes() { val sessions = ConcurrentHashMap>() val status = ConcurrentHashMap() + val logger = LoggerFactory.getLogger(this.javaClass.name) fun addSession(uid: String, session: WebSocketServerSession) { sessions.computeIfAbsent(uid) { ConcurrentLinkedQueue() }.add(session) @@ -49,7 +47,7 @@ fun Routing.wsTimerRoutes() { if(status[uid] == TimerType.STREAM_OFF) { CoroutineScope(Dispatchers.Default).launch { - sendSerialized(TimerResponse(TimerType.STREAM_OFF.value, "")) + sendSerialized(TimerResponse(TimerType.STREAM_OFF.value, null)) } }