add Chisu playlist functions

- add Websocket
- add API
- version up to 1.2.0
This commit is contained in:
dalbodeule 2024-08-04 14:09:11 +09:00
parent dc81bb09f2
commit 91573a4048
No known key found for this signature in database
GPG Key ID: EFA860D069C9FA65
7 changed files with 146 additions and 7 deletions

View File

@ -16,7 +16,7 @@ import space.mori.chzzk_bot.common.services.UserService
object AlertCommand : CommandInterface { object AlertCommand : CommandInterface {
private val logger = LoggerFactory.getLogger(this::class.java) private val logger = LoggerFactory.getLogger(this::class.java)
override val name: String = "alert" 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.CHANNEL, "channel", "알림을 보낼 채널을 입력하세요."))
.addOptions(OptionData(OptionType.STRING, "content", "표시될 텍스트를 입력하세요. 비워두면 알람이 취소됩니다.")) .addOptions(OptionData(OptionType.STRING, "content", "표시될 텍스트를 입력하세요. 비워두면 알람이 취소됩니다."))

View File

@ -34,6 +34,9 @@ dependencies {
// https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp // https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp
implementation("com.squareup.okhttp3:okhttp:4.12.0") 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")) testImplementation(kotlin("test"))
} }

View File

@ -1,6 +1,6 @@
kotlin.code.style=official kotlin.code.style=official
group = space.mori group = space.mori
version = 1.1.2 version = 1.2.0
org.gradle.jvmargs=-Dfile.encoding=UTF-8 org.gradle.jvmargs=-Dfile.encoding=UTF-8
org.gradle.console=plain org.gradle.console=plain

View File

@ -13,6 +13,7 @@ import io.ktor.server.routing.*
import io.ktor.server.websocket.* import io.ktor.server.websocket.*
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import space.mori.chzzk_bot.webserver.routes.apiRoutes 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 space.mori.chzzk_bot.webserver.routes.wsTimerRoutes
import java.time.Duration import java.time.Duration
@ -38,9 +39,10 @@ val server = embeddedServer(Netty, port = 8080) {
routing { routing {
apiRoutes() apiRoutes()
wsTimerRoutes() wsTimerRoutes()
wsSongRoutes()
swaggerUI("swagger-ui/index.html", "openapi/documentation.yaml") { swaggerUI("swagger-ui/index.html", "openapi/documentation.yaml") {
options { options {
version = "1.1.0" version = "1.2.0"
} }
} }
} }

View File

@ -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)
}
}
}

View File

@ -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<String, ConcurrentLinkedQueue<WebSocketServerSession>>()
val status = ConcurrentHashMap<String, TimerType>()
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?
)

View File

@ -9,18 +9,16 @@ import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.koin.java.KoinJavaComponent.inject import org.koin.java.KoinJavaComponent.inject
import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import space.mori.chzzk_bot.common.events.* import space.mori.chzzk_bot.common.events.*
import space.mori.chzzk_bot.common.services.UserService import space.mori.chzzk_bot.common.services.UserService
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
val logger: Logger = LoggerFactory.getLogger("WSTimerRoutes")
fun Routing.wsTimerRoutes() { fun Routing.wsTimerRoutes() {
val sessions = ConcurrentHashMap<String, ConcurrentLinkedQueue<WebSocketServerSession>>() val sessions = ConcurrentHashMap<String, ConcurrentLinkedQueue<WebSocketServerSession>>()
val status = ConcurrentHashMap<String, TimerType>() val status = ConcurrentHashMap<String, TimerType>()
val logger = LoggerFactory.getLogger(this.javaClass.name)
fun addSession(uid: String, session: WebSocketServerSession) { fun addSession(uid: String, session: WebSocketServerSession) {
sessions.computeIfAbsent(uid) { ConcurrentLinkedQueue() }.add(session) sessions.computeIfAbsent(uid) { ConcurrentLinkedQueue() }.add(session)
@ -49,7 +47,7 @@ fun Routing.wsTimerRoutes() {
if(status[uid] == TimerType.STREAM_OFF) { if(status[uid] == TimerType.STREAM_OFF) {
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
sendSerialized(TimerResponse(TimerType.STREAM_OFF.value, "")) sendSerialized(TimerResponse(TimerType.STREAM_OFF.value, null))
} }
} }