mirror of
https://github.com/dalbodeule/chibot-chzzk-bot.git
synced 2025-06-09 07:18:22 +00:00
add WebSocket timers
- EventDispatcher, TimerEvent add.
This commit is contained in:
parent
da13e8b834
commit
a9ee40e936
@ -12,6 +12,7 @@ import xyz.r2turntrue.chzzk4j.chat.ChzzkChat
|
|||||||
import xyz.r2turntrue.chzzk4j.types.channel.ChzzkChannel
|
import xyz.r2turntrue.chzzk4j.types.channel.ChzzkChannel
|
||||||
import java.lang.Exception
|
import java.lang.Exception
|
||||||
import java.net.SocketTimeoutException
|
import java.net.SocketTimeoutException
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
object ChzzkHandler {
|
object ChzzkHandler {
|
||||||
private val handlers = mutableListOf<UserHandler>()
|
private val handlers = mutableListOf<UserHandler>()
|
||||||
@ -19,7 +20,7 @@ object ChzzkHandler {
|
|||||||
@Volatile private var running: Boolean = false
|
@Volatile private var running: Boolean = false
|
||||||
|
|
||||||
fun addUser(chzzkChannel: ChzzkChannel, user: User) {
|
fun addUser(chzzkChannel: ChzzkChannel, user: User) {
|
||||||
handlers.add(UserHandler(chzzkChannel, logger, user))
|
handlers.add(UserHandler(chzzkChannel, logger, user, streamStartTime = null))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun enable() {
|
fun enable() {
|
||||||
@ -82,18 +83,19 @@ object ChzzkHandler {
|
|||||||
|
|
||||||
class UserHandler(
|
class UserHandler(
|
||||||
val channel: ChzzkChannel,
|
val channel: ChzzkChannel,
|
||||||
private val logger: Logger,
|
val logger: Logger,
|
||||||
private var user: User,
|
private var user: User,
|
||||||
private var _isActive: Boolean = false
|
private var _isActive: Boolean = false,
|
||||||
|
var streamStartTime: LocalDateTime?,
|
||||||
) {
|
) {
|
||||||
private lateinit var messageHandler: MessageHandler
|
private lateinit var messageHandler: MessageHandler
|
||||||
|
|
||||||
private var listener: ChzzkChat = chzzk.chat(channel.channelId)
|
var listener: ChzzkChat = chzzk.chat(channel.channelId)
|
||||||
.withAutoReconnect(true)
|
.withAutoReconnect(true)
|
||||||
.withChatListener(object : ChatEventListener {
|
.withChatListener(object : ChatEventListener {
|
||||||
override fun onConnect(chat: ChzzkChat, isReconnecting: Boolean) {
|
override fun onConnect(chat: ChzzkChat, isReconnecting: Boolean) {
|
||||||
logger.info("ChzzkChat connected. ${channel.channelName} - ${channel.channelId} / reconnected: $isReconnecting")
|
logger.info("ChzzkChat connected. ${channel.channelName} - ${channel.channelId} / reconnected: $isReconnecting")
|
||||||
messageHandler = MessageHandler(channel, logger, chat)
|
messageHandler = MessageHandler(this@UserHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onError(ex: Exception) {
|
override fun onError(ex: Exception) {
|
||||||
@ -137,11 +139,13 @@ class UserHandler(
|
|||||||
listener.connectBlocking()
|
listener.connectBlocking()
|
||||||
|
|
||||||
Discord.sendDiscord(user, status)
|
Discord.sendDiscord(user, status)
|
||||||
|
streamStartTime = LocalDateTime.now()
|
||||||
|
|
||||||
listener.sendChat("${user.username} 님의 방송이 감지되었습니다.")
|
listener.sendChat("${user.username} 님의 방송이 감지되었습니다.")
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
logger.info("${user.username} is offline.")
|
logger.info("${user.username} is offline.")
|
||||||
|
streamStartTime = null
|
||||||
listener.closeAsync()
|
listener.closeAsync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,21 @@
|
|||||||
package space.mori.chzzk_bot.chatbot.chzzk
|
package space.mori.chzzk_bot.chatbot.chzzk
|
||||||
|
|
||||||
import org.slf4j.Logger
|
import space.mori.chzzk_bot.common.events.EventDispatcher
|
||||||
|
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.CommandService
|
||||||
import space.mori.chzzk_bot.common.services.CounterService
|
import space.mori.chzzk_bot.common.services.CounterService
|
||||||
import space.mori.chzzk_bot.common.services.UserService
|
import space.mori.chzzk_bot.common.services.UserService
|
||||||
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 xyz.r2turntrue.chzzk4j.types.channel.ChzzkChannel
|
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
|
|
||||||
|
|
||||||
class MessageHandler(
|
class MessageHandler(
|
||||||
private val channel: ChzzkChannel,
|
private val handler: UserHandler
|
||||||
private val logger: Logger,
|
|
||||||
private val listener: ChzzkChat
|
|
||||||
) {
|
) {
|
||||||
private val commands = mutableMapOf<String, (msg: ChatMessage, user: User) -> Unit>()
|
private val commands = mutableMapOf<String, (msg: ChatMessage, user: User) -> Unit>()
|
||||||
|
|
||||||
@ -27,6 +26,10 @@ class MessageHandler(
|
|||||||
private val followPattern = Regex("<following>")
|
private val followPattern = Regex("<following>")
|
||||||
private val daysPattern = """<days:(\d{4})-(\d{2})-(\d{2})>""".toRegex()
|
private val daysPattern = """<days:(\d{4})-(\d{2})-(\d{2})>""".toRegex()
|
||||||
|
|
||||||
|
private val channel = handler.channel
|
||||||
|
private val logger = handler.logger
|
||||||
|
private val listener = handler.listener
|
||||||
|
|
||||||
init {
|
init {
|
||||||
reloadCommand()
|
reloadCommand()
|
||||||
}
|
}
|
||||||
@ -109,6 +112,50 @@ class MessageHandler(
|
|||||||
listener.sendChat("명령어 '$command' 삭제되었습니다.")
|
listener.sendChat("명령어 '$command' 삭제되었습니다.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun timerCommand(msg: ChatMessage, user: User) {
|
||||||
|
if (msg.profile?.userRoleCode == "common_user") {
|
||||||
|
listener.sendChat("매니저만 이 명령어를 사용할 수 있습니다.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val parts = msg.content.split("/", limit = 2)
|
||||||
|
if (parts.size < 2) {
|
||||||
|
listener.sendChat("타이머 명령어 형식을 잘 찾아봐주세요!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val command = parts[1]
|
||||||
|
val dispatcher = EventDispatcher
|
||||||
|
when(command) {
|
||||||
|
"업타임" -> {
|
||||||
|
val currentTime = LocalDateTime.now()
|
||||||
|
val streamOnTime = handler.streamStartTime
|
||||||
|
|
||||||
|
val hours = ChronoUnit.HOURS.between(currentTime, streamOnTime)
|
||||||
|
val minutes = ChronoUnit.MINUTES.between(currentTime.plusHours(hours), streamOnTime)
|
||||||
|
val seconds = ChronoUnit.MINUTES.between(currentTime.plusHours(hours).plusMinutes(minutes), streamOnTime)
|
||||||
|
|
||||||
|
dispatcher.dispatch(TimerEvent(
|
||||||
|
user.token,
|
||||||
|
TimerType.TIMER,
|
||||||
|
String.format("%02d:%02d:%02d", hours, minutes, seconds)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
"삭제" -> dispatcher.dispatch(TimerEvent(user.token, TimerType.REMOVE, ""))
|
||||||
|
else -> {
|
||||||
|
try {
|
||||||
|
val time = command.toInt()
|
||||||
|
val currentTime = LocalDateTime.now()
|
||||||
|
val timestamp = ChronoUnit.MINUTES.addTo(currentTime, time.toLong())
|
||||||
|
|
||||||
|
dispatcher.dispatch(TimerEvent(user.token, TimerType.TIMER, timestamp.toString()))
|
||||||
|
} catch(_: Exception) {
|
||||||
|
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,19 @@
|
|||||||
|
package space.mori.chzzk_bot.common.events
|
||||||
|
|
||||||
|
interface Event
|
||||||
|
|
||||||
|
interface EventHandler<E: Event> {
|
||||||
|
suspend fun handle(event: E)
|
||||||
|
}
|
||||||
|
|
||||||
|
object EventDispatcher {
|
||||||
|
private val handlers = mutableMapOf<Class<out Event>, MutableList<EventHandler<out Event>>>()
|
||||||
|
|
||||||
|
fun <E : Event> register(eventClass: Class<E>, handler: EventHandler<E>) {
|
||||||
|
handlers.computeIfAbsent(eventClass) { mutableListOf() }.add(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <E : Event> dispatch(event: E) {
|
||||||
|
handlers[event::class.java]?.forEach { (it as EventHandler<E>).handle(event) }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package space.mori.chzzk_bot.common.events
|
||||||
|
|
||||||
|
enum class TimerType {
|
||||||
|
UPTIME, TIMER, REMOVE
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimerEvent(
|
||||||
|
val uid: String,
|
||||||
|
val type: TimerType,
|
||||||
|
val time: String?
|
||||||
|
): Event
|
@ -1,6 +1,7 @@
|
|||||||
package space.mori.chzzk_bot.webserver
|
package space.mori.chzzk_bot.webserver
|
||||||
|
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
|
import io.ktor.serialization.kotlinx.*
|
||||||
import io.ktor.serialization.kotlinx.json.*
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.engine.*
|
import io.ktor.server.engine.*
|
||||||
@ -12,9 +13,18 @@ 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.wsTimerRoutes
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
val server = embeddedServer(Netty, port = 8080) {
|
val server = embeddedServer(Netty, port = 8080) {
|
||||||
install(WebSockets)
|
install(WebSockets) {
|
||||||
|
pingPeriod = Duration.ofSeconds(15)
|
||||||
|
timeout = Duration.ofSeconds(15)
|
||||||
|
maxFrameSize = Long.MAX_VALUE
|
||||||
|
masking = false
|
||||||
|
contentConverter = KotlinxWebsocketSerializationConverter(Json)
|
||||||
|
}
|
||||||
|
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
json(Json {
|
json(Json {
|
||||||
prettyPrint = true
|
prettyPrint = true
|
||||||
@ -27,6 +37,7 @@ val server = embeddedServer(Netty, port = 8080) {
|
|||||||
}
|
}
|
||||||
routing {
|
routing {
|
||||||
apiRoutes()
|
apiRoutes()
|
||||||
|
wsTimerRoutes()
|
||||||
swaggerUI("swagger-ui/index.html", "openapi/documentation.yaml") {
|
swaggerUI("swagger-ui/index.html", "openapi/documentation.yaml") {
|
||||||
options {
|
options {
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
@ -0,0 +1,76 @@
|
|||||||
|
package space.mori.chzzk_bot.webserver.routes
|
||||||
|
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
import io.ktor.server.websocket.*
|
||||||
|
import io.ktor.websocket.*
|
||||||
|
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import space.mori.chzzk_bot.common.events.Event
|
||||||
|
import space.mori.chzzk_bot.common.events.TimerType
|
||||||
|
import space.mori.chzzk_bot.common.services.UserService
|
||||||
|
import space.mori.chzzk_bot.common.events.EventDispatcher
|
||||||
|
import space.mori.chzzk_bot.common.events.EventHandler
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
fun Routing.wsTimerRoutes() {
|
||||||
|
val sessions = ConcurrentHashMap<String, WebSocketServerSession>()
|
||||||
|
|
||||||
|
webSocket("/timer/{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
|
||||||
|
}
|
||||||
|
|
||||||
|
sessions[uid] = this
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (frame in incoming) {
|
||||||
|
when(frame) {
|
||||||
|
is Frame.Text -> {
|
||||||
|
|
||||||
|
}
|
||||||
|
is Frame.Ping -> send(Frame.Pong(frame.data))
|
||||||
|
else -> {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(_: ClosedReceiveChannelException) {
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
sessions.remove(uid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run {
|
||||||
|
val dispatcher = EventDispatcher
|
||||||
|
|
||||||
|
dispatcher.register(TimerEvent::class.java, object : EventHandler<TimerEvent> {
|
||||||
|
override suspend fun handle(event: TimerEvent) {
|
||||||
|
sessions[event.uid]?.sendSerialized(TimerResponse(event.type, event.time ?: ""))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class TimerType {
|
||||||
|
UPTIME, TIMER
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimerEvent(
|
||||||
|
val uid: String,
|
||||||
|
val type: TimerType,
|
||||||
|
val time: String?
|
||||||
|
): Event
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TimerResponse(
|
||||||
|
val type: TimerType,
|
||||||
|
val time: String?
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user