29 Commits
0.1.4 ... 1.0.3

Author SHA1 Message Date
JinU Choi
1d23ac5121 Merge pull request #10 from dalbodeule/develop
BUG FIX: fix #9
2024-07-29 16:01:30 +09:00
dalbodeule
8f8d2f895a BUG FIX: fix #9 2024-07-29 15:58:50 +09:00
JinU Choi
47b4b8252f Merge pull request #8 from dalbodeule/develop
BUG FIX: daily counter with update updated_at column
2024-07-27 21:09:48 +09:00
dalbodeule
64880318e8 BUG FIX: daily counter with update updated_at column 2024-07-27 21:06:56 +09:00
JinU Choi
cec07f9859 Merge pull request #7 from dalbodeule/develop
add guild update, update document, update version to 1.0.1
2024-07-27 11:38:26 +09:00
dalbodeule
18ee27e567 add guild update, update document, update version to 1.0.1 2024-07-27 11:36:17 +09:00
JinU Choi
34ec0f29ff Merge pull request #6 from dalbodeule/develop
v1.0.0

- add managers (in discord)
- manager can add/remove/modify commands
- update some packages.
2024-07-27 10:23:50 +09:00
dalbodeule
2b999c5acb dockerfile update 2024-07-27 10:20:32 +09:00
dalbodeule
c669943f87 manager can add/update/delete commands 2024-07-27 10:13:33 +09:00
dalbodeule
efaaf5ae9f update library versions 2024-07-27 09:54:22 +09:00
JinU Choi
c8fca6995c Merge pull request #5 from dalbodeule/develop
Add discord managers.
2024-07-26 20:28:46 +09:00
dalbodeule
903f1f7f38 add Manager add/remove/list command, database tables. 2024-07-26 20:24:37 +09:00
dalbodeule
e4b6f819ca some improve MessageHandler.kt 2024-07-26 19:20:10 +09:00
JinU Choi
2c1f42e4af Merge pull request #4 from dalbodeule/develop
add days placeholder
2024-07-23 17:47:44 +09:00
dalbodeule
ff90e5fe7f add days placeholder
- days placeholder <days:yyyy-mm-dd> pattern added.
2024-07-23 17:44:28 +09:00
JinU Choi
a227dda930 Merge pull request #3 from dalbodeule/develop
fix README.md (with commands, placeholders, stacks)
2024-07-22 10:53:24 +09:00
dalbodeule
3e09989202 fix README.md (with commands, placeholders, stacks) 2024-07-22 10:45:52 +09:00
dalbodeule
eddca6dbab fix README.md 2024-07-19 13:30:45 +09:00
dalbodeule
9c48dd676e remove github ci/cd 2024-07-18 23:17:32 +09:00
dalbodeule
779604d21e update README.md 2024-07-18 23:00:36 +09:00
dalbodeule
83791fe59b remove useless prints. 2024-07-15 10:16:12 +09:00
dalbodeule
fe63a4af15 chzzk followdate fix 2024-07-14 19:47:12 +09:00
dalbodeule
7522ec6f9e chzzk followdate error handler add 2024-07-14 18:37:52 +09:00
dalbodeule
c35b3082cc log debug to info. level changed 2024-07-05 14:06:31 +09:00
dalbodeule
19d3f23cd7 chzzk livestream status handler fix 2024-07-03 22:33:25 +09:00
dalbodeule
80d777dad5 chzzkChat only connected on live 2024-07-02 10:04:23 +09:00
dalbodeule
d2071b323e register command in chat some fix (4x) 2024-06-29 21:58:50 +09:00
dalbodeule
f2b30c8b00 register command in chat some fix (3x) 2024-06-29 21:44:18 +09:00
dalbodeule
947e6d4bb3 register command in chat some fix (2x) 2024-06-29 21:38:19 +09:00
24 changed files with 523 additions and 168 deletions

View File

@@ -1,53 +0,0 @@
name: build
# event trigger: main 브랜치에 push 이벤트 발생 시 jobs가 실행된다.
on:
push:
branches: [ "main" ]
# 권한 설정
permissions:
contents: read
# jobs 정의
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up JDK 21
uses: graalvm/setup-graalvm@v1
with:
java-version: '21'
distribution: 'graalvm'
- name: Build with Gradle
run: ./gradlew build
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set datetime variable
id: vars
run: echo "DATETIME=$(date +'%Y%m%d%H%M')" >> $GITHUB_ENV
- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/chzzkbot:${{ env.DATETIME }}
${{ secrets.DOCKER_USERNAME }}/chzzkbot:latest

9
.idea/dataSources.xml generated
View File

@@ -1,14 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="@localhost" uuid="90f8ee11-600e-4155-a316-e8062c7c828b">
<driver-ref>mysql.8</driver-ref>
<data-source source="LOCAL" name="@prod" uuid="90f8ee11-600e-4155-a316-e8062c7c828b">
<driver-ref>mariadb</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3306</jdbc-url>
<jdbc-driver>org.mariadb.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mariadb://localhost:3306</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.resource.type" value="Deployment" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>

View File

@@ -1,23 +1,10 @@
# Stage 1: Build the JAR file
FROM gradle:jdk21 as build
WORKDIR /app
# Copy the Gradle files and source code
COPY build.gradle.kts settings.gradle.kts gradlew gradle.properties ./
COPY gradle gradle
COPY src src
# Build the project using Gradle
RUN ./gradlew build
# Stage 2: Run the JAR file using JDK 21
# Use a base image with JDK 21 for the final image
FROM openjdk:21-jdk
WORKDIR /app
# Copy the JAR file from the build stage
COPY --from=build /app/build/libs/*.jar app.jar
# Copy the JAR file from the TeamCity build artifacts
COPY build/libs/chzzk_bot-*.jar app.jar
# Set the entry point
ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@@ -1,3 +1,72 @@
# maybe_chzzk_bot_kotlin
# nabot_chzzk_bot
[discord](https://discord.gg/up8ANZegmy)
[![Discord](https://img.shields.io/discord/1250093195870867577)](https://discord.gg/up8ANZegmy)&nbsp; &nbsp;[![Build Status](https://teamcity.mori.space/app/rest/builds/buildType:NabotChzzkBot_Build/statusIcon)](https://teamcity.mori.space/project/NabotChzzkBot)&nbsp; &nbsp;[![Docker Image Version](https://img.shields.io/docker/v/dalbodeule/chzzkbot)](https://hub.docker.com/repository/docker/dalbodeule/chzzkbot/general)
## Chzzk Chatbot with [JDA5](https://github.com/discord-jda/JDA), [chzzk4j](https://github.com/R2turnTrue/chzzk4j)
네이버 게임스트리밍 플랫폼 치지직의 챗봇입니다.
## 지원 기능
### Placeholders
- [x] \<name>
- [x] \<following>
- [x] \<counter:counter_name>
- [x] \<personal_counter:counter_name>
- [x] \<daily_counter:counter_name>
- [x] \<days:yyyy-mm-dd>
### 관리 명령어 (on Discord)
- [x] /register chzzk_id: \[치지직 고유ID]
- [x] /alert channel: \[디스코드 Channel ID] content: \[알림 내용]
- [x] /add label: \[명령어] content: \[내용]
- [ ] /list
- [x] /update label: \[명령어] content: \[내용]
- [x] /delete label: \[명령어]
### 매니저 명령어 (on Discord)
- [x] /addmanager user: \[Discord user]
- [x] /listmanager
- [x] /removemanager user: \[Discord user]
### 관리 명령어 (on Chzzk chat)
- [x] !명령어추가 \[명령어] \[내용]
- [x] !명령어수정 \[명령어] \[내용]
- [x] !명령어삭제 \[명령어]
### Envs
- DISCORD_TOKEN
- DB_URL
- DB_USER
- DB_PASS
- RUN_AGENT = `false`
- NID_AUT
- NID_SES
### 사용 예시
- 팔로우
- `/add label: !팔로우 content: <name>님은 오늘로 <following>일째 팔로우네요!`
- 출첵
- `/add label: !출첵 content: <name>님의 <daily_counter:attendance>번째 출석! fail_content: <name>님은 오늘 이미 출석했어요! <daily_counter:attendance>번 했네요?`
- `/add label: ? content: <name>님이 <counter:hook>개째 갈고리 수집`
- ㄱㅇㅇ
- `/add label: ㄱㅇㅇ content: <counter:cute>번째 ㄱㅇㅇ`
- `/add label: ㄱㅇㅇ content: 나누 귀여움 +<counter:cute>`
-
- `/add label: 풉 content: <counter:poop>번째 비웃음?`
- `/add label: 풉키풉키 content: <counter:poop>번째 비웃음?`
- 바보
- `/add label: 바보 content: 나 바보 아니다?`
- `/add label: 바보 content: <counter:fool> 번째 바보? 나 바보 아니다?`
- 첫방송
- `/add label: 첫방송 content: 24년 7월 23일부터 <days:2024-07-23>일 째 방송중!`
## 사용 기술스택
- [Exposed](https://github.com/JetBrains/Exposed)
- [Kotlin](https://github.com/JetBrains/kotlin)
- [JDA5](https://github.com/discord-jda/JDA)
- [chzzk4j](https://github.com/R2turnTrue/chzzk4j)
- [HikariCP](https://github.com/brettwooldridge/HikariCP)
- [gson](https://github.com/google/gson)
- [mariadb](https://mariadb.org/)
- [docker](https://www.docker.com/)
- [Teamcity](https://www.jetbrains.com/teamcity/)

View File

@@ -29,22 +29,23 @@ repositories {
dependencies {
// https://mvnrepository.com/artifact/net.dv8tion/JDA
implementation("net.dv8tion:JDA:5.0.0-beta.24") {
implementation("net.dv8tion:JDA:5.0.1") {
exclude(module = "opus-java")
}
// https://mvnrepository.com/artifact/io.github.R2turnTrue/chzzk4j
implementation("io.github.R2turnTrue:chzzk4j:0.0.8")
implementation("io.github.R2turnTrue:chzzk4j:0.0.9")
implementation("ch.qos.logback:logback-classic:1.4.14")
// https://mvnrepository.com/artifact/ch.qos.logback/logback-classic
implementation("ch.qos.logback:logback-classic:1.5.6")
// https://mvnrepository.com/artifact/org.jetbrains.exposed/exposed-core
implementation("org.jetbrains.exposed:exposed-core:0.51.1")
implementation("org.jetbrains.exposed:exposed-core:0.52.0")
// https://mvnrepository.com/artifact/org.jetbrains.exposed/exposed-dao
implementation("org.jetbrains.exposed:exposed-dao:0.51.1")
implementation("org.jetbrains.exposed:exposed-dao:0.52.0")
// https://mvnrepository.com/artifact/org.jetbrains.exposed/exposed-jdbc
implementation("org.jetbrains.exposed:exposed-jdbc:0.51.1")
implementation("org.jetbrains.exposed:exposed-jdbc:0.52.0")
// https://mvnrepository.com/artifact/org.jetbrains.exposed/exposed-kotlin-datetime
implementation("org.jetbrains.exposed:exposed-java-time:0.51.1")
implementation("org.jetbrains.exposed:exposed-java-time:0.52.0")
// https://mvnrepository.com/artifact/com.zaxxer/HikariCP
implementation("com.zaxxer:HikariCP:5.1.0")
@@ -54,11 +55,13 @@ dependencies {
// https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-reflect
implementation("org.jetbrains.kotlin:kotlin-reflect:2.0.0")
// https://mvnrepository.com/artifact/com.google.code.gson/gson
implementation("com.google.code.gson:gson:2.11.0")
// https://mvnrepository.com/artifact/org.mariadb.jdbc/mariadb-java-client
implementation("org.mariadb.jdbc:mariadb-java-client:3.4.0")
implementation("org.mariadb.jdbc:mariadb-java-client:3.4.1")
// https://mvnrepository.com/artifact/io.github.cdimascio/dotenv-kotlin
implementation("io.github.cdimascio:dotenv-kotlin:6.4.1")
kotlin("stdlib")
@@ -77,4 +80,8 @@ tasks.withType<Jar> {
})
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
tasks.named<JavaExec>("run") {
systemProperty("logback.configurationFile", "logback-debug.xml")
}

View File

@@ -1,6 +1,6 @@
kotlin.code.style=official
group = space.mori
version = 1.0.0
version = 1.0.3
org.gradle.jvmargs=-Dfile.encoding=UTF-8
org.gradle.console=plain

View File

@@ -19,7 +19,7 @@ object Connector {
init {
Database.connect(dataSource)
val tables = listOf(Users, Commands, Counters, DailyCounters, PersonalCounters)
val tables = listOf(Users, Commands, Counters, DailyCounters, PersonalCounters, Managers)
transaction {
SchemaUtils.createMissingTablesAndColumns(* tables.toTypedArray())

View File

@@ -21,12 +21,17 @@ data class IFollowContent(
val badge: Badge? = null,
val title: Title? = null,
val verifiedMark: Boolean = false,
val activityBadges: List<String> = emptyList(),
val activityBadges: List<Badge> = emptyList(),
val streamingProperty: StreamingProperty = StreamingProperty()
)
data class Badge(
val imageUrl: String = ""
val badgeNo: Int?,
val badgeId: String?,
val imageUrl: String?,
val title: String?,
val description: String?,
val activated: Boolean?
)
data class Title(
@@ -117,6 +122,7 @@ fun getFollowDate(chatID: String, userId: String) : IData<IFollowContent> {
return follow
} catch(e: Exception) {
println(e.stackTrace)
throw e
}
}

View File

@@ -62,7 +62,7 @@ object ChzzkHandler {
try {
val streamInfo = getStreamInfo(it.channel.channelId)
if (streamInfo.content.status == "OPEN" && !it.isActive) it.isActive(true, streamInfo)
if (streamInfo.content.status == "CLOSED" && it.isActive) it.isActive(false, streamInfo)
if (streamInfo.content.status == "CLOSE" && it.isActive) it.isActive(false, streamInfo)
} catch(e: SocketTimeoutException) {
logger.info("Timeout: ${it.channel.channelName} / ${e.stackTraceToString()}")
} catch (e: Exception) {
@@ -116,11 +116,6 @@ class UserHandler(
})
.build()
init {
logger.info("ChzzkChat connecting... ${channel.channelName} - ${channel.channelId}")
listener.connectAsync()
}
internal fun disable() {
listener.closeAsync()
}
@@ -140,6 +135,10 @@ class UserHandler(
_isActive = value
if(value) {
logger.info("${user.username} is live.")
logger.info("ChzzkChat connecting... ${channel.channelName} - ${channel.channelId}")
listener.connectAsync()
if(user.liveAlertMessage != "" && user.liveAlertGuild != null && user.liveAlertChannel != null) {
val channel = discord.getChannel(user.liveAlertGuild!!, user.liveAlertChannel!!) ?: throw RuntimeException("${user.liveAlertChannel} is not valid.")
@@ -164,8 +163,7 @@ class UserHandler(
}
} else {
logger.info("${user.username} is offline.")
listener.sendChat("${user.username} 님! 방송 수고하셨습니다.")
listener.closeAsync()
}
}
}

View File

@@ -1,7 +1,6 @@
package space.mori.chzzk_bot.chzzk
import org.slf4j.Logger
import space.mori.chzzk_bot.models.Command
import space.mori.chzzk_bot.models.User
import space.mori.chzzk_bot.services.CommandService
import space.mori.chzzk_bot.services.CounterService
@@ -9,11 +8,9 @@ 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
import java.text.SimpleDateFormat
import java.time.LocalDate
import java.time.Period
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.*
import java.time.temporal.ChronoUnit
class MessageHandler(
@@ -28,6 +25,7 @@ class MessageHandler(
private val dailyCounterPattern = Regex("<daily_counter:([^>]+)>")
private val namePattern = Regex("<name>")
private val followPattern = Regex("<following>")
private val daysPattern = """<days:(\d{4})-(\d{2})-(\d{2})>""".toRegex()
init {
reloadCommand()
@@ -54,7 +52,7 @@ class MessageHandler(
}
private fun manageAddCommand(msg: ChatMessage, user: User) {
if (msg.profile?.userRoleCode != "streaming_channel_manager" && msg.profile?.userRoleCode != "streamer") {
if (msg.profile?.userRoleCode == "common_user") {
listener.sendChat("매니저만 명령어를 추가할 수 있습니다.")
return
}
@@ -64,7 +62,7 @@ class MessageHandler(
listener.sendChat("명령어 추가 형식은 '!명령어추가 명령어 내용'입니다.")
return
}
if (commands.containsKey(parts[0])) {
if (commands.containsKey(parts[1])) {
listener.sendChat("${parts[1]} 명령어는 이미 있는 명령어입니다.")
return
}
@@ -75,7 +73,7 @@ class MessageHandler(
}
private fun manageUpdateCommand(msg: ChatMessage, user: User) {
if (msg.profile?.userRoleCode != "streaming_channel_manager" && msg.profile?.userRoleCode != "streamer") {
if (msg.profile?.userRoleCode == "common_user") {
listener.sendChat("매니저만 명령어를 추가할 수 있습니다.")
return
}
@@ -85,7 +83,7 @@ class MessageHandler(
listener.sendChat("명령어 수정 형식은 '!명령어수정 명령어 내용'입니다.")
return
}
if (!commands.containsKey(parts[0])) {
if (!commands.containsKey(parts[1])) {
listener.sendChat("${parts[1]} 명령어는 없는 명령어입니다.")
return
}
@@ -96,7 +94,7 @@ class MessageHandler(
}
private fun manageRemoveCommand(msg: ChatMessage, user: User) {
if (msg.profile?.userRoleCode != "streaming_channel_manager" && msg.profile?.userRoleCode != "streamer") {
if (msg.profile?.userRoleCode == "common_user") {
listener.sendChat("매니저만 명령어를 삭제할 수 있습니다.")
return
}
@@ -121,58 +119,75 @@ class MessageHandler(
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, msg.userId, 1, user).toString()
}
result = dailyCounterPattern.replace(result) {
val name = it.groupValues[1]
// Replace dailyCounterPattern
result = dailyCounterPattern.replace(result) { matchResult ->
val name = matchResult.groupValues[1]
val dailyCounter = CounterService.getDailyCounterValue(name, msg.userId, user)
return@replace if(dailyCounter.second)
if (dailyCounter.second) {
CounterService.updateDailyCounterValue(name, msg.userId, 1, user).first.toString()
else {
} else {
isFail = true
dailyCounter.first.toString()
}
}
if(isFail && chat.second != "") {
// Handle fail case
if (isFail && chat.second.isNotEmpty()) {
result = chat.second
result = dailyCounterPattern.replace(result) {
val name = it.groupValues[1]
result = dailyCounterPattern.replace(result) { matchResult ->
val name = matchResult.groupValues[1]
val dailyCounter = CounterService.getDailyCounterValue(name, msg.userId, user)
dailyCounter.first.toString()
}
}
result = followPattern.replace(result) {
val following = getFollowDate(listener.chatId, msg.userId)
val dateString: String = following.content.streamingProperty.following?.followDate ?: SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(
Date()
)
val today = LocalDate.now()
// Replace followPattern
result = followPattern.replace(result) { matchResult ->
try {
val followingDate = getFollowDate(listener.chatId, msg.userId)
.content.streamingProperty.following?.followDate
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
// 문자열을 LocalDate 객체로 변환
val pastDate = LocalDate.parse(dateString, formatter)
val period = followingDate?.let {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
val pastDate = LocalDateTime.parse(it, formatter)
val today = LocalDateTime.now()
ChronoUnit.DAYS.between(pastDate, today)
} ?: 0
val period = Period.between(pastDate, today)
period.days.toString()
}
if(isFail) {
return chat.second
period.toString()
} catch (e: Exception) {
logger.error(e.message)
"0"
}
}
// Replace daysPattern
result = daysPattern.replace(result) { matchResult ->
val (year, month, day) = matchResult.destructured
val pastDate = LocalDateTime.of(year.toInt(), month.toInt(), day.toInt(), 0, 0, 0)
val today = LocalDateTime.now()
val daysBetween = ChronoUnit.DAYS.between(pastDate, today)
daysBetween.toString()
}
// Replace counterPattern
result = counterPattern.replace(result) { matchResult ->
val name = matchResult.groupValues[1]
CounterService.updateCounterValue(name, 1, user).toString()
}
// Replace personalCounterPattern
result = personalCounterPattern.replace(result) { matchResult ->
val name = matchResult.groupValues[1]
CounterService.updatePersonalCounterValue(name, msg.userId, 1, user).toString()
}
// Replace namePattern
result = namePattern.replace(result, userName)
return result
}
}

View File

@@ -4,11 +4,14 @@ import net.dv8tion.jda.api.JDA
import net.dv8tion.jda.api.JDABuilder
import net.dv8tion.jda.api.entities.Activity
import net.dv8tion.jda.api.entities.Guild
import net.dv8tion.jda.api.events.guild.GuildJoinEvent
import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
import net.dv8tion.jda.api.hooks.ListenerAdapter
import space.mori.chzzk_bot.dotenv
import org.slf4j.LoggerFactory
import space.mori.chzzk_bot.discord.commands.*
import space.mori.chzzk_bot.services.ManagerService
class Discord: ListenerAdapter() {
private lateinit var bot: JDA
@@ -22,6 +25,9 @@ class Discord: ListenerAdapter() {
RegisterCommand,
RemoveCommand,
UpdateCommand,
AddManagerCommand,
ListManagerCommand,
RemoveManagerCommand,
)
override fun onSlashCommandInteraction(event: SlashCommandInteractionEvent) {
@@ -31,6 +37,30 @@ class Discord: ListenerAdapter() {
handler?.run(event, bot)
}
override fun onGuildMemberRemove(event: GuildMemberRemoveEvent) {
event.member?.let { ManagerService.deleteManager(event.guild.idLong, it.idLong) }
}
override fun onGuildJoin(event: GuildJoinEvent) {
commandUpdate(event.guild)
}
private fun commandUpdate(guild: Guild) {
guild.updateCommands().addCommands(* commands.map { it.command}.toTypedArray())
.onSuccess {
logger.info("Command update on guild success!")
}
.queue()
}
private fun commandUpdate(bot: JDA) {
bot.updateCommands().addCommands(* commands.map { it.command}.toTypedArray())
.onSuccess {
logger.info("Command update bot boot success!")
}
.queue()
}
internal fun enable() {
val thread = Thread {
try {
@@ -41,18 +71,9 @@ class Discord: ListenerAdapter() {
guild = bot.getGuildById(dotenv["GUILD_ID"])
bot.updateCommands()
.addCommands(* commands.map { it.command }.toTypedArray())
.onSuccess {
logger.info("Command update success!")
logger.debug("Command list: ${commands.joinToString("/ ") { it.name }}")
}
.queue()
if (guild == null) {
logger.info("No guild found!")
this.disable()
commandUpdate(bot)
bot.guilds.forEach {
commandUpdate(it)
}
} catch (e: Exception) {
logger.info("Could not enable Discord!")

View File

@@ -10,6 +10,7 @@ import space.mori.chzzk_bot.chzzk.ChzzkHandler
import space.mori.chzzk_bot.chzzk.Connector
import space.mori.chzzk_bot.discord.CommandInterface
import space.mori.chzzk_bot.services.CommandService
import space.mori.chzzk_bot.services.ManagerService
import space.mori.chzzk_bot.services.UserService
object AddCommand : CommandInterface {
@@ -30,13 +31,19 @@ object AddCommand : CommandInterface {
return
}
val user = UserService.getUser(event.user.idLong)
if(user == null) {
event.hook.sendMessage("치지직 계정을 찾을 수 없습니다.").queue()
var user = UserService.getUser(event.user.idLong)
val manager = event.guild?.idLong?.let { ManagerService.getUser(it, event.user.idLong) }
if(user == null && manager == null) {
event.hook.sendMessage("당신은 이 명령어를 사용할 수 없습니다.").queue()
return
}
val commands = CommandService.getCommands(user)
if (manager != null) {
user = manager.user
ManagerService.updateManager(user, event.user.idLong, event.user.effectiveName)
}
val commands = CommandService.getCommands(user!!)
if (commands.any { it.command == label }) {
event.hook.sendMessage("$label 명령어는 이미 있습니다! 업데이트 명령어를 써주세요.").queue()
return

View File

@@ -0,0 +1,47 @@
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.discord.CommandInterface
import space.mori.chzzk_bot.services.ManagerService
import space.mori.chzzk_bot.services.UserService
object AddManagerCommand : CommandInterface {
private val logger = LoggerFactory.getLogger(this::class.java)
override val name: String = "addmanager"
override val command = Commands.slash(name, "매니저를 추가합니다.")
.addOptions(OptionData(OptionType.USER, "user", "추가할 유저를 선택하세요.", true))
override fun run(event: SlashCommandInteractionEvent, bot: JDA) {
val manager = event.getOption("user")?.asUser
if(manager == null) {
event.hook.sendMessage("유저는 필수사항입니다.").queue()
return
}
if(manager.idLong == event.user.idLong) {
event.hook.sendMessage("자신은 매니저로 설정할 수 없습니다.").queue()
return
}
val user = UserService.getUser(event.user.idLong)
if(user == null) {
event.hook.sendMessage("치지직 계정을 찾을 수 없습니다.").queue()
return
}
try {
ManagerService.saveManager(user, manager.idLong, manager.effectiveName)
if(user.liveAlertGuild == null)
UserService.updateLiveAlert(user.id.value, event.guild!!.idLong, event.channelIdLong, "")
event.hook.sendMessage("등록이 완료되었습니다. ${manager.effectiveName}").queue()
} catch (e: Exception) {
event.hook.sendMessage("에러가 발생했습니다.").queue()
logger.debug(e.stackTraceToString())
}
}
}

View File

@@ -9,6 +9,7 @@ import org.slf4j.LoggerFactory
import space.mori.chzzk_bot.chzzk.ChzzkHandler
import space.mori.chzzk_bot.chzzk.Connector
import space.mori.chzzk_bot.discord.CommandInterface
import space.mori.chzzk_bot.services.ManagerService
import space.mori.chzzk_bot.services.UserService
object AlertCommand : CommandInterface {
@@ -22,13 +23,19 @@ object AlertCommand : CommandInterface {
val channel = event.getOption("channel")?.asChannel
val content = event.getOption("content")?.asString
val user = UserService.getUser(event.user.idLong)
if(user == null) {
event.hook.sendMessage("치지직 계정을 찾을 수 없습니다.").queue()
var user = UserService.getUser(event.user.idLong)
val manager = event.guild?.idLong?.let { ManagerService.getUser(it, event.user.idLong) }
if(user == null && manager == null) {
event.hook.sendMessage("당신은 이 명령어를 사용할 수 없습니다.").queue()
return
}
val chzzkChannel = Connector.getChannel(user.token)
if (manager != null) {
user = manager.user
ManagerService.updateManager(user, event.user.idLong, event.user.effectiveName)
}
val chzzkChannel = Connector.getChannel(user!!.token)
try {
val newUser = UserService.updateLiveAlert(user.id.value, channel?.guild?.idLong ?: 0L, channel?.idLong ?: 0L, content ?: "")

View File

@@ -0,0 +1,44 @@
package space.mori.chzzk_bot.discord.commands
import net.dv8tion.jda.api.EmbedBuilder
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.CommandInterface
import space.mori.chzzk_bot.services.ManagerService
import space.mori.chzzk_bot.services.UserService
object ListManagerCommand : CommandInterface {
private val logger = LoggerFactory.getLogger(this::class.java)
override val name: String = "listmanager"
override val command = Commands.slash(name, "매니저 목록을 확인합니다.")
override fun run(event: SlashCommandInteractionEvent, bot: JDA) {
try {
val managers = event.guild?.idLong?.let { ManagerService.getAllUsers(it) }
if(managers == null) {
event.channel.sendMessage("여기에서는 사용할 수 없습니다.")
return
}
val user = UserService.getUserWithGuildId(event.guild!!.idLong)
val embed = EmbedBuilder()
embed.setTitle("${user!!.username} 매니저 목록")
embed.setDescription("매니저 목록입니다.")
var idx = 1
managers.forEach {
embed.addField("${idx++}", it.lastUserName ?: it.managerId.toString(), true)
}
event.channel.sendMessageEmbeds(embed.build()).queue()
} catch (e: Exception) {
event.hook.sendMessage("에러가 발생했습니다.").queue()
logger.debug(e.stackTraceToString())
}
}
}

View File

@@ -10,6 +10,7 @@ import space.mori.chzzk_bot.chzzk.ChzzkHandler
import space.mori.chzzk_bot.chzzk.Connector
import space.mori.chzzk_bot.discord.CommandInterface
import space.mori.chzzk_bot.services.CommandService
import space.mori.chzzk_bot.services.ManagerService
import space.mori.chzzk_bot.services.UserService
object RemoveCommand : CommandInterface {
@@ -26,12 +27,19 @@ object RemoveCommand : CommandInterface {
return
}
val user = UserService.getUser(event.user.idLong)
if(user == null) {
event.hook.sendMessage("치지직 계정을 찾을 수 없습니다.").queue()
var user = UserService.getUser(event.user.idLong)
val manager = event.guild?.idLong?.let { ManagerService.getUser(it, event.user.idLong) }
if(user == null && manager == null) {
event.hook.sendMessage("당신은 이 명령어를 사용할 수 없습니다.").queue()
return
}
val chzzkChannel = Connector.getChannel(user.token)
if (manager != null) {
user = manager.user
ManagerService.updateManager(user, event.user.idLong, event.user.effectiveName)
}
val chzzkChannel = Connector.getChannel(user!!.token)
try {
CommandService.removeCommand(user, label)

View File

@@ -0,0 +1,49 @@
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.discord.CommandInterface
import space.mori.chzzk_bot.services.ManagerService
import space.mori.chzzk_bot.services.UserService
object RemoveManagerCommand : CommandInterface {
private val logger = LoggerFactory.getLogger(this::class.java)
override val name: String = "removemanager"
override val command = Commands.slash(name, "매니저를 삭제합니다.")
.addOptions(OptionData(OptionType.USER, "user", "삭제할 유저를 선택하세요.", true))
override fun run(event: SlashCommandInteractionEvent, bot: JDA) {
val manager = event.getOption("user")?.asUser
if(manager == null) {
event.hook.sendMessage("유저는 필수사항입니다.").queue()
return
}
if(manager.idLong == event.user.idLong) {
event.hook.sendMessage("자신은 매니저로 설정할 수 없습니다.").queue()
return
}
val user = UserService.getUser(event.user.idLong)
if(user == null) {
event.hook.sendMessage("치지직 계정을 찾을 수 없습니다.").queue()
return
}
if(ManagerService.getUser(user.liveAlertGuild ?: 0L, manager.idLong) == null) {
event.hook.sendMessage("${manager.name}은 매니저가 아닙니다.")
}
try {
ManagerService.deleteManager(user, manager.idLong)
event.hook.sendMessage("삭제가 완료되었습니다. ${manager.effectiveName}").queue()
} catch (e: Exception) {
event.hook.sendMessage("에러가 발생했습니다.").queue()
logger.debug(e.stackTraceToString())
}
}
}

View File

@@ -10,6 +10,7 @@ import space.mori.chzzk_bot.chzzk.ChzzkHandler
import space.mori.chzzk_bot.chzzk.Connector
import space.mori.chzzk_bot.discord.CommandInterface
import space.mori.chzzk_bot.services.CommandService
import space.mori.chzzk_bot.services.ManagerService
import space.mori.chzzk_bot.services.UserService
object UpdateCommand : CommandInterface {
@@ -30,12 +31,19 @@ object UpdateCommand : CommandInterface {
return
}
val user = UserService.getUser(event.user.idLong)
if(user == null) {
event.hook.sendMessage("치지직 계정을 찾을 수 없습니다.").queue()
var user = UserService.getUser(event.user.idLong)
val manager = event.guild?.idLong?.let { ManagerService.getUser(it, event.user.idLong) }
if(user == null && manager == null) {
event.hook.sendMessage("당신은 이 명령어를 사용할 수 없습니다.").queue()
return
}
val chzzkChannel = Connector.getChannel(user.token)
if (manager != null) {
user = manager.user
ManagerService.updateManager(user, event.user.idLong, event.user.effectiveName)
}
val chzzkChannel = Connector.getChannel(user!!.token)
try {
CommandService.updateCommand(user, label, content, failContent ?: "")

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 Managers: IntIdTable("manager") {
val user = reference("user", Users)
val managerId = long("manager_id")
val discordGuildId = long("discord_guild_id")
var lastUserName = varchar("last_user_name", 255).nullable()
}
class Manager(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<Manager>(Managers)
var user by User referencedOn Managers.user
var managerId by Managers.managerId
var discordGuildId by Managers.discordGuildId
var lastUserName by Managers.lastUserName
}

View File

@@ -81,8 +81,6 @@ object CounterService {
(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
@@ -98,6 +96,7 @@ object CounterService {
Pair(counter.value, false)
else {
counter.value += increment
counter.updatedAt = today
Pair(counter.value, true)
}
}

View File

@@ -0,0 +1,70 @@
package space.mori.chzzk_bot.services
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.transactions.transaction
import space.mori.chzzk_bot.models.Manager
import space.mori.chzzk_bot.models.Managers
import space.mori.chzzk_bot.models.User
object ManagerService {
fun saveManager(user: User, discordId: Long, name: String): Manager {
if (user.liveAlertGuild == null)
throw RuntimeException("${user.username} has no liveAlertGuild")
return transaction {
Manager.new {
this.user = user
this.discordGuildId = user.liveAlertGuild!!
this.managerId = discordId
this.lastUserName = name
}
}
}
fun updateManager(user: User, discordId: Long, name: String): Manager {
if (user.liveAlertGuild == null)
throw RuntimeException("${user.username} has no liveAlertGuild")
val manager = getUser(user.liveAlertGuild!!, discordId)
if (manager == null)
throw RuntimeException("$name isn't manager.")
manager.lastUserName = name
return manager
}
fun getUser(guildId: Long, discordId: Long): Manager? {
return transaction {
val manager = Manager.find(
(Managers.discordGuildId eq guildId) and (Managers.managerId eq discordId)
)
manager.firstOrNull()
}
}
fun getAllUsers(guildId: Long): List<Manager> {
return transaction {
Manager.find(Managers.discordGuildId eq guildId).toList()
}
}
fun deleteManager(user: User, discordId: Long): Manager {
if (user.liveAlertGuild == null)
throw RuntimeException("${user.username} has no liveAlertGuild")
return deleteManager(user.liveAlertGuild!!, discordId)
}
fun deleteManager(guildId: Long, discordId: Long): Manager {
return transaction {
val managerRow = Manager.find((Managers.discordGuildId eq guildId) and (Managers.managerId eq discordId)).firstOrNull()
managerRow ?: throw RuntimeException("Manager not found! $discordId")
managerRow.delete()
managerRow
}
}
}

View File

@@ -39,6 +39,14 @@ object UserService {
}
}
fun getUserWithGuildId(discordGuildId: Long): User? {
return transaction {
val users = User.find(Users.liveAlertGuild eq discordGuildId)
users.firstOrNull()
}
}
fun getAllUsers(): List<User> {
return transaction {
User.all().toList()

View File

@@ -0,0 +1,35 @@
<configuration>
<!-- 콘솔에 출력하는 기본 로그 설정 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
<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="o.m.jdbc.client.impl.StandardClient" level="INFO" />
<logger name="o.m.jdbc.message.server.OkPacket" level="INFO" />
<logger name="o.m.jdbc.client.impl.StandardClient" 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="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>

View File

@@ -3,7 +3,7 @@
<!-- 콘솔에 출력하는 기본 로그 설정 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
<level>INFO</level>
</filter>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>