add chzzk chat handler, command handler

This commit is contained in:
dalbodeule 2024-06-12 22:19:19 +09:00
parent 294bf04a50
commit 4b7fe25b21
No known key found for this signature in database
GPG Key ID: EFA860D069C9FA65
14 changed files with 299 additions and 16 deletions

3
.idea/sqldialects.xml generated
View File

@ -3,4 +3,7 @@
<component name="SqlDialectMappings">
<file url="PROJECT" dialect="MariaDB" />
</component>
<component name="SqlResolveMappings">
<file url="file://$PROJECT_DIR$/src/main/kotlin/space/mori/chzzk_bot/models/Command.kt" scope="{&quot;node&quot;:{ &quot;@negative&quot;:&quot;1&quot;, &quot;group&quot;:{ &quot;@kind&quot;:&quot;root&quot;, &quot;node&quot;:{ &quot;@negative&quot;:&quot;1&quot; } } }}" />
</component>
</project>

View File

@ -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

View File

@ -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)
}
}
}
}

View File

@ -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<String>) {
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<String>) {
discord.disable()
}
}
Runtime.getRuntime().addShutdownHook(Thread {
logger.info("Shutting down...")
chzzkHandler.disable()
discord.disable()
connector.dataSource.close()
})
}

View File

@ -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<UserHandler>()
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()
}
}

View File

@ -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 ?: "----"}")
}
}

View File

@ -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<String, () -> 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() }
}
}

View File

@ -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()

View File

@ -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()

View File

@ -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())
}
}
}

View File

@ -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<Int>) : IntEntity(id) {
companion object : IntEntityClass<Command>(Commands)
var user by User referencedOn Commands.user
var command by Commands.command
var content by Commands.content
}

View File

@ -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<Command> {
return transaction {
return@transaction Command.find(Commands.user eq user.id)
.toList()
}
}
}

View File

@ -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<User> {
return transaction {
return@transaction User.all().toList()
}
}
}

View File

@ -0,0 +1,27 @@
<configuration>
<!-- 콘솔에 출력하는 기본 로그 설정 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- HikariCP 로그 레벨 설정 -->
<logger name="com.zaxxer.hikari" level="INFO" />
<logger name="com.zaxxer.hikari.HikariConfig" level="WARN" />
<logger name="com.zaxxer.hikari.pool.PoolBase" level="WARN" />
<logger name="com.zaxxer.hikari.pool.HikariPool" level="WARN" />
<logger name="com.zaxxer.hikari.util.DriverDataSource" level="WARN" />
<!-- Exposed 로그 레벨 설정 -->
<logger name="org.jetbrains.exposed" level="INFO" />
<logger name="org.jetbrains.exposed.sql" level="WARN" />
<logger name="org.jetbrains.exposed.sql.transactions" level="WARN" />
<!-- 루트 로거 설정 -->
<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>
</configuration>