mirror of
https://github.com/dalbodeule/chibot-chzzk-bot.git
synced 2025-06-09 07:18:22 +00:00
Merge pull request #26 from dalbodeule/develop
add Chisu playlist, etc...
This commit is contained in:
commit
cc81e6d722
1
.idea/.gitignore
generated
vendored
1
.idea/.gitignore
generated
vendored
@ -6,3 +6,4 @@
|
|||||||
# Datasource local storage ignored files
|
# Datasource local storage ignored files
|
||||||
/dataSources/
|
/dataSources/
|
||||||
/dataSources.local.xml
|
/dataSources.local.xml
|
||||||
|
discord.xml
|
@ -4,15 +4,11 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.java.KoinJavaComponent.inject
|
import org.koin.java.KoinJavaComponent.inject
|
||||||
import space.mori.chzzk_bot.common.events.CoroutinesEventBus
|
import space.mori.chzzk_bot.common.events.*
|
||||||
import space.mori.chzzk_bot.common.events.TimerEvent
|
|
||||||
import space.mori.chzzk_bot.common.events.TimerType
|
|
||||||
import space.mori.chzzk_bot.common.models.User
|
import space.mori.chzzk_bot.common.models.User
|
||||||
import space.mori.chzzk_bot.common.services.CommandService
|
import space.mori.chzzk_bot.common.services.*
|
||||||
import space.mori.chzzk_bot.common.services.CounterService
|
|
||||||
import space.mori.chzzk_bot.common.services.TimerConfigService
|
|
||||||
import space.mori.chzzk_bot.common.services.UserService
|
|
||||||
import space.mori.chzzk_bot.common.utils.getUptime
|
import space.mori.chzzk_bot.common.utils.getUptime
|
||||||
|
import space.mori.chzzk_bot.common.utils.getYoutubeVideo
|
||||||
import xyz.r2turntrue.chzzk4j.chat.ChatMessage
|
import xyz.r2turntrue.chzzk4j.chat.ChatMessage
|
||||||
import xyz.r2turntrue.chzzk4j.chat.ChzzkChat
|
import xyz.r2turntrue.chzzk4j.chat.ChzzkChat
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
@ -46,7 +42,13 @@ class MessageHandler(
|
|||||||
val user = UserService.getUser(channel.channelId)
|
val user = UserService.getUser(channel.channelId)
|
||||||
?: throw RuntimeException("User not found. it's bug? ${channel.channelName} - ${channel.channelId}")
|
?: throw RuntimeException("User not found. it's bug? ${channel.channelName} - ${channel.channelId}")
|
||||||
val commands = CommandService.getCommands(user)
|
val commands = CommandService.getCommands(user)
|
||||||
val manageCommands = mapOf("!명령어추가" to this::manageAddCommand, "!명령어삭제" to this::manageRemoveCommand, "!명령어수정" to this::manageUpdateCommand, "!시간" to this::timerCommand)
|
val manageCommands = mapOf(
|
||||||
|
"!명령어추가" to this::manageAddCommand,
|
||||||
|
"!명령어삭제" to this::manageRemoveCommand,
|
||||||
|
"!명령어수정" to this::manageUpdateCommand,
|
||||||
|
"!시간" to this::timerCommand,
|
||||||
|
"!노래추가" to this::songAddCommand
|
||||||
|
)
|
||||||
|
|
||||||
manageCommands.forEach { (commandName, command) ->
|
manageCommands.forEach { (commandName, command) ->
|
||||||
this.commands[commandName] = command
|
this.commands[commandName] = command
|
||||||
@ -186,6 +188,50 @@ class MessageHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// songs
|
||||||
|
fun songAddCommand(msg: ChatMessage, user: User) {
|
||||||
|
val parts = msg.content.split(" ", limit = 3)
|
||||||
|
if (parts.size < 2) {
|
||||||
|
listener.sendChat("유튜브 URL을 입력해주세요!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val url = parts[1]
|
||||||
|
val songs = SongListService.getSong(user)
|
||||||
|
|
||||||
|
if (songs.any { it.url == url }) {
|
||||||
|
listener.sendChat("같은 노래가 이미 신청되어 있습니다.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val video = getYoutubeVideo(url)
|
||||||
|
if (video == null) {
|
||||||
|
listener.sendChat("유튜브에서 찾을 수 없어요!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SongListService.saveSong(
|
||||||
|
user,
|
||||||
|
msg.userId,
|
||||||
|
video.url,
|
||||||
|
video.name,
|
||||||
|
video.author,
|
||||||
|
video.length
|
||||||
|
)
|
||||||
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
dispatcher.post(SongEvent(
|
||||||
|
user.token,
|
||||||
|
SongType.ADD,
|
||||||
|
msg.userId,
|
||||||
|
video.name,
|
||||||
|
video.author,
|
||||||
|
video.length
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
listener.sendChat("노래가 추가되었습니다.")
|
||||||
|
}
|
||||||
|
|
||||||
internal fun handle(msg: ChatMessage, user: User) {
|
internal fun handle(msg: ChatMessage, user: User) {
|
||||||
val commandKey = msg.content.split(' ')[0]
|
val commandKey = msg.content.split(' ')[0]
|
||||||
|
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
package space.mori.chzzk_bot.chatbot.chzzk
|
||||||
|
|
||||||
|
import space.mori.chzzk_bot.common.models.User
|
||||||
|
import space.mori.chzzk_bot.common.services.CommandService
|
||||||
|
import xyz.r2turntrue.chzzk4j.chat.ChatMessage
|
||||||
|
|
||||||
|
class SongModule {
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -64,7 +64,7 @@ object AddCommand : CommandInterface {
|
|||||||
try {
|
try {
|
||||||
ChzzkHandler.reloadCommand(chzzkChannel!!)
|
ChzzkHandler.reloadCommand(chzzkChannel!!)
|
||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {}
|
||||||
event.hook.sendMessage("등록이 완료되었습니다. $label = $content/$failContent").queue()
|
event.hook.sendMessage("등록이 완료되었습니다. $label = $content / $failContent").queue()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
event.hook.sendMessage("에러가 발생했습니다.").queue()
|
event.hook.sendMessage("에러가 발생했습니다.").queue()
|
||||||
logger.debug(e.stackTraceToString())
|
logger.debug(e.stackTraceToString())
|
||||||
|
@ -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", "표시될 텍스트를 입력하세요. 비워두면 알람이 취소됩니다."))
|
||||||
|
|
||||||
|
@ -31,6 +31,12 @@ dependencies {
|
|||||||
// https://mvnrepository.com/artifact/io.github.cdimascio/dotenv-kotlin
|
// https://mvnrepository.com/artifact/io.github.cdimascio/dotenv-kotlin
|
||||||
implementation("io.github.cdimascio:dotenv-kotlin:6.4.1")
|
implementation("io.github.cdimascio:dotenv-kotlin:6.4.1")
|
||||||
|
|
||||||
|
// 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"))
|
testImplementation(kotlin("test"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +32,9 @@ object Connector {
|
|||||||
PersonalCounters,
|
PersonalCounters,
|
||||||
Managers,
|
Managers,
|
||||||
TimerConfigs,
|
TimerConfigs,
|
||||||
LiveStatuses
|
LiveStatuses,
|
||||||
|
SongLists,
|
||||||
|
SongConfigs
|
||||||
)
|
)
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
package space.mori.chzzk_bot.common.events
|
||||||
|
|
||||||
|
enum class SongType(var value: Int) {
|
||||||
|
ADD(0),
|
||||||
|
REMOVE(1),
|
||||||
|
NEXT(2),
|
||||||
|
|
||||||
|
STREAM_OFF(50)
|
||||||
|
}
|
||||||
|
|
||||||
|
class SongEvent(
|
||||||
|
val uid: String,
|
||||||
|
val type: SongType,
|
||||||
|
val req_uid: String?,
|
||||||
|
val name: String?,
|
||||||
|
val author: String?,
|
||||||
|
val time: Int?,
|
||||||
|
): Event {
|
||||||
|
var TAG = javaClass.simpleName
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
package space.mori.chzzk_bot.common.models
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.dao.IntEntity
|
||||||
|
import org.jetbrains.exposed.dao.IntEntityClass
|
||||||
|
import org.jetbrains.exposed.dao.id.EntityID
|
||||||
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
import org.jetbrains.exposed.sql.ReferenceOption
|
||||||
|
|
||||||
|
object SongConfigs: IntIdTable("song_config") {
|
||||||
|
val user = reference("user", Users, onDelete = ReferenceOption.CASCADE)
|
||||||
|
val option = integer("option")
|
||||||
|
}
|
||||||
|
class SongConfig(id: EntityID<Int>) : IntEntity(id) {
|
||||||
|
companion object : IntEntityClass<TimerConfig>(TimerConfigs)
|
||||||
|
|
||||||
|
var user by User referencedOn TimerConfigs.user
|
||||||
|
var option by TimerConfigs.option
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package space.mori.chzzk_bot.common.models
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.dao.IntEntity
|
||||||
|
import org.jetbrains.exposed.dao.IntEntityClass
|
||||||
|
import org.jetbrains.exposed.dao.id.EntityID
|
||||||
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
import org.jetbrains.exposed.sql.javatime.datetime
|
||||||
|
|
||||||
|
object SongLists: IntIdTable("song_list") {
|
||||||
|
val user = reference("user", Users)
|
||||||
|
val uid = varchar("uid", 64)
|
||||||
|
val url = varchar("url", 128)
|
||||||
|
val name = text("name")
|
||||||
|
val author = text("author")
|
||||||
|
val time = integer("time")
|
||||||
|
val created_at = datetime("created_at")
|
||||||
|
}
|
||||||
|
|
||||||
|
class SongList(id: EntityID<Int>) : IntEntity(id) {
|
||||||
|
companion object : IntEntityClass<SongList>(SongLists)
|
||||||
|
|
||||||
|
var url by SongLists.url
|
||||||
|
var name by SongLists.name
|
||||||
|
var author by SongLists.author
|
||||||
|
var time by SongLists.time
|
||||||
|
var created_at by SongLists.created_at
|
||||||
|
|
||||||
|
var user by User referencedOn SongLists.user
|
||||||
|
var uid by SongLists.uid
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
package space.mori.chzzk_bot.common.services
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
|
import org.jetbrains.exposed.sql.and
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import space.mori.chzzk_bot.common.models.SongList
|
||||||
|
import space.mori.chzzk_bot.common.models.SongLists
|
||||||
|
import space.mori.chzzk_bot.common.models.User
|
||||||
|
|
||||||
|
object SongListService {
|
||||||
|
fun saveSong(user: User, uid: String, url: String, name: String, author: String, time: Int) {
|
||||||
|
return transaction {
|
||||||
|
SongList.new {
|
||||||
|
this.user = user
|
||||||
|
this.uid = uid
|
||||||
|
this.url = url
|
||||||
|
this.name = name
|
||||||
|
this.author = author
|
||||||
|
this.time = time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSong(user: User, uid: String): List<SongList> {
|
||||||
|
return transaction {
|
||||||
|
SongList.find(
|
||||||
|
(SongLists.user eq user.id) and
|
||||||
|
(SongLists.uid eq uid)
|
||||||
|
).toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSong(user: User): List<SongList> {
|
||||||
|
return transaction {
|
||||||
|
SongList.find(SongLists.user eq user.id).toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteSong(user: User, uid: String, name: String): SongList {
|
||||||
|
return transaction {
|
||||||
|
val songRow = SongList.find(
|
||||||
|
(SongLists.user eq user.id) and
|
||||||
|
(SongLists.uid eq uid) and
|
||||||
|
(SongLists.name eq name)
|
||||||
|
).firstOrNull()
|
||||||
|
|
||||||
|
songRow ?: throw RuntimeException("Song not found! ${user.username} / $uid / $name")
|
||||||
|
|
||||||
|
songRow.delete()
|
||||||
|
songRow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
package space.mori.chzzk_bot.common.utils
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import io.github.cdimascio.dotenv.dotenv
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
data class YoutubeVideo(
|
||||||
|
val url: String,
|
||||||
|
val name: String,
|
||||||
|
val author: String,
|
||||||
|
val length: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
val regex = ".*(?:youtu.be/|v/|u/\\w/|embed/|watch\\?v=|&v=)([^#&?]*).*".toRegex()
|
||||||
|
val durationRegex = """PT(\d+H)?(\d+m)?(\d+S)?""".toRegex()
|
||||||
|
|
||||||
|
val client = OkHttpClient()
|
||||||
|
val gson = Gson()
|
||||||
|
|
||||||
|
val dotenv = dotenv {
|
||||||
|
ignoreIfMissing = true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun getYoutubeVideoId(url: String): String? {
|
||||||
|
val matchResult = regex.find(url)
|
||||||
|
|
||||||
|
return matchResult?.groups?.get(1)?.value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseDuration(duration: String): Int {
|
||||||
|
val matchResult = durationRegex.find(duration)
|
||||||
|
val (hours, minutes, seconds) = matchResult?.destructured ?: return 0
|
||||||
|
|
||||||
|
val hourInSec = hours.dropLast(1).toIntOrNull()?.times(3600) ?: 0
|
||||||
|
val minutesInSec = minutes.dropLast(1).toIntOrNull()?.times(60) ?: 0
|
||||||
|
val totalSeconds = seconds.dropLast(1).toIntOrNull() ?: 0
|
||||||
|
|
||||||
|
return hourInSec + minutesInSec + totalSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getYoutubeVideo(url: String): YoutubeVideo? {
|
||||||
|
val videoId = getYoutubeVideoId(url)
|
||||||
|
|
||||||
|
val api = HttpUrl.Builder()
|
||||||
|
.scheme("https")
|
||||||
|
.host("www.googleapis.com")
|
||||||
|
.addPathSegment("youtube")
|
||||||
|
.addPathSegment("v3")
|
||||||
|
.addPathSegment("videos")
|
||||||
|
.addQueryParameter("id", videoId)
|
||||||
|
.addQueryParameter("key", dotenv["YOUTUBE_API_KEY"])
|
||||||
|
.addQueryParameter("part", "snippet")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(api)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
client.newCall(request).execute().use { response ->
|
||||||
|
if (!response.isSuccessful) throw IOException("Unexpected code $response")
|
||||||
|
|
||||||
|
val responseBody = response.body?.string()
|
||||||
|
val json = gson.fromJson(responseBody, JsonObject::class.java)
|
||||||
|
val items = json.getAsJsonArray("items")
|
||||||
|
|
||||||
|
if (items == null || items.size() == 0) return null
|
||||||
|
|
||||||
|
val item = items[0].asJsonObject
|
||||||
|
val snippet = item.getAsJsonObject("snippet")
|
||||||
|
val contentDetail = item.asJsonObject.getAsJsonObject("contentDetail")
|
||||||
|
val status = contentDetail.getAsJsonObject("status")
|
||||||
|
|
||||||
|
if (!status.get("embeddable").asBoolean) return null
|
||||||
|
|
||||||
|
val duration = contentDetail.get("duration").asString
|
||||||
|
val length = parseDuration(duration)
|
||||||
|
|
||||||
|
return YoutubeVideo(
|
||||||
|
"https://www.youtube.com/watch?v=$videoId",
|
||||||
|
snippet.get("title").asString,
|
||||||
|
snippet.get("channelTitle").asString,
|
||||||
|
length
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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?
|
||||||
|
)
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user