mirror of
https://github.com/dalbodeule/chibot-chzzk-bot.git
synced 2025-06-09 07:18:22 +00:00
commit
85ad7fe5ad
@ -12,6 +12,7 @@ import xyz.r2turntrue.chzzk4j.chat.ChzzkChat
|
||||
import xyz.r2turntrue.chzzk4j.types.channel.ChzzkChannel
|
||||
import java.lang.Exception
|
||||
import java.net.SocketTimeoutException
|
||||
import java.time.LocalDateTime
|
||||
|
||||
object ChzzkHandler {
|
||||
private val handlers = mutableListOf<UserHandler>()
|
||||
@ -19,7 +20,7 @@ object ChzzkHandler {
|
||||
@Volatile private var running: Boolean = false
|
||||
|
||||
fun addUser(chzzkChannel: ChzzkChannel, user: User) {
|
||||
handlers.add(UserHandler(chzzkChannel, logger, user))
|
||||
handlers.add(UserHandler(chzzkChannel, logger, user, streamStartTime = null))
|
||||
}
|
||||
|
||||
fun enable() {
|
||||
@ -82,18 +83,19 @@ object ChzzkHandler {
|
||||
|
||||
class UserHandler(
|
||||
val channel: ChzzkChannel,
|
||||
private val logger: Logger,
|
||||
val logger: Logger,
|
||||
private var user: User,
|
||||
private var _isActive: Boolean = false
|
||||
private var _isActive: Boolean = false,
|
||||
var streamStartTime: LocalDateTime?,
|
||||
) {
|
||||
private lateinit var messageHandler: MessageHandler
|
||||
|
||||
private var listener: ChzzkChat = chzzk.chat(channel.channelId)
|
||||
var listener: ChzzkChat = chzzk.chat(channel.channelId)
|
||||
.withAutoReconnect(true)
|
||||
.withChatListener(object : ChatEventListener {
|
||||
override fun onConnect(chat: ChzzkChat, isReconnecting: Boolean) {
|
||||
logger.info("ChzzkChat connected. ${channel.channelName} - ${channel.channelId} / reconnected: $isReconnecting")
|
||||
messageHandler = MessageHandler(channel, logger, chat)
|
||||
messageHandler = MessageHandler(this@UserHandler)
|
||||
}
|
||||
|
||||
override fun onError(ex: Exception) {
|
||||
@ -137,11 +139,13 @@ class UserHandler(
|
||||
listener.connectBlocking()
|
||||
|
||||
Discord.sendDiscord(user, status)
|
||||
streamStartTime = LocalDateTime.now()
|
||||
|
||||
listener.sendChat("${user.username} 님의 방송이 감지되었습니다.")
|
||||
|
||||
} else {
|
||||
logger.info("${user.username} is offline.")
|
||||
streamStartTime = null
|
||||
listener.closeAsync()
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +1,21 @@
|
||||
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.services.CommandService
|
||||
import space.mori.chzzk_bot.common.services.CounterService
|
||||
import space.mori.chzzk_bot.common.services.UserService
|
||||
import xyz.r2turntrue.chzzk4j.chat.ChatMessage
|
||||
import xyz.r2turntrue.chzzk4j.chat.ChzzkChat
|
||||
import xyz.r2turntrue.chzzk4j.types.channel.ChzzkChannel
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
|
||||
class MessageHandler(
|
||||
private val channel: ChzzkChannel,
|
||||
private val logger: Logger,
|
||||
private val listener: ChzzkChat
|
||||
private val handler: UserHandler
|
||||
) {
|
||||
private val commands = mutableMapOf<String, (msg: ChatMessage, user: User) -> Unit>()
|
||||
|
||||
@ -27,6 +26,10 @@ class MessageHandler(
|
||||
private val followPattern = Regex("<following>")
|
||||
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 {
|
||||
reloadCommand()
|
||||
}
|
||||
@ -109,6 +112,50 @@ class MessageHandler(
|
||||
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) {
|
||||
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
|
||||
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.engine.*
|
||||
@ -12,9 +13,18 @@ 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.wsTimerRoutes
|
||||
import java.time.Duration
|
||||
|
||||
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) {
|
||||
json(Json {
|
||||
prettyPrint = true
|
||||
@ -27,6 +37,7 @@ val server = embeddedServer(Netty, port = 8080) {
|
||||
}
|
||||
routing {
|
||||
apiRoutes()
|
||||
wsTimerRoutes()
|
||||
swaggerUI("swagger-ui/index.html", "openapi/documentation.yaml") {
|
||||
options {
|
||||
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