diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index 63772a3..257aa19 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -3,4 +3,7 @@ + + + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 90c8074..6e27add 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -58,6 +58,7 @@ dependencies { } // https://mvnrepository.com/artifact/io.github.R2turnTrue/chzzk4j implementation("io.github.R2turnTrue:chzzk4j:0.0.7") + implementation("ch.qos.logback:logback-classic:1.4.14") // https://mvnrepository.com/artifact/org.jetbrains.exposed/exposed-core diff --git a/src/main/kotlin/space/mori/chzzk_bot/Connector.kt b/src/main/kotlin/space/mori/chzzk_bot/Connector.kt index 970b3e0..b5b9f0e 100644 --- a/src/main/kotlin/space/mori/chzzk_bot/Connector.kt +++ b/src/main/kotlin/space/mori/chzzk_bot/Connector.kt @@ -6,6 +6,8 @@ import io.github.cdimascio.dotenv.dotenv import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.SchemaUtils import org.jetbrains.exposed.sql.transactions.transaction +import org.slf4j.LoggerFactory +import space.mori.chzzk_bot.models.Commands import space.mori.chzzk_bot.models.Users object Connector { @@ -22,9 +24,12 @@ object Connector { init { Database.connect(dataSource) + val tables = listOf(Users, Commands) transaction { - SchemaUtils.createMissingTablesAndColumns(Users) + tables.forEach { table -> + SchemaUtils.createMissingTablesAndColumns(table) + } } } } \ 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 da597a0..1aa0223 100644 --- a/src/main/kotlin/space/mori/chzzk_bot/Main.kt +++ b/src/main/kotlin/space/mori/chzzk_bot/Main.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import org.slf4j.Logger import org.slf4j.LoggerFactory +import space.mori.chzzk_bot.chzzk.ChzzkHandler import space.mori.chzzk_bot.discord.Discord import space.mori.chzzk_bot.chzzk.Connector as ChzzkConnector import java.util.concurrent.TimeUnit @@ -15,10 +16,12 @@ val logger: Logger = LoggerFactory.getLogger("main") fun main(args: Array) { val discord = Discord() - Connector - ChzzkConnector + val connector = Connector + val chzzkConnector = ChzzkConnector + val chzzkHandler = ChzzkHandler discord.enable() + chzzkHandler.enable() if(dotenv.get("RUN_AGENT", "false").toBoolean()) { runBlocking { @@ -26,4 +29,11 @@ fun main(args: Array) { discord.disable() } } + + Runtime.getRuntime().addShutdownHook(Thread { + logger.info("Shutting down...") + chzzkHandler.disable() + discord.disable() + connector.dataSource.close() + }) } \ No newline at end of file diff --git a/src/main/kotlin/space/mori/chzzk_bot/chzzk/ChzzkHandler.kt b/src/main/kotlin/space/mori/chzzk_bot/chzzk/ChzzkHandler.kt new file mode 100644 index 0000000..783cb2c --- /dev/null +++ b/src/main/kotlin/space/mori/chzzk_bot/chzzk/ChzzkHandler.kt @@ -0,0 +1,69 @@ +package space.mori.chzzk_bot.chzzk + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import space.mori.chzzk_bot.chzzk.Connector.chzzk +import space.mori.chzzk_bot.services.UserService +import xyz.r2turntrue.chzzk4j.chat.ChatEventListener +import xyz.r2turntrue.chzzk4j.chat.ChatMessage +import xyz.r2turntrue.chzzk4j.chat.ChzzkChat +import xyz.r2turntrue.chzzk4j.types.channel.ChzzkChannel +import java.lang.Exception + +object ChzzkHandler { + private val handlers = mutableListOf() + private val logger = LoggerFactory.getLogger(this::class.java) + + internal fun addUser(chzzkChannel: ChzzkChannel) { + handlers.add(UserHandler(chzzkChannel, logger)) + } + + internal fun enable() { + UserService.getAllUsers().map { + chzzk.getChannel(it.token)?.let { token -> addUser(token)} + } + } + + internal fun disable() { + handlers.forEach { handler -> + handler.disable() + } + } +} + +class UserHandler(private val channel: ChzzkChannel, private val logger: Logger) { + private lateinit var messageHandler: MessageHandler + + private 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) + } + + override fun onError(ex: Exception) { + logger.info("ChzzkChat error. ${channel.channelName} - ${channel.channelId}") + logger.debug(ex.stackTraceToString()) + } + + override fun onChat(msg: ChatMessage) { + messageHandler.handle(msg) + } + + override fun onConnectionClosed(code: Int, reason: String?, remote: Boolean, tryingToReconnect: Boolean) { + logger.info("ChzzkChat closed. ${channel.channelName} - ${channel.channelId}") + logger.info("Reason: $reason / $tryingToReconnect") + } + }) + .build() + + init { + logger.info("ChzzkChat connecting... ${channel.channelName} - ${channel.channelId}") + listener.connectBlocking() + } + + internal fun disable() { + listener.closeBlocking() + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/mori/chzzk_bot/chzzk/Connector.kt b/src/main/kotlin/space/mori/chzzk_bot/chzzk/Connector.kt index 84b54a2..d7a8633 100644 --- a/src/main/kotlin/space/mori/chzzk_bot/chzzk/Connector.kt +++ b/src/main/kotlin/space/mori/chzzk_bot/chzzk/Connector.kt @@ -1,14 +1,21 @@ package space.mori.chzzk_bot.chzzk import io.github.cdimascio.dotenv.dotenv +import org.slf4j.LoggerFactory import xyz.r2turntrue.chzzk4j.Chzzk import xyz.r2turntrue.chzzk4j.ChzzkBuilder +import xyz.r2turntrue.chzzk4j.types.channel.ChzzkChannel object Connector { private val dotenv = dotenv() val chzzk: Chzzk = ChzzkBuilder() .withAuthorization(dotenv["NID_AUT"], dotenv["NID_SES"]) .build() + private val logger = LoggerFactory.getLogger(this::class.java) - fun getChannel(channelId: String) = chzzk.getChannel(channelId) + fun getChannel(channelId: String): ChzzkChannel? = chzzk.getChannel(channelId) + + init { + logger.info("chzzk logged: ${chzzk.isLoggedIn} / ${chzzk.loggedUser?.nickname ?: "----"}") + } } \ No newline at end of file diff --git a/src/main/kotlin/space/mori/chzzk_bot/chzzk/MessageHandler.kt b/src/main/kotlin/space/mori/chzzk_bot/chzzk/MessageHandler.kt new file mode 100644 index 0000000..bb4261c --- /dev/null +++ b/src/main/kotlin/space/mori/chzzk_bot/chzzk/MessageHandler.kt @@ -0,0 +1,34 @@ +package space.mori.chzzk_bot.chzzk + +import org.slf4j.Logger +import space.mori.chzzk_bot.services.CommandService +import space.mori.chzzk_bot.services.UserService +import xyz.r2turntrue.chzzk4j.chat.ChatMessage +import xyz.r2turntrue.chzzk4j.chat.ChzzkChat +import xyz.r2turntrue.chzzk4j.types.channel.ChzzkChannel +class MessageHandler( + private val channel: ChzzkChannel, + private val logger: Logger, + private val listener: ChzzkChat +) { + private val commands = mutableMapOf Unit>() + + init { + val user = UserService.getUser(channel.channelId) + ?: throw RuntimeException("User not found. it's bug? ${channel.channelName} - ${channel.channelId}") + val commands = CommandService.getCommands(user) + + commands.map { + this.commands.put(it.command.lowercase()) { + logger.debug("${channel.channelName} - ${it.command} - ${it.content}") + listener.sendChat(it.content) + } + } + } + + internal fun handle(msg: ChatMessage) { + val commandKey = msg.content.split(' ')[0] + + commands[commandKey.lowercase()]?.let { it() } + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/mori/chzzk_bot/discord/Discord.kt b/src/main/kotlin/space/mori/chzzk_bot/discord/Discord.kt index 5780f59..4964876 100644 --- a/src/main/kotlin/space/mori/chzzk_bot/discord/Discord.kt +++ b/src/main/kotlin/space/mori/chzzk_bot/discord/Discord.kt @@ -6,13 +6,13 @@ import net.dv8tion.jda.api.entities.Activity import net.dv8tion.jda.api.entities.Guild import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent import net.dv8tion.jda.api.hooks.ListenerAdapter +import org.slf4j.LoggerFactory import space.mori.chzzk_bot.dotenv -import space.mori.chzzk_bot.logger -import kotlin.concurrent.thread class Discord: ListenerAdapter() { private lateinit var bot: JDA private var guild: Guild? = null + private val logger = LoggerFactory.getLogger(this::class.java) private val commands = getCommands() diff --git a/src/main/kotlin/space/mori/chzzk_bot/discord/commands/Ping.kt b/src/main/kotlin/space/mori/chzzk_bot/discord/commands/Ping.kt index 81b7c43..9b217e6 100644 --- a/src/main/kotlin/space/mori/chzzk_bot/discord/commands/Ping.kt +++ b/src/main/kotlin/space/mori/chzzk_bot/discord/commands/Ping.kt @@ -3,13 +3,15 @@ package space.mori.chzzk_bot.discord.commands import net.dv8tion.jda.api.JDA import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent import net.dv8tion.jda.api.interactions.commands.build.Commands +import org.slf4j.LoggerFactory import space.mori.chzzk_bot.discord.Command import space.mori.chzzk_bot.discord.CommandInterface @Command() object Ping: CommandInterface { - override val command = Commands.slash("ping", "봇이 살아있을까요?") + private val logger = LoggerFactory.getLogger(this::class.java) override val name = "ping" + override val command = Commands.slash(name, "봇이 살아있을까요?") override fun run(event: SlashCommandInteractionEvent, bot: JDA) { event.hook.sendMessage("${event.user.asMention} Pong!").queue() diff --git a/src/main/kotlin/space/mori/chzzk_bot/discord/commands/Register.kt b/src/main/kotlin/space/mori/chzzk_bot/discord/commands/Register.kt new file mode 100644 index 0000000..8e23a86 --- /dev/null +++ b/src/main/kotlin/space/mori/chzzk_bot/discord/commands/Register.kt @@ -0,0 +1,51 @@ +package space.mori.chzzk_bot.discord.commands + +import net.dv8tion.jda.api.JDA +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent +import net.dv8tion.jda.api.interactions.commands.OptionType +import net.dv8tion.jda.api.interactions.commands.build.Commands +import net.dv8tion.jda.api.interactions.commands.build.OptionData +import org.slf4j.LoggerFactory +import space.mori.chzzk_bot.chzzk.ChzzkHandler +import space.mori.chzzk_bot.chzzk.Connector +import space.mori.chzzk_bot.discord.Command +import space.mori.chzzk_bot.discord.CommandInterface +import space.mori.chzzk_bot.services.UserService + +@Command +object Register: CommandInterface { + private val logger = LoggerFactory.getLogger(this::class.java) + override val name = "register" + override val command = Commands.slash(name, "치지직 계정을 등록합니다.") + .addOptions( + OptionData( + OptionType.STRING, + "chzzk_id", + "36da10b7c35800f298e9c565a396bafd 형식으로 입력해주세요.", + true + ) + ) + + override fun run(event: SlashCommandInteractionEvent, bot: JDA) { + val chzzkID = event.getOption("chzzk_id")?.asString + if(chzzkID == null) { + event.hook.sendMessage("치지직 계정은 필수 입력입니다.").queue() + return + } + + val chzzkChannel = Connector.getChannel(chzzkID) + if (chzzkChannel == null) { + event.hook.sendMessage("치지직 계정을 찾을 수 없습니다.").queue() + return + } + + try { + UserService.saveUser(chzzkChannel.channelName, chzzkChannel.channelId, event.user.idLong) + ChzzkHandler.addUser(chzzkChannel) + event.hook.sendMessage("등록이 완료되었습니다. ${chzzkChannel.channelId} - ${chzzkChannel.channelName}") + } catch(e: Exception) { + event.hook.sendMessage("에러가 발생했습니다.").queue() + logger.debug(e.stackTraceToString()) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/mori/chzzk_bot/models/Command.kt b/src/main/kotlin/space/mori/chzzk_bot/models/Command.kt new file mode 100644 index 0000000..fde8fa0 --- /dev/null +++ b/src/main/kotlin/space/mori/chzzk_bot/models/Command.kt @@ -0,0 +1,21 @@ +package space.mori.chzzk_bot.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 Commands: IntIdTable("commands") { + val user = reference("user", Users, onDelete = ReferenceOption.CASCADE) + val command = varchar("command", 255) + val content = text("content") +} + +class Command(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(Commands) + + var user by User referencedOn Commands.user + var command by Commands.command + var content by Commands.content +} \ No newline at end of file diff --git a/src/main/kotlin/space/mori/chzzk_bot/services/CommandService.kt b/src/main/kotlin/space/mori/chzzk_bot/services/CommandService.kt new file mode 100644 index 0000000..0e3b4ea --- /dev/null +++ b/src/main/kotlin/space/mori/chzzk_bot/services/CommandService.kt @@ -0,0 +1,32 @@ +package space.mori.chzzk_bot.services + +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.transactions.transaction +import space.mori.chzzk_bot.models.Command +import space.mori.chzzk_bot.models.Commands +import space.mori.chzzk_bot.models.User + +object CommandService { + fun saveCommand(user: User, command: String, content: String): Command { + return transaction { + return@transaction Command.new { + this.user = user + this.command = command + this.content = content + } + } + } + + fun getCommand(id: Int): Command? { + return transaction { + return@transaction Command.findById(id) + } + } + + fun getCommands(user: User): List { + return transaction { + return@transaction Command.find(Commands.user eq user.id) + .toList() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/mori/chzzk_bot/services/UserService.kt b/src/main/kotlin/space/mori/chzzk_bot/services/UserService.kt index ac92abb..e062523 100644 --- a/src/main/kotlin/space/mori/chzzk_bot/services/UserService.kt +++ b/src/main/kotlin/space/mori/chzzk_bot/services/UserService.kt @@ -1,25 +1,46 @@ package space.mori.chzzk_bot.services import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.transactions.transaction import space.mori.chzzk_bot.models.User import space.mori.chzzk_bot.models.Users -class UserService { - fun saveUser(user: User) { - User.new { - username = user.username - token = user.token - discord = user.discord +object UserService { + fun saveUser(username: String, token: String, discordID: Long): User { + return transaction { + return@transaction User.new { + this.username = username + this.token = token + this.discord = discordID + } } } fun getUser(id: Int): User? { - return User.findById(id) + return transaction { + return@transaction User.findById(id) + } } fun getUser(discordID: Long): User? { - val users = User.find(Users.discord eq discordID) + return transaction { + val users = User.find(Users.discord eq discordID) - return users.firstOrNull() + return@transaction users.firstOrNull() + } + } + + fun getUser(chzzkID: String): User? { + return transaction { + val users = User.find(Users.token eq chzzkID) + + return@transaction users.firstOrNull() + } + } + + fun getAllUsers(): List { + return transaction { + return@transaction User.all().toList() + } } } \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..366d123 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,27 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file