diff --git a/build.gradle.kts b/build.gradle.kts index 2397b3a..b250405 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,11 +27,6 @@ repositories { } dependencies { - // https://mvnrepository.com/artifact/net.dv8tion/JDA - implementation("net.dv8tion:JDA:5.0.1") { - exclude(module = "opus-java") - } - // https://mvnrepository.com/artifact/ch.qos.logback/logback-classic implementation("ch.qos.logback:logback-classic:1.5.6") @@ -46,6 +41,9 @@ dependencies { // https://mvnrepository.com/artifact/io.github.cdimascio/dotenv-kotlin implementation("io.github.cdimascio:dotenv-kotlin:6.4.1") + // https://mvnrepository.com/artifact/io.insert-koin/koin-core + implementation("io.insert-koin:koin-core:4.0.0-RC1") + kotlin("stdlib") listOf(project(":common"), project(":chatbot"), project(":webserver")).forEach { diff --git a/chatbot/build.gradle.kts b/chatbot/build.gradle.kts index 5c53230..70f889d 100644 --- a/chatbot/build.gradle.kts +++ b/chatbot/build.gradle.kts @@ -11,7 +11,7 @@ repositories { dependencies { // https://mvnrepository.com/artifact/net.dv8tion/JDA - implementation("net.dv8tion:JDA:5.0.1") { + api("net.dv8tion:JDA:5.0.1") { exclude(module = "opus-java") } @@ -35,6 +35,9 @@ dependencies { // https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp implementation("com.squareup.okhttp3:okhttp:4.12.0") + // https://mvnrepository.com/artifact/io.insert-koin/koin-core + implementation("io.insert-koin:koin-core:4.0.0-RC1") + testImplementation(kotlin("test")) listOf(project(":common")).forEach { diff --git a/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/chzzk/ChzzkHandler.kt b/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/chzzk/ChzzkHandler.kt index 4b996f9..745ab5d 100644 --- a/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/chzzk/ChzzkHandler.kt +++ b/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/chzzk/ChzzkHandler.kt @@ -1,5 +1,9 @@ package space.mori.chzzk_bot.chatbot.chzzk +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.slf4j.Logger import org.slf4j.LoggerFactory import space.mori.chzzk_bot.chatbot.chzzk.Connector.chzzk @@ -138,11 +142,14 @@ class UserHandler( logger.info("ChzzkChat connecting... ${channel.channelName} - ${channel.channelId}") listener.connectBlocking() - Discord.sendDiscord(user, status) streamStartTime = LocalDateTime.now() listener.sendChat("${user.username} 님의 방송이 감지되었습니다.") + CoroutineScope(Dispatchers.Default).launch { + delay(5000L) + Discord.sendDiscord(user, status) + } } else { logger.info("${user.username} is offline.") streamStartTime = null diff --git a/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/chzzk/MessageHandler.kt b/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/chzzk/MessageHandler.kt index 63f25c0..920a93f 100644 --- a/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/chzzk/MessageHandler.kt +++ b/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/chzzk/MessageHandler.kt @@ -1,6 +1,10 @@ package space.mori.chzzk_bot.chatbot.chzzk -import space.mori.chzzk_bot.common.events.EventDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.koin.java.KoinJavaComponent.inject +import space.mori.chzzk_bot.common.events.CoroutinesEventBus import space.mori.chzzk_bot.common.events.TimerEvent import space.mori.chzzk_bot.common.events.TimerType import space.mori.chzzk_bot.common.models.User @@ -30,6 +34,8 @@ class MessageHandler( private val logger = handler.logger private val listener = handler.listener + private val dispatcher: CoroutinesEventBus by inject(CoroutinesEventBus::class.java) + init { reloadCommand() } @@ -118,39 +124,53 @@ class MessageHandler( return } - val parts = msg.content.split("/", limit = 2) + val parts = msg.content.split(" ", limit = 2) if (parts.size < 2) { listener.sendChat("타이머 명령어 형식을 잘 찾아봐주세요!") return } - val command = parts[1] - val dispatcher = EventDispatcher - when(command) { + when (val command = parts[1]) { "업타임" -> { + logger.debug("${user.token} / 업타임") 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) + val hours = ChronoUnit.HOURS.between(streamOnTime, currentTime) + val minutes = ChronoUnit.MINUTES.between(streamOnTime?.plusHours(hours), currentTime) + val seconds = ChronoUnit.SECONDS.between(streamOnTime?.plusHours(hours)?.plusMinutes(minutes), currentTime) - dispatcher.dispatch(TimerEvent( - user.token, - TimerType.TIMER, - String.format("%02d:%02d:%02d", hours, minutes, seconds) - )) + CoroutineScope(Dispatchers.Default).launch { + dispatcher.post( + TimerEvent( + user.token, + TimerType.TIMER, + String.format("%02d:%02d:%02d", hours, minutes, seconds) + ) + ) + } + } + "삭제" -> { + logger.debug("${user.token} / 삭제") + CoroutineScope(Dispatchers.Default).launch { + dispatcher.post(TimerEvent(user.token, TimerType.REMOVE, "")) + } } - "삭제" -> dispatcher.dispatch(TimerEvent(user.token, TimerType.REMOVE, "")) else -> { + logger.debug("${user.token} / 그외") try { val time = command.toInt() val currentTime = LocalDateTime.now() - val timestamp = ChronoUnit.MINUTES.addTo(currentTime, time.toLong()) + val timestamp = currentTime.plus(time.toLong(), ChronoUnit.MINUTES) - dispatcher.dispatch(TimerEvent(user.token, TimerType.TIMER, timestamp.toString())) - } catch(_: Exception) { + CoroutineScope(Dispatchers.Default).launch { + dispatcher.post(TimerEvent(user.token, TimerType.TIMER, timestamp.toString())) + } + } catch (e: NumberFormatException) { listener.sendChat("!타이머/숫자 형식으로 적어주세요! 단위: 분") + } catch (e: Exception) { + listener.sendChat("타이머 설정 중 오류가 발생했습니다.") + logger.error("Error processing timer command: ${e.message}", e) } } } @@ -190,7 +210,7 @@ class MessageHandler( } // Replace followPattern - result = followPattern.replace(result) { matchResult -> + result = followPattern.replace(result) { _ -> try { val followingDate = getFollowDate(listener.chatId, msg.userId) .content.streamingProperty.following?.followDate diff --git a/common/src/main/kotlin/space/mori/chzzk_bot/common/events/EventBase.kt b/common/src/main/kotlin/space/mori/chzzk_bot/common/events/EventBase.kt index f847082..6ce9c2a 100644 --- a/common/src/main/kotlin/space/mori/chzzk_bot/common/events/EventBase.kt +++ b/common/src/main/kotlin/space/mori/chzzk_bot/common/events/EventBase.kt @@ -1,19 +1,32 @@ package space.mori.chzzk_bot.common.events +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.launch +import kotlin.reflect.KClass + interface Event -interface EventHandler { - fun handle(event: E) +interface EventBus { + suspend fun post(event: T) + fun subscribe(eventClass: KClass, listener: (T) -> Unit) } -object EventDispatcher { - private val handlers = mutableMapOf, MutableList>>() +class CoroutinesEventBus: EventBus { + private val _events = MutableSharedFlow() + val events: SharedFlow get() = _events - fun register(eventClass: Class, handler: EventHandler) { - handlers.computeIfAbsent(eventClass) { mutableListOf() }.add(handler) - } + override suspend fun post(event: T) = _events.emit(event) - fun dispatch(event: E) { - handlers[event::class.java]?.forEach { (it as EventHandler).handle(event) } + override fun subscribe(eventClass: KClass, listener: (T) -> Unit) { + CoroutineScope(Dispatchers.Default).launch { + events.filterIsInstance(eventClass) + .collect { + listener(it) + } + } } -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/space/mori/chzzk_bot/common/events/TimerEvents.kt b/common/src/main/kotlin/space/mori/chzzk_bot/common/events/TimerEvents.kt index 00533f0..0e23c9d 100644 --- a/common/src/main/kotlin/space/mori/chzzk_bot/common/events/TimerEvents.kt +++ b/common/src/main/kotlin/space/mori/chzzk_bot/common/events/TimerEvents.kt @@ -8,4 +8,6 @@ class TimerEvent( val uid: String, val type: TimerType, val time: String? -): Event \ No newline at end of file +): Event { + var TAG = javaClass.simpleName +} \ No newline at end of file diff --git a/src/main/kotlin/space/mori/chzzk_bot/Main.kt b/src/main/kotlin/space/mori/chzzk_bot/Main.kt index 8af78b0..e2c9cfb 100644 --- a/src/main/kotlin/space/mori/chzzk_bot/Main.kt +++ b/src/main/kotlin/space/mori/chzzk_bot/Main.kt @@ -3,12 +3,15 @@ package space.mori.chzzk_bot import io.github.cdimascio.dotenv.dotenv import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking +import org.koin.core.context.GlobalContext.startKoin +import org.koin.dsl.module import org.slf4j.Logger import org.slf4j.LoggerFactory import space.mori.chzzk_bot.chatbot.chzzk.ChzzkHandler import space.mori.chzzk_bot.chatbot.discord.Discord import space.mori.chzzk_bot.chatbot.chzzk.Connector as ChzzkConnector import space.mori.chzzk_bot.common.Connector +import space.mori.chzzk_bot.common.events.CoroutinesEventBus import space.mori.chzzk_bot.webserver.start import space.mori.chzzk_bot.webserver.stop import java.util.concurrent.TimeUnit @@ -26,6 +29,13 @@ val chzzkConnector = ChzzkConnector val chzzkHandler = ChzzkHandler fun main(args: Array) { + val dispatcher = module { + single { CoroutinesEventBus() } + } + startKoin { + modules(dispatcher) + } + discord.enable() chzzkHandler.enable() chzzkHandler.runStreamInfo() diff --git a/webserver/build.gradle.kts b/webserver/build.gradle.kts index ed4d4bb..ff34b6c 100644 --- a/webserver/build.gradle.kts +++ b/webserver/build.gradle.kts @@ -29,6 +29,9 @@ dependencies { // https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-reflect implementation("org.jetbrains.kotlin:kotlin-reflect:2.0.0") + // https://mvnrepository.com/artifact/io.insert-koin/koin-core + implementation("io.insert-koin:koin-core:4.0.0-RC1") + // https://mvnrepository.com/artifact/ch.qos.logback/logback-classic implementation("ch.qos.logback:logback-classic:1.5.6") diff --git a/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/WSTimerRoutes.kt b/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/WSTimerRoutes.kt index 832a1db..baa82bb 100644 --- a/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/WSTimerRoutes.kt +++ b/webserver/src/main/kotlin/space/mori/chzzk_bot/webserver/routes/WSTimerRoutes.kt @@ -6,15 +6,19 @@ import io.ktor.websocket.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.ClosedReceiveChannelException +import kotlinx.coroutines.flow.forEach +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.serialization.Serializable -import space.mori.chzzk_bot.common.events.Event +import org.koin.java.KoinJavaComponent.inject +import org.slf4j.LoggerFactory +import space.mori.chzzk_bot.common.events.* 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 +val logger = LoggerFactory.getLogger("WSTimerRoutes") + fun Routing.wsTimerRoutes() { val sessions = ConcurrentHashMap() @@ -51,29 +55,16 @@ fun Routing.wsTimerRoutes() { } } - run { - val dispatcher = EventDispatcher + val dispatcher: CoroutinesEventBus by inject(CoroutinesEventBus::class.java) - dispatcher.register(TimerEvent::class.java, object : EventHandler { - override fun handle(event: TimerEvent) { - CoroutineScope(Dispatchers.IO).launch { - sessions[event.uid]?.sendSerialized(TimerResponse(event.type, event.time ?: "")) - } - } - }) + dispatcher.subscribe(TimerEvent::class) { + logger.debug("TimerEvent: {} / {}", it.uid, it.type) + CoroutineScope(Dispatchers.Default).launch { + sessions[it.uid]?.sendSerialized(TimerResponse(it.type, it.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,