mirror of
https://github.com/dalbodeule/chibot-chzzk-bot.git
synced 2025-06-09 07:18:22 +00:00
add WSSongListRoutes.kt
- add Websocket backend. - add session start command. - some improve logics.
This commit is contained in:
parent
47228394d5
commit
0a4e8193bb
3
.idea/.gitignore
generated
vendored
3
.idea/.gitignore
generated
vendored
@ -6,4 +6,5 @@
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
discord.xml
|
||||
discord.xml
|
||||
inspectionProfiles/Project_Default.xml
|
@ -4,10 +4,12 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.java.KoinJavaComponent.inject
|
||||
import space.mori.chzzk_bot.chatbot.discord.Discord.Companion.bot
|
||||
import space.mori.chzzk_bot.common.events.*
|
||||
import space.mori.chzzk_bot.common.models.User
|
||||
import space.mori.chzzk_bot.common.services.*
|
||||
import space.mori.chzzk_bot.common.utils.getFollowDate
|
||||
import space.mori.chzzk_bot.common.utils.getRandomString
|
||||
import space.mori.chzzk_bot.common.utils.getUptime
|
||||
import space.mori.chzzk_bot.common.utils.getYoutubeVideo
|
||||
import xyz.r2turntrue.chzzk4j.chat.ChatMessage
|
||||
@ -15,6 +17,7 @@ import xyz.r2turntrue.chzzk4j.chat.ChzzkChat
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.UUID
|
||||
|
||||
|
||||
class MessageHandler(
|
||||
@ -57,7 +60,8 @@ class MessageHandler(
|
||||
"!명령어수정" to this::manageUpdateCommand,
|
||||
"!시간" to this::timerCommand,
|
||||
"!노래추가" to this::songAddCommand,
|
||||
"!노래목록" to this::songListCommand
|
||||
"!노래목록" to this::songListCommand,
|
||||
"!노래시작" to this::songStartCommand
|
||||
)
|
||||
|
||||
manageCommands.forEach { (commandName, command) ->
|
||||
@ -248,6 +252,26 @@ class MessageHandler(
|
||||
listener.sendChat("리스트는 여기입니다. https://nabot.mori.space/songs/${user.token}")
|
||||
}
|
||||
|
||||
private fun songStartCommand(msg: ChatMessage, user: User) {
|
||||
if (msg.profile?.userRoleCode == "common_user") {
|
||||
listener.sendChat("매니저만 이 명령어를 사용할 수 있습니다.")
|
||||
return
|
||||
}
|
||||
|
||||
val session = "${UUID.randomUUID()}${UUID.randomUUID()}".replace("-", "")
|
||||
val password = getRandomString(8)
|
||||
|
||||
SongConfigService.updateSession(user, session, password)
|
||||
|
||||
bot.getUserById(user.token)?.let { it ->
|
||||
val channel = it.openPrivateChannel()
|
||||
channel.onSuccess { privateChannel ->
|
||||
privateChannel.sendMessage("여기로 접속해주세요! https://nabot,mori.space/songlist/${session}.\n인증번호는 ||$password|| 입니다.").queue()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal fun handle(msg: ChatMessage, user: User) {
|
||||
if(msg.userId == ChzzkHandler.botUid) return
|
||||
|
||||
|
@ -8,11 +8,19 @@ import org.jetbrains.exposed.sql.ReferenceOption
|
||||
|
||||
object SongConfigs: IntIdTable("song_config") {
|
||||
val user = reference("user", Users, onDelete = ReferenceOption.CASCADE)
|
||||
val option = integer("option")
|
||||
val token = varchar("token", 64).nullable()
|
||||
val password = varchar("password", 8).nullable()
|
||||
val streamerOnly = bool("streamer_only").default(false)
|
||||
val queueLimit = integer("queue_limit").default(50)
|
||||
val personalLimit = integer("personal_limit").default(5)
|
||||
}
|
||||
class SongConfig(id: EntityID<Int>) : IntEntity(id) {
|
||||
companion object : IntEntityClass<TimerConfig>(TimerConfigs)
|
||||
companion object : IntEntityClass<SongConfig>(SongConfigs)
|
||||
|
||||
var user by User referencedOn TimerConfigs.user
|
||||
var option by TimerConfigs.option
|
||||
var user by User referencedOn SongConfigs.user
|
||||
var token by SongConfigs.token
|
||||
var password by SongConfigs.password
|
||||
var streamerOnly by SongConfigs.streamerOnly
|
||||
var queueLimit by SongConfigs.queueLimit
|
||||
var personalLimit by SongConfigs.personalLimit
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package space.mori.chzzk_bot.common.services
|
||||
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import space.mori.chzzk_bot.common.models.SongConfig
|
||||
import space.mori.chzzk_bot.common.models.SongConfigs
|
||||
import space.mori.chzzk_bot.common.models.User
|
||||
|
||||
object SongConfigService {
|
||||
private fun initConfig(user: User): SongConfig {
|
||||
return transaction {
|
||||
SongConfig.new {
|
||||
this.user = user
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getConfig(user: User): SongConfig {
|
||||
return transaction {
|
||||
var songConfig = SongConfig.find(SongConfigs.user eq user.id).firstOrNull()
|
||||
if (songConfig == null) {
|
||||
songConfig = initConfig(user)
|
||||
}
|
||||
songConfig
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun getConfig(token: String): SongConfig? {
|
||||
return transaction {
|
||||
SongConfig.find(SongConfigs.token eq token).firstOrNull()
|
||||
}
|
||||
}
|
||||
fun getUserByToken(token: String): User? {
|
||||
return transaction {
|
||||
val songConfig = SongConfig.find(SongConfigs.token eq token).firstOrNull()
|
||||
if(songConfig == null) null
|
||||
else UserService.getUser(songConfig.user.discord)
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePersonalLimit(user: User, limit: Int): SongConfig {
|
||||
return transaction {
|
||||
var songConfig = SongConfig.find(SongConfigs.user eq user.id).firstOrNull()
|
||||
if (songConfig == null) {
|
||||
songConfig = initConfig(user)
|
||||
}
|
||||
songConfig.personalLimit = limit
|
||||
songConfig
|
||||
}
|
||||
}
|
||||
fun updateQueueLimit(user: User, limit: Int): SongConfig {
|
||||
return transaction {
|
||||
var songConfig = SongConfig.find(SongConfigs.user eq user.id).firstOrNull()
|
||||
if (songConfig == null) {
|
||||
songConfig = initConfig(user)
|
||||
}
|
||||
songConfig.queueLimit = limit
|
||||
songConfig
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSession(user: User, token: String?, password: String?): SongConfig {
|
||||
return transaction {
|
||||
var songConfig = SongConfig.find(SongConfigs.user eq user.id).firstOrNull()
|
||||
if (songConfig == null) {
|
||||
songConfig = initConfig(user)
|
||||
}
|
||||
songConfig.token = token
|
||||
songConfig.password = password
|
||||
|
||||
songConfig
|
||||
}
|
||||
}
|
||||
|
||||
fun updateStreamerOnly(user: User, config: Boolean): SongConfig {
|
||||
return transaction {
|
||||
var songConfig = SongConfig.find(SongConfigs.user eq user.id).firstOrNull()
|
||||
if (songConfig == null) {
|
||||
songConfig = initConfig(user)
|
||||
}
|
||||
songConfig.streamerOnly = config
|
||||
|
||||
songConfig
|
||||
}
|
||||
}
|
||||
}
|
@ -129,7 +129,7 @@ fun getFollowDate(chatID: String, userId: String) : IData<IFollowContent?> {
|
||||
}
|
||||
|
||||
fun getStreamInfo(userId: String) : IData<IStreamInfo?> {
|
||||
val url = "https://api.chzzk.naver.com/service/v2/channels/${userId}/live-detail"
|
||||
val url = "https://api.chzzk.naver.com/service/v3/channels/${userId}/live-detail"
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
|
@ -0,0 +1,9 @@
|
||||
package space.mori.chzzk_bot.common.utils
|
||||
|
||||
fun getRandomString(length: Int): String {
|
||||
val charPool = ('a'..'z') + ('0'..'9')
|
||||
return (1..length)
|
||||
.map { kotlin.random.Random.nextInt(0, charPool.size) }
|
||||
.map(charPool::get)
|
||||
.joinToString("")
|
||||
}
|
@ -28,7 +28,6 @@ fun getYoutubeVideoId(url: String): String? {
|
||||
}
|
||||
|
||||
fun parseDuration(duration: String): Int {
|
||||
println(duration)
|
||||
val matchResult = durationRegex.find(duration)
|
||||
val (hours, minutes, seconds) = matchResult?.destructured ?: return 0
|
||||
|
||||
@ -67,8 +66,6 @@ fun getYoutubeVideo(url: String): YoutubeVideo? {
|
||||
|
||||
if (items == null || items.size() == 0) return null
|
||||
|
||||
println(json)
|
||||
|
||||
val item = items[0].asJsonObject
|
||||
val snippet = item.getAsJsonObject("snippet")
|
||||
val contentDetail = item.getAsJsonObject("contentDetails")
|
||||
|
@ -12,10 +12,7 @@ import io.ktor.server.plugins.swagger.*
|
||||
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.apiSongRoutes
|
||||
import space.mori.chzzk_bot.webserver.routes.wsSongRoutes
|
||||
import space.mori.chzzk_bot.webserver.routes.wsTimerRoutes
|
||||
import space.mori.chzzk_bot.webserver.routes.*
|
||||
import java.time.Duration
|
||||
|
||||
val server = embeddedServer(Netty, port = 8080) {
|
||||
@ -42,6 +39,7 @@ val server = embeddedServer(Netty, port = 8080) {
|
||||
apiSongRoutes()
|
||||
wsTimerRoutes()
|
||||
wsSongRoutes()
|
||||
wsSongListRoutes()
|
||||
swaggerUI("swagger-ui/index.html", "openapi/documentation.yaml") {
|
||||
options {
|
||||
version = "1.2.0"
|
||||
|
@ -0,0 +1,167 @@
|
||||
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.SongConfigService
|
||||
import space.mori.chzzk_bot.common.services.SongListService
|
||||
import space.mori.chzzk_bot.common.utils.getYoutubeVideo
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
fun Routing.wsSongListRoutes() {
|
||||
val sessions = ConcurrentHashMap<String, ConcurrentLinkedQueue<WebSocketServerSession>>()
|
||||
val status = ConcurrentHashMap<String, SongType>()
|
||||
val logger = LoggerFactory.getLogger(this.javaClass.name)
|
||||
|
||||
val dispatcher: CoroutinesEventBus by inject(CoroutinesEventBus::class.java)
|
||||
|
||||
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("/songlist/{sid}") {
|
||||
val sid = call.parameters["sid"]
|
||||
val pw = call.request.headers["X-Auth-Token"]
|
||||
val session = sid?.let { SongConfigService.getConfig(it) }
|
||||
val user = sid?.let {SongConfigService.getUserByToken(sid) }
|
||||
if (sid == null) {
|
||||
close(CloseReason(CloseReason.Codes.CANNOT_ACCEPT, "Invalid SID"))
|
||||
return@webSocket
|
||||
}
|
||||
if (user == null || session == null || session.password != pw) {
|
||||
close(CloseReason(CloseReason.Codes.CANNOT_ACCEPT, "Invalid SID"))
|
||||
return@webSocket
|
||||
}
|
||||
|
||||
addSession(sid, this)
|
||||
|
||||
if(status[sid] == SongType.STREAM_OFF) {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
sendSerialized(SongResponse(
|
||||
SongType.STREAM_OFF.value,
|
||||
user.token,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
for (frame in incoming) {
|
||||
when(frame) {
|
||||
is Frame.Text -> {
|
||||
val data = receiveDeserialized<SongRequest>()
|
||||
|
||||
if(data.maxQueue != null && data.maxQueue > 0) SongConfigService.updateQueueLimit(user, data.maxQueue)
|
||||
if(data.maxUserLimit != null && data.maxUserLimit > 0) SongConfigService.updatePersonalLimit(user, data.maxUserLimit)
|
||||
if(data.isStreamerOnly != null) SongConfigService.updateStreamerOnly(user, data.isStreamerOnly)
|
||||
|
||||
if(data.url != null) {
|
||||
val youtubeVideo = getYoutubeVideo(data.url)
|
||||
|
||||
dispatcher.post(
|
||||
SongEvent(
|
||||
user.token,
|
||||
SongType.ADD,
|
||||
user.token,
|
||||
user.username,
|
||||
youtubeVideo?.name,
|
||||
youtubeVideo?.author,
|
||||
youtubeVideo?.length
|
||||
)
|
||||
)
|
||||
}
|
||||
if(data.remove != null && data.remove > 0) {
|
||||
val songs = SongListService.getSong(user)
|
||||
if(songs.size < data.remove) {
|
||||
val song = songs[data.remove]
|
||||
SongListService.deleteSong(user, song.uid, song.name)
|
||||
|
||||
dispatcher.post(
|
||||
SongEvent(
|
||||
user.token,
|
||||
SongType.REMOVE,
|
||||
user.token,
|
||||
user.username,
|
||||
song.name,
|
||||
song.author,
|
||||
0
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is Frame.Ping -> send(Frame.Pong(frame.data))
|
||||
else -> {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(e: ClosedReceiveChannelException) {
|
||||
logger.error("Error in WebSocket: ${e.message}")
|
||||
} finally {
|
||||
removeSession(sid, this)
|
||||
}
|
||||
}
|
||||
|
||||
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.reqUid,
|
||||
it.name,
|
||||
it.author,
|
||||
it.time
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatcher.subscribe(TimerEvent::class) {
|
||||
if(it.type == TimerType.STREAM_OFF) {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
sessions[it.uid]?.forEach { ws ->
|
||||
ws.sendSerialized(SongResponse(
|
||||
it.type.value,
|
||||
it.uid,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SongRequest(
|
||||
val type: Int,
|
||||
val uid: String,
|
||||
val url: String?,
|
||||
val maxQueue: Int?,
|
||||
val maxUserLimit: Int?,
|
||||
val isStreamerOnly: Boolean?,
|
||||
val remove: Int?
|
||||
)
|
@ -17,7 +17,7 @@ import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
fun Routing.wsSongRoutes() {
|
||||
val sessions = ConcurrentHashMap<String, ConcurrentLinkedQueue<WebSocketServerSession>>()
|
||||
val status = ConcurrentHashMap<String, TimerType>()
|
||||
val status = ConcurrentHashMap<String, SongType>()
|
||||
val logger = LoggerFactory.getLogger(this.javaClass.name)
|
||||
|
||||
fun addSession(uid: String, session: WebSocketServerSession) {
|
||||
@ -45,7 +45,7 @@ fun Routing.wsSongRoutes() {
|
||||
|
||||
addSession(uid, this)
|
||||
|
||||
if(status[uid] == TimerType.STREAM_OFF) {
|
||||
if(status[uid] == SongType.STREAM_OFF) {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
sendSerialized(SongResponse(
|
||||
SongType.STREAM_OFF.value,
|
||||
@ -94,6 +94,22 @@ fun Routing.wsSongRoutes() {
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatcher.subscribe(TimerEvent::class) {
|
||||
if(it.type == TimerType.STREAM_OFF) {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
sessions[it.uid]?.forEach { ws ->
|
||||
ws.sendSerialized(SongResponse(
|
||||
it.type.value,
|
||||
it.uid,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
Loading…
x
Reference in New Issue
Block a user