add counter, counter handlers

This commit is contained in:
dalbodeule 2024-06-13 14:43:49 +09:00
parent 4da72f194e
commit 20f6d84040
No known key found for this signature in database
GPG Key ID: EFA860D069C9FA65
16 changed files with 285 additions and 41 deletions

View File

@ -66,9 +66,9 @@ dependencies {
// https://mvnrepository.com/artifact/org.jetbrains.exposed/exposed-dao
implementation("org.jetbrains.exposed:exposed-dao:0.51.1")
// https://mvnrepository.com/artifact/org.jetbrains.exposed/exposed-jdbc
runtimeOnly("org.jetbrains.exposed:exposed-jdbc:0.51.1")
implementation("org.jetbrains.exposed:exposed-jdbc:0.51.1")
// https://mvnrepository.com/artifact/org.jetbrains.exposed/exposed-kotlin-datetime
runtimeOnly("org.jetbrains.exposed:exposed-kotlin-datetime:0.51.1")
implementation("org.jetbrains.exposed:exposed-java-time:0.51.1")
// https://mvnrepository.com/artifact/com.zaxxer/HikariCP
implementation("com.zaxxer:HikariCP:5.1.0")

View File

@ -7,13 +7,12 @@ 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
import space.mori.chzzk_bot.models.*
object Connector {
private val dotenv = dotenv()
val hikariConfig = HikariConfig().apply {
private val hikariConfig = HikariConfig().apply {
jdbcUrl = dotenv["DB_URL"]
driverClassName = "org.mariadb.jdbc.Driver"
username = dotenv["DB_USER"]
@ -24,12 +23,10 @@ object Connector {
init {
Database.connect(dataSource)
val tables = listOf(Users, Commands)
val tables = listOf(Users, Commands, Counters, DailyCounters, PersonalCounters)
transaction {
tables.forEach { table ->
SchemaUtils.createMissingTablesAndColumns(table)
}
SchemaUtils.createMissingTablesAndColumns(* tables.toTypedArray())
}
}
}

View File

@ -9,6 +9,7 @@ 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
import kotlin.system.exitProcess
val dotenv = dotenv()
val logger: Logger = LoggerFactory.getLogger("main")
@ -26,7 +27,7 @@ fun main(args: Array<String>) {
if(dotenv.get("RUN_AGENT", "false").toBoolean()) {
runBlocking {
delay(TimeUnit.MINUTES.toMillis(1))
discord.disable()
exitProcess(0)
}
}

View File

@ -3,6 +3,7 @@ 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.models.User
import space.mori.chzzk_bot.services.UserService
import xyz.r2turntrue.chzzk4j.chat.ChatEventListener
import xyz.r2turntrue.chzzk4j.chat.ChatMessage
@ -14,13 +15,13 @@ 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 addUser(chzzkChannel: ChzzkChannel, user: User) {
handlers.add(UserHandler(chzzkChannel, logger, user))
}
internal fun enable() {
UserService.getAllUsers().map {
chzzk.getChannel(it.token)?.let { token -> addUser(token)}
chzzk.getChannel(it.token)?.let { token -> addUser(token, it)}
}
}
@ -31,7 +32,7 @@ object ChzzkHandler {
}
internal fun reloadCommand(chzzkChannel: ChzzkChannel) {
val handler = handlers.firstOrNull { it.channel == chzzkChannel }
val handler = handlers.firstOrNull { it.channel.channelId == chzzkChannel.channelId }
if (handler != null)
handler.reloadCommand()
else
@ -39,7 +40,9 @@ object ChzzkHandler {
}
}
class UserHandler(val channel: ChzzkChannel, private val logger: Logger) {
class UserHandler(
val channel: ChzzkChannel, private val logger: Logger, private val user: User
) {
private lateinit var messageHandler: MessageHandler
private var listener: ChzzkChat = chzzk.chat(channel.channelId)
@ -56,7 +59,7 @@ class UserHandler(val channel: ChzzkChannel, private val logger: Logger) {
}
override fun onChat(msg: ChatMessage) {
messageHandler.handle(msg)
messageHandler.handle(msg, user)
}
override fun onConnectionClosed(code: Int, reason: String?, remote: Boolean, tryingToReconnect: Boolean) {

View File

@ -1,7 +1,9 @@
package space.mori.chzzk_bot.chzzk
import org.slf4j.Logger
import space.mori.chzzk_bot.models.User
import space.mori.chzzk_bot.services.CommandService
import space.mori.chzzk_bot.services.CounterService
import space.mori.chzzk_bot.services.UserService
import xyz.r2turntrue.chzzk4j.chat.ChatMessage
import xyz.r2turntrue.chzzk4j.chat.ChzzkChat
@ -12,7 +14,12 @@ class MessageHandler(
private val logger: Logger,
private val listener: ChzzkChat
) {
private val commands = mutableMapOf<String, () -> Unit>()
private val commands = mutableMapOf<String, (msg: ChatMessage, user: User) -> Unit>()
private val counterPattern = Regex("<counter:([^>]+)>")
private val personalCounterPattern = Regex("<counter_personal:([^>]+)>")
private val dailyCounterPattern = Regex("<daily_counter:([^>]+)>")
private val namePattern = Regex("<name>")
init {
reloadCommand()
@ -24,16 +31,59 @@ class MessageHandler(
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)
this.commands.put(it.command.lowercase()) { msg, user ->
logger.debug("${channel.channelName} - ${it.command} - ${it.content}/${it.failContent}")
val result = replaceCounters(Pair(it.content, it.failContent), user, msg.userId, msg.profile?.nickname ?: "")
listener.sendChat(result)
}
}
}
internal fun handle(msg: ChatMessage) {
internal fun handle(msg: ChatMessage, user: User) {
val commandKey = msg.content.split(' ')[0]
commands[commandKey.lowercase()]?.let { it() }
commands[commandKey.lowercase()]?.let { it(msg, user) }
}
private fun replaceCounters(chat: Pair<String, String>, user: User, userId: String, userName: String): String {
var result = chat.first
var isFail = false
result = counterPattern.replace(result) {
val name = it.groupValues[1]
CounterService.updateCounterValue(name, 1, user).toString()
}
result = personalCounterPattern.replace(result) {
val name = it.groupValues[1]
CounterService.updatePersonalCounterValue(name, userId, 1, user).toString()
}
result = dailyCounterPattern.replace(result) {
val name = it.groupValues[1]
val dailyCounter = CounterService.getDailyCounterValue(name, userId, user)
return@replace if(dailyCounter.second)
CounterService.updateDailyCounterValue(name, userId, 1, user).first.toString()
else {
isFail = true
dailyCounter.first.toString()
}
}
if(isFail) {
result = chat.second
result = dailyCounterPattern.replace(result) {
val name = it.groupValues[1]
val dailyCounter = CounterService.getDailyCounterValue(name, userId, user)
dailyCounter.first.toString()
}
}
result = namePattern.replace(result, userName)
return result
}
}

View File

@ -20,10 +20,12 @@ object AddCommand : CommandInterface {
override val command = Commands.slash(name, "명령어를 추가합니다.")
.addOptions(OptionData(OptionType.STRING, "label", "작동할 명령어를 입력하세요.", true))
.addOptions(OptionData(OptionType.STRING, "content", "표시될 텍스트를 입력하세요.", true))
.addOptions(OptionData(OptionType.STRING, "fail_content", "카운터 업데이트 실패시 표시될 텍스트를 입력하세요.", false))
override fun run(event: SlashCommandInteractionEvent, bot: JDA) {
val label = event.getOption("label")?.asString
val content = event.getOption("content")?.asString
val failContent = event.getOption("fail_content")?.asString
if(label == null || content == null) {
event.hook.sendMessage("명령어와 텍스트는 필수 입력입니다.").queue()
@ -35,14 +37,21 @@ object AddCommand : CommandInterface {
event.hook.sendMessage("치지직 계정을 찾을 수 없습니다.").queue()
return
}
val commands = CommandService.getCommands(user)
if (commands.any { it.command == label }) {
event.hook.sendMessage("$label 명령어는 이미 있습니다! 업데이트 명령어를 써주세요.").queue()
return
}
val chzzkChannel = Connector.getChannel(user.token)
try {
CommandService.saveCommand(user, label, content)
CommandService.saveCommand(user, label, content, failContent ?: "")
try {
ChzzkHandler.reloadCommand(chzzkChannel!!)
} catch (_: Exception) {}
event.hook.sendMessage("등록이 완료되었습니다. $label = $content").queue()
event.hook.sendMessage("등록이 완료되었습니다. $label = $content/$failContent").queue()
} catch (e: Exception) {
event.hook.sendMessage("에러가 발생했습니다.").queue()
logger.debug(e.stackTraceToString())

View File

@ -40,8 +40,8 @@ object Register: CommandInterface {
}
try {
UserService.saveUser(chzzkChannel.channelName, chzzkChannel.channelId, event.user.idLong)
ChzzkHandler.addUser(chzzkChannel)
val user = UserService.saveUser(chzzkChannel.channelName, chzzkChannel.channelId, event.user.idLong)
ChzzkHandler.addUser(chzzkChannel, user)
event.hook.sendMessage("등록이 완료되었습니다. `${chzzkChannel.channelId}` - `${chzzkChannel.channelName}`")
} catch(e: Exception) {
event.hook.sendMessage("에러가 발생했습니다.").queue()

View File

@ -21,10 +21,12 @@ object UpdateCommand : CommandInterface {
override val command = Commands.slash(name, "명령어를 수정합니다.")
.addOptions(OptionData(OptionType.STRING, "label", "수정할 명령어를 입력하세요.", true))
.addOptions(OptionData(OptionType.STRING, "content", "표시될 텍스트를 입력하세요.", true))
.addOptions(OptionData(OptionType.STRING, "fail_content", "카운터 업데이트 실패시 표시될 텍스트를 입력하세요.", false))
override fun run(event: SlashCommandInteractionEvent, bot: JDA) {
val label = event.getOption("label")?.asString
val content = event.getOption("content")?.asString
val failContent = event.getOption("fail_content")?.asString
if(label == null || content == null) {
event.hook.sendMessage("명령어와 텍스트는 필수 입력입니다.").queue()
@ -39,10 +41,8 @@ object UpdateCommand : CommandInterface {
val chzzkChannel = Connector.getChannel(user.token)
try {
CommandService.updateCommand(user, label, content)
try {
ChzzkHandler.reloadCommand(chzzkChannel!!)
} catch (_: Exception) {}
CommandService.updateCommand(user, label, content, failContent ?: "")
chzzkChannel?.let { ChzzkHandler.reloadCommand(it) }
event.hook.sendMessage("등록이 완료되었습니다. $label = $content").queue()
} catch (e: Exception) {
event.hook.sendMessage("에러가 발생했습니다.").queue()

View File

@ -10,6 +10,7 @@ object Commands: IntIdTable("commands") {
val user = reference("user", Users, onDelete = ReferenceOption.CASCADE)
val command = varchar("command", 255)
val content = text("content")
val failContent = text("fail_content")
}
class Command(id: EntityID<Int>) : IntEntity(id) {
@ -18,4 +19,5 @@ class Command(id: EntityID<Int>) : IntEntity(id) {
var user by User referencedOn Commands.user
var command by Commands.command
var content by Commands.content
var failContent by Commands.failContent
}

View File

@ -0,0 +1,20 @@
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
object Counters: IntIdTable("counters") {
val name = varchar("name", 255)
val value = integer("value")
val user = reference("streamer", Users)
}
class Counter(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<Counter>(Counters)
var name by Counters.name
var value by Counters.value
var user by User referencedOn Counters.user
}

View File

@ -0,0 +1,25 @@
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.javatime.date
object DailyCounters: IntIdTable("daily_counters") {
val name = varchar("name", 255)
val userId = varchar("user_id", 64)
val value = integer("value")
val updatedAt = date("updated_at")
val user = reference("streamer", Users)
}
class DailyCounter(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<DailyCounter>(DailyCounters)
var name by DailyCounters.name
var userId by DailyCounters.userId
var value by DailyCounters.value
var updatedAt by DailyCounters.updatedAt
var user by User referencedOn DailyCounters.user
}

View File

@ -0,0 +1,22 @@
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
object PersonalCounters: IntIdTable("personal_counters") {
val name = varchar("name", 255)
val userId = varchar("user_id", 64)
val value = integer("value")
val user = reference("streamer", Users)
}
class PersonalCounter(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<PersonalCounter>(PersonalCounters)
var name by PersonalCounters.name
var userId by PersonalCounters.userId
var value by PersonalCounters.value
var user by User referencedOn PersonalCounters.user
}

View File

@ -9,12 +9,13 @@ 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 {
fun saveCommand(user: User, command: String, content: String, failContent: String): Command {
return transaction {
return@transaction Command.new {
this.user = user
this.command = command
this.content = content
this.failContent = failContent
}
}
}
@ -26,31 +27,32 @@ object CommandService {
commandRow ?: throw RuntimeException("Command not found! $command")
commandRow.delete()
return@transaction commandRow
commandRow
}
}
fun updateCommand(user: User, command: String, content: String): Command {
fun updateCommand(user: User, command: String, content: String, failContent: String): Command {
return transaction {
val updated = Commands.update({Commands.user eq user.id and(Commands.command eq command)}) {
it[Commands.content] = content
it[Commands.failContent] = failContent
}
if(updated == 0) throw RuntimeException("Command not found! $command")
return@transaction Command.find(Commands.user eq user.id and(Commands.command eq command)).first()
Command.find(Commands.user eq user.id and(Commands.command eq command)).first()
}
}
fun getCommand(id: Int): Command? {
return transaction {
return@transaction Command.findById(id)
Command.findById(id)
}
}
fun getCommands(user: User): List<Command> {
return transaction {
return@transaction Command.find(Commands.user eq user.id)
Command.find(Commands.user eq user.id)
.toList()
}
}

View File

@ -0,0 +1,105 @@
package space.mori.chzzk_bot.services
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.transactions.transaction
import space.mori.chzzk_bot.models.*
import java.time.LocalDate
object CounterService {
fun getCounterValue(name: String, user: User): Int {
return transaction {
Counter.find {
(Counters.name eq name) and (Counters.user eq user.id)
}.singleOrNull()?.value ?: 0
}
}
fun updateCounterValue(name: String, increment: Int, user: User): Int {
return transaction {
val counter = Counter.find {
(Counters.name eq name) and (Counters.user eq user.id) }.singleOrNull()
return@transaction if (counter != null) {
counter.value += increment
counter.value
} else {
val newCounter = Counter.new {
this.name = name
this.value = increment
this.user = user
}
newCounter.value
}
}
}
fun getPersonalCounterValue(name: String, userId: String, user: User): Int {
return transaction {
PersonalCounter.find {
(PersonalCounters.name eq name) and (PersonalCounters.userId eq userId) and (PersonalCounters.user eq user.id)
}.singleOrNull()?.value ?: 0
}
}
fun updatePersonalCounterValue(name: String, userId: String, increment: Int, user: User): Int {
return transaction {
val counter = PersonalCounter.find {
(PersonalCounters.name eq name) and (PersonalCounters.userId eq userId) and (PersonalCounters.user eq user.id)
}.singleOrNull()
return@transaction if (counter != null) {
counter.value += increment
counter.value
} else {
val newCounter = PersonalCounter.new {
this.name = name
this.value = increment
this.userId = userId
this.user = user
}
newCounter.value
}
}
}
fun getDailyCounterValue(name: String, userId: String, user: User): Pair<Int, Boolean> {
val today = LocalDate.now()
return transaction {
val counter = DailyCounter.find {
(DailyCounters.name eq name) and (DailyCounters.userId eq userId) and (DailyCounters.user eq user.id)
}.singleOrNull()
Pair(counter?.value ?: 0, counter?.updatedAt != today)
}
}
fun updateDailyCounterValue(name: String, userId: String, increment: Int, user: User): Pair<Int, Boolean> {
val today = LocalDate.now()
return transaction {
val counter = DailyCounter.find {
(DailyCounters.name eq name) and (DailyCounters.userId eq userId) and (DailyCounters.user eq user.id)
}.singleOrNull()
println("$counter")
if(counter == null) {
val newCounter = DailyCounter.new {
this.name = name
this.value = increment
this.userId = userId
this.updatedAt = today
this.user = user
}
return@transaction Pair(newCounter.value, true)
}
return@transaction if(counter.updatedAt == today)
Pair(counter.value, false)
else {
counter.value += increment
Pair(counter.value, true)
}
}
}
}

View File

@ -8,7 +8,7 @@ import space.mori.chzzk_bot.models.Users
object UserService {
fun saveUser(username: String, token: String, discordID: Long): User {
return transaction {
return@transaction User.new {
User.new {
this.username = username
this.token = token
this.discord = discordID
@ -18,7 +18,7 @@ object UserService {
fun getUser(id: Int): User? {
return transaction {
return@transaction User.findById(id)
User.findById(id)
}
}
@ -26,7 +26,7 @@ object UserService {
return transaction {
val users = User.find(Users.discord eq discordID)
return@transaction users.firstOrNull()
users.firstOrNull()
}
}
@ -34,13 +34,13 @@ object UserService {
return transaction {
val users = User.find(Users.token eq chzzkID)
return@transaction users.firstOrNull()
users.firstOrNull()
}
}
fun getAllUsers(): List<User> {
return transaction {
return@transaction User.all().toList()
User.all().toList()
}
}
}

View File

@ -380,6 +380,10 @@
"name":"kotlinx.coroutines.CancellableContinuationImpl",
"fields":[{"name":"_decisionAndIndex$volatile"}, {"name":"_parentHandle$volatile"}, {"name":"_state$volatile"}]
},
{
"name":"kotlinx.coroutines.CompletedExceptionally",
"fields":[{"name":"_handled$volatile"}]
},
{
"name":"kotlinx.coroutines.EventLoopImplBase",
"fields":[{"name":"_delayed$volatile"}, {"name":"_isCompleted$volatile"}, {"name":"_queue$volatile"}]
@ -388,6 +392,10 @@
"name":"kotlinx.coroutines.JobSupport",
"fields":[{"name":"_parentHandle$volatile"}, {"name":"_state$volatile"}]
},
{
"name":"kotlinx.coroutines.JobSupport$Finishing",
"fields":[{"name":"_exceptionsHolder$volatile"}, {"name":"_isCompleting$volatile"}, {"name":"_rootCause$volatile"}]
},
{
"name":"kotlinx.coroutines.internal.DispatchedContinuation",
"fields":[{"name":"_reusableCancellableContinuation$volatile"}]