Compare commits

..

7 Commits

Author SHA1 Message Date
dalbodeule
1465716e72
[hotfix] hotfix on register and activate logics. 2025-05-16 00:54:43 +09:00
dalbodeule
d0292e0aa6
[hotfix] hotfix on alert embed tags. 2025-05-16 00:42:28 +09:00
dalbodeule
b2ffd18126
[hotfix] hotfix on lateinit botuid is not initialized 2025-05-16 00:38:26 +09:00
dalbodeule
5fa04a6725
[hotfix] hotfix on manage users. 2025-05-16 00:29:50 +09:00
dalbodeule
f65c446bed
[hotfix] hotfix on some codes. 2025-05-16 00:25:28 +09:00
JinU Choi
729a88a2b3
Merge pull request #126 from dalbodeule/develop
[refactor] user and live stream handling logic
2025-05-16 00:01:07 +09:00
dalbodeule
a896269087
[refactor] user and live stream handling logic
Replaced ChzzkUserCache with event-based user fetching for cleaner architecture. Integrated new ChzzkUserFindEvent and ChzzkUserReceiveEvent to handle user data retrieval. Removed old utility methods and streamlined live stream status checks with updated APIs.
2025-05-15 04:57:17 +09:00
10 changed files with 146 additions and 247 deletions

View File

@ -1,19 +1,14 @@
package space.mori.chzzk_bot.chatbot.chzzk package space.mori.chzzk_bot.chatbot.chzzk
import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import org.koin.java.KoinJavaComponent.inject import org.koin.java.KoinJavaComponent.inject
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import space.mori.chzzk_bot.chatbot.chzzk.Connector.client as ChzzkClient
import space.mori.chzzk_bot.chatbot.chzzk.Connector.getChannel import space.mori.chzzk_bot.chatbot.chzzk.Connector.getChannel
import space.mori.chzzk_bot.chatbot.discord.Discord import space.mori.chzzk_bot.chatbot.discord.Discord
import space.mori.chzzk_bot.chatbot.utils.refreshAccessToken
import space.mori.chzzk_bot.common.events.* import space.mori.chzzk_bot.common.events.*
import space.mori.chzzk_bot.common.models.User import space.mori.chzzk_bot.common.models.User
import space.mori.chzzk_bot.common.services.LiveStatusService import space.mori.chzzk_bot.common.services.LiveStatusService
@ -21,12 +16,12 @@ import space.mori.chzzk_bot.common.services.TimerConfigService
import space.mori.chzzk_bot.common.services.UserService import space.mori.chzzk_bot.common.services.UserService
import space.mori.chzzk_bot.common.utils.* import space.mori.chzzk_bot.common.utils.*
import xyz.r2turntrue.chzzk4j.ChzzkClient import xyz.r2turntrue.chzzk4j.ChzzkClient
import xyz.r2turntrue.chzzk4j.auth.ChzzkSimpleUserLoginAdapter
import xyz.r2turntrue.chzzk4j.session.ChzzkSessionBuilder import xyz.r2turntrue.chzzk4j.session.ChzzkSessionBuilder
import xyz.r2turntrue.chzzk4j.session.ChzzkSessionSubscriptionType import xyz.r2turntrue.chzzk4j.session.ChzzkSessionSubscriptionType
import xyz.r2turntrue.chzzk4j.session.ChzzkUserSession import xyz.r2turntrue.chzzk4j.session.ChzzkUserSession
import xyz.r2turntrue.chzzk4j.session.event.SessionChatMessageEvent import xyz.r2turntrue.chzzk4j.session.event.SessionChatMessageEvent
import xyz.r2turntrue.chzzk4j.types.channel.ChzzkChannel import xyz.r2turntrue.chzzk4j.types.channel.ChzzkChannel
import xyz.r2turntrue.chzzk4j.types.channel.live.ChzzkLiveStatus
import java.lang.Exception import java.lang.Exception
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
import java.time.LocalDateTime import java.time.LocalDateTime
@ -43,6 +38,7 @@ object ChzzkHandler {
} }
fun enable() { fun enable() {
botUid = Connector.client.fetchLoggedUser().userId
UserService.getAllUsers().map { UserService.getAllUsers().map {
if(!it.isDisabled) if(!it.isDisabled)
try { try {
@ -53,8 +49,8 @@ object ChzzkHandler {
} }
handlers.forEach { handler -> handlers.forEach { handler ->
val streamInfo = getStreamInfo(handler.channel.channelId) val streamInfo = Connector.getLive(handler.channel.channelId)
if (streamInfo.content?.status == "OPEN") handler.isActive(true, streamInfo) if (streamInfo?.isOnline == true) handler.isActive(true, streamInfo)
} }
dispatcher.subscribe(UserRegisterEvent::class) { dispatcher.subscribe(UserRegisterEvent::class) {
@ -113,15 +109,15 @@ object ChzzkHandler {
handlers.forEach { handlers.forEach {
if (!running) return@forEach if (!running) return@forEach
try { try {
val streamInfo = getStreamInfo(it.channel.channelId) val streamInfo = Connector.getLive(it.channel.channelId)
if (streamInfo.content?.status == "OPEN" && !it.isActive) { if (streamInfo?.isOnline == true && !it.isActive) {
try { try {
it.isActive(true, streamInfo) it.isActive(true, streamInfo)
} catch(e: Exception) { } catch(e: Exception) {
logger.info("Exception: ${e.stackTraceToString()}") logger.info("Exception: ${e.stackTraceToString()}")
} }
} }
if (streamInfo.content?.status == "CLOSE" && it.isActive) it.isActive(false, streamInfo) if (streamInfo?.isOnline == false && it.isActive) it.isActive(false, streamInfo)
} catch (e: SocketTimeoutException) { } catch (e: SocketTimeoutException) {
logger.info("Thread 1 Timeout: ${it.channel.channelName} / ${e.stackTraceToString()}") logger.info("Thread 1 Timeout: ${it.channel.channelName} / ${e.stackTraceToString()}")
} catch (e: Exception) { } catch (e: Exception) {
@ -141,19 +137,19 @@ object ChzzkHandler {
handlers.forEach { handlers.forEach {
if (!running) return@forEach if (!running) return@forEach
try { try {
val streamInfo = getStreamInfo(it.channel.channelId) val streamInfo = Connector.getLive(it.channel.channelId)
if (streamInfo.content?.status == "OPEN" && !it.isActive) { if (streamInfo?.isOnline == true && !it.isActive) {
try { try {
it.isActive(true, streamInfo) it.isActive(true, streamInfo)
} catch(e: Exception) { } catch(e: Exception) {
logger.info("Exception: ${e.stackTraceToString()}") logger.info("Exception: ${e.stackTraceToString()}")
} }
} }
if (streamInfo.content?.status == "CLOSE" && it.isActive) it.isActive(false, streamInfo) if (streamInfo?.isOnline == false && it.isActive) it.isActive(false, streamInfo)
} catch (e: SocketTimeoutException) { } catch (e: SocketTimeoutException) {
logger.info("Thread 2 Timeout: ${it.channel.channelName} / ${e.stackTraceToString()}") logger.info("Thread 1 Timeout: ${it.channel.channelName} / ${e.stackTraceToString()}")
} catch (e: Exception) { } catch (e: Exception) {
logger.info("Thread 2 Exception: ${it.channel.channelName} / ${e.stackTraceToString()}") logger.info("Thread 1 Exception: ${it.channel.channelName} / ${e.stackTraceToString()}")
} finally { } finally {
Thread.sleep(5000) Thread.sleep(5000)
} }
@ -204,8 +200,8 @@ class UserHandler(
var streamStartTime: LocalDateTime?, var streamStartTime: LocalDateTime?,
) { ) {
var messageHandler: MessageHandler var messageHandler: MessageHandler
lateinit var client: ChzzkClient var client: ChzzkClient
lateinit var listener: ChzzkUserSession var listener: ChzzkUserSession
private val dispatcher: CoroutinesEventBus by inject(CoroutinesEventBus::class.java) private val dispatcher: CoroutinesEventBus by inject(CoroutinesEventBus::class.java)
private var _isActive: Boolean private var _isActive: Boolean
@ -220,20 +216,28 @@ class UserHandler(
if(user?.accessToken == null || user.refreshToken == null) { if(user?.accessToken == null || user.refreshToken == null) {
throw RuntimeException("AccessToken or RefreshToken is not valid.") throw RuntimeException("AccessToken or RefreshToken is not valid.")
} }
try {
val tokens = ChzzkClient.refreshAccessToken(user.refreshToken!!) client = Connector.getClient(user.accessToken!!, user.refreshToken!!)
client = Connector.getClient(tokens.first, tokens.second) client.loginAsync().join()
listener = ChzzkSessionBuilder(client).buildUserSession() client.refreshTokenAsync().join()
UserService.setRefreshToken(user, tokens.first, tokens.second) UserService.setRefreshToken(user, client.loginResult.accessToken(), client.loginResult.refreshToken())
listener.createAndConnectAsync().join() listener = ChzzkSessionBuilder(client).buildUserSession()
listener.on(SessionChatMessageEvent::class.java) { listener.createAndConnectAsync().join()
messageHandler.handle(it.message, user)
messageHandler = MessageHandler(this@UserHandler)
listener.on(SessionChatMessageEvent::class.java) {
messageHandler.handle(it.message, user)
}
} catch(e: Exception) {
logger.error("Exception(${user.username}): ${e.stackTraceToString()}")
throw RuntimeException("Exception: ${e.stackTraceToString()}")
} }
messageHandler = MessageHandler(this@UserHandler)
} }
internal fun disable() { internal fun disable() {
@ -252,7 +256,7 @@ class UserHandler(
internal val isActive: Boolean internal val isActive: Boolean
get() = _isActive get() = _isActive
internal fun isActive(value: Boolean, status: IData<IStreamInfo?>) { internal fun isActive(value: Boolean, status: ChzzkLiveStatus) {
if(value) { if(value) {
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
logger.info("${user.username} is live.") logger.info("${user.username} is live.")
@ -262,7 +266,7 @@ class UserHandler(
logger.info("ChzzkChat connecting... ${channel.channelName} - ${channel.channelId}") logger.info("ChzzkChat connecting... ${channel.channelName} - ${channel.channelId}")
listener.subscribeAsync(ChzzkSessionSubscriptionType.CHAT) listener.subscribeAsync(ChzzkSessionSubscriptionType.CHAT)
streamStartTime = status.content?.openDate?.let { convertChzzkDateToLocalDateTime(it) } streamStartTime = LocalDateTime.now()
if(!_isActive) { if(!_isActive) {
_isActive = true _isActive = true

View File

@ -1,28 +1,56 @@
package space.mori.chzzk_bot.chatbot.chzzk package space.mori.chzzk_bot.chatbot.chzzk
import io.github.cdimascio.dotenv.dotenv import io.github.cdimascio.dotenv.dotenv
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.koin.java.KoinJavaComponent.inject
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import space.mori.chzzk_bot.common.events.ChzzkUserFindEvent
import space.mori.chzzk_bot.common.events.ChzzkUserReceiveEvent
import space.mori.chzzk_bot.common.events.CoroutinesEventBus
import xyz.r2turntrue.chzzk4j.ChzzkClient import xyz.r2turntrue.chzzk4j.ChzzkClient
import xyz.r2turntrue.chzzk4j.ChzzkClientBuilder import xyz.r2turntrue.chzzk4j.ChzzkClientBuilder
import xyz.r2turntrue.chzzk4j.auth.ChzzkOauthLoginAdapter import xyz.r2turntrue.chzzk4j.auth.ChzzkLegacyLoginAdapter
import xyz.r2turntrue.chzzk4j.auth.ChzzkSimpleUserLoginAdapter import xyz.r2turntrue.chzzk4j.auth.ChzzkSimpleUserLoginAdapter
import xyz.r2turntrue.chzzk4j.types.channel.ChzzkChannel import xyz.r2turntrue.chzzk4j.types.channel.ChzzkChannel
import xyz.r2turntrue.chzzk4j.types.channel.live.ChzzkLiveStatus
import kotlin.getValue
val dotenv = dotenv { val dotenv = dotenv {
ignoreIfMissing = true ignoreIfMissing = true
} }
@OptIn(DelicateCoroutinesApi::class)
object Connector { object Connector {
val adapter = ChzzkLegacyLoginAdapter(dotenv["NID_AUT"], dotenv["NID_SES"])
val client: ChzzkClient = ChzzkClientBuilder(dotenv["NAVER_CLIENT_ID"], dotenv["NAVER_CLIENT_SECRET"]) val client: ChzzkClient = ChzzkClientBuilder(dotenv["NAVER_CLIENT_ID"], dotenv["NAVER_CLIENT_SECRET"])
.withLoginAdapter(adapter)
.build() .build()
private val logger = LoggerFactory.getLogger(this::class.java) private val logger = LoggerFactory.getLogger(this::class.java)
private val dispatcher: CoroutinesEventBus by inject(CoroutinesEventBus::class.java)
fun getChannel(channelId: String): ChzzkChannel? = client.fetchChannel(channelId) fun getChannel(channelId: String): ChzzkChannel? = client.fetchChannel(channelId)
fun getLive(channelId: String): ChzzkLiveStatus? = client.fetchLiveStatus(channelId)
init { init {
logger.info("chzzk logged: ${client.isLoggedIn}") logger.info("chzzk logged: ${client.isLoggedIn}")
client.loginAsync().join() client.loginAsync().join()
dispatcher.subscribe(ChzzkUserFindEvent::class) { event ->
GlobalScope.launch {
val user = getChannel(event.uid)
dispatcher.post(ChzzkUserReceiveEvent(
find = user != null,
uid = user?.channelId,
nickname = user?.channelName,
isStreamOn = user?.isBroadcasting,
avatarUrl = user?.channelImageUrl
))
}
}
} }
fun getClient(accessToken: String, refreshToken: String): ChzzkClient { fun getClient(accessToken: String, refreshToken: String): ChzzkClient {
@ -33,4 +61,6 @@ object Connector {
return client return client
} }
} }

View File

@ -12,11 +12,11 @@ import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEve
import net.dv8tion.jda.api.hooks.ListenerAdapter import net.dv8tion.jda.api.hooks.ListenerAdapter
import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import space.mori.chzzk_bot.common.utils.IData
import space.mori.chzzk_bot.common.utils.IStreamInfo
import space.mori.chzzk_bot.chatbot.discord.commands.* import space.mori.chzzk_bot.chatbot.discord.commands.*
import space.mori.chzzk_bot.common.models.User import space.mori.chzzk_bot.common.models.User
import xyz.r2turntrue.chzzk4j.types.channel.live.ChzzkLiveStatus
import java.time.Instant import java.time.Instant
import kotlin.jvm.optionals.getOrNull
val dotenv = dotenv { val dotenv = dotenv {
ignoreIfMissing = true ignoreIfMissing = true
@ -33,20 +33,19 @@ class Discord: ListenerAdapter() {
return bot.getGuildById(guildId)?.getTextChannelById(channelId) return bot.getGuildById(guildId)?.getTextChannelById(channelId)
} }
fun sendDiscord(user: User, status: IData<IStreamInfo?>) { fun sendDiscord(user: User, status: ChzzkLiveStatus) {
if(status.content == null) return
if(user.liveAlertMessage != null && user.liveAlertGuild != null && user.liveAlertChannel != null) { if(user.liveAlertMessage != null && user.liveAlertGuild != null && user.liveAlertChannel != null) {
val channel = getChannel(user.liveAlertGuild ?: 0, user.liveAlertChannel ?: 0) val channel = getChannel(user.liveAlertGuild ?: 0, user.liveAlertChannel ?: 0)
?: throw RuntimeException("${user.liveAlertChannel} is not valid.") ?: throw RuntimeException("${user.liveAlertChannel} is not valid.")
val embed = EmbedBuilder() val embed = EmbedBuilder()
embed.setTitle(status.content!!.liveTitle, "https://chzzk.naver.com/live/${user.token}") embed.setTitle(status.title, "https://chzzk.naver.com/live/${user.token}")
embed.setDescription("${user.username} 님이 방송을 시작했습니다.") embed.setDescription("${user.username} 님이 방송을 시작했습니다.")
embed.setTimestamp(Instant.now()) embed.setTimestamp(Instant.now())
embed.setAuthor(user.username, "https://chzzk.naver.com/live/${user.token}", status.content!!.channel.channelImageUrl) embed.setAuthor(user.username, "https://chzzk.naver.com/live/${user.token}")
embed.addField("카테고리", status.content!!.liveCategoryValue, true) embed.addField("카테고리", status.liveCategoryValue, true)
embed.addField("태그", status.content!!.tags.joinToString(", "), true) embed.addField("태그", status.tags.joinToString(", ") { it.trim() }, true)
embed.setImage(status.content!!.liveImageUrl.replace("{type}", "1080")) // embed.setImage(status.)
channel.sendMessage( channel.sendMessage(
MessageCreateBuilder() MessageCreateBuilder()

View File

@ -1,55 +0,0 @@
package space.mori.chzzk_bot.chatbot.utils
import com.google.gson.Gson
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import space.mori.chzzk_bot.common.utils.client
import xyz.r2turntrue.chzzk4j.ChzzkClient
import java.io.IOException
val client = OkHttpClient.Builder()
.addNetworkInterceptor { chain ->
chain.proceed(
chain.request()
.newBuilder()
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
.build()
)
}
.build()
val gson = Gson()
data class RefreshTokenResponse(
val accessToken: String,
val refreshToken: String,
val expiresIn: Int,
val tokenType: String = "Bearer",
val scope: String
)
fun ChzzkClient.refreshAccessToken(refreshToken: String): Pair<String, String> {
val url = "https://openapi.chzzk.naver.com/auth/v1/token"
val request = Request.Builder()
.url(url)
.header("Content-Type", "application/json")
.post(gson.toJson(mapOf(
"grantType" to "refresh_token",
"refreshToken" to refreshToken,
"clientId" to this.apiClientId,
"clientSecret" to this.apiSecret
)).toRequestBody())
.build()
client.newCall(request).execute().use { response ->
try {
if(!response.isSuccessful) throw IOException("Unexpected code ${response.code}")
val body = response.body?.string()
val data = gson.fromJson(body, RefreshTokenResponse::class.java)
return Pair(data.accessToken, data.refreshToken)
} catch(e: Exception) {
throw e
}
}
}

View File

@ -0,0 +1,7 @@
package space.mori.chzzk_bot.common.events
data class ChzzkUserFindEvent(
val uid: String
): Event {
val TAG = javaClass.simpleName
}

View File

@ -0,0 +1,11 @@
package space.mori.chzzk_bot.common.events
data class ChzzkUserReceiveEvent(
val find: Boolean = true,
val uid: String? = null,
val nickname: String? = null,
val isStreamOn: Boolean? = null,
val avatarUrl: String? = null,
): Event {
val TAG = javaClass.simpleName
}

View File

@ -52,49 +52,6 @@ data class NicknameColor(
val colorCode: String = "" val colorCode: String = ""
) )
// Stream info
data class IStreamInfo(
val liveId: Int = 0,
val liveTitle: String = "",
val status: String = "",
val liveImageUrl: String = "",
val defaultThumbnailImageUrl: String? = null,
val concurrentUserCount: Int = 0,
val accumulateCount: Int = 0,
val openDate: String = "",
val closeDate: String = "",
val adult: Boolean = false,
val clipActive: Boolean = false,
val tags: List<String> = emptyList(),
val chatChannelId: String = "",
val categoryType: String = "",
val liveCategory: String = "",
val liveCategoryValue: String = "",
val chatActive: Boolean = true,
val chatAvailableGroup: String = "",
val paidPromotion: Boolean = false,
val chatAvailableCondition: String = "",
val minFollowerMinute: Int = 0,
val livePlaybackJson: String = "",
val p2pQuality: List<Any> = emptyList(),
val channel: Channel = Channel(),
val livePollingStatusJson: String = "",
val userAdultStatus: String? = null,
val chatDonationRankingExposure: Boolean = true,
val adParameter: AdParameter = AdParameter()
)
data class Channel(
val channelId: String = "",
val channelName: String = "",
val channelImageUrl: String = "",
val verifiedMark: Boolean = false
)
data class AdParameter(
val tag: String = ""
)
// OkHttpClient에 Interceptor 추가 // OkHttpClient에 Interceptor 추가
val client = OkHttpClient.Builder() val client = OkHttpClient.Builder()
.addNetworkInterceptor { chain -> .addNetworkInterceptor { chain ->
@ -127,41 +84,3 @@ fun getFollowDate(chatID: String, userId: String) : IData<IFollowContent?> {
} }
} }
} }
fun getStreamInfo(userId: String) : IData<IStreamInfo?> {
val url = "https://api.chzzk.naver.com/service/v3/channels/${userId}/live-detail"
val request = Request.Builder()
.url(url)
.build()
client.newCall(request).execute().use { response ->
try {
if(!response.isSuccessful) throw IOException("Unexpected code ${response.code}")
val body = response.body?.string()
val follow = gson.fromJson(body, object: TypeToken<IData<IStreamInfo?>>() {})
return follow
} catch(e: Exception) {
throw e
}
}
}
fun getUserInfo(userId: String): IData<Channel?> {
val url = "https://api.chzzk.naver.com/service/v1/channels/${userId}"
val request = Request.Builder()
.url(url)
.build()
client.newCall(request).execute().use { response ->
try {
if(!response.isSuccessful) throw IOException("Unexpected code ${response.code}")
val body = response.body?.string()
val channel = gson.fromJson(body, object: TypeToken<IData<Channel?>>() {})
return channel
} catch(e: Exception) {
throw e
}
}
}

View File

@ -22,12 +22,16 @@ import io.ktor.server.websocket.*
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.koin.java.KoinJavaComponent.inject
import space.mori.chzzk_bot.common.events.CoroutinesEventBus
import space.mori.chzzk_bot.common.events.UserRegisterEvent
import space.mori.chzzk_bot.common.services.UserService import space.mori.chzzk_bot.common.services.UserService
import space.mori.chzzk_bot.webserver.routes.* import space.mori.chzzk_bot.webserver.routes.*
import space.mori.chzzk_bot.webserver.utils.DiscordRatelimits import space.mori.chzzk_bot.webserver.utils.DiscordRatelimits
import java.math.BigInteger import java.math.BigInteger
import java.security.SecureRandom import java.security.SecureRandom
import java.time.Duration import java.time.Duration
import kotlin.getValue
import kotlin.time.toKotlinDuration import kotlin.time.toKotlinDuration
val dotenv = dotenv { val dotenv = dotenv {
@ -81,6 +85,8 @@ val server = embeddedServer(Netty, port = 8080, ) {
} }
} }
routing { routing {
val dispatcher: CoroutinesEventBus by inject(CoroutinesEventBus::class.java)
route("/auth") { route("/auth") {
// discord login // discord login
authenticate("auth-oauth-discord") { authenticate("auth-oauth-discord") {
@ -191,7 +197,12 @@ val server = embeddedServer(Netty, port = 8080, ) {
val userInfo = getChzzkUser(tokenResponse.content.accessToken) val userInfo = getChzzkUser(tokenResponse.content.accessToken)
if(userInfo.content != null) { if(userInfo.content != null) {
val user = UserService.getUser(userInfo.content.channelId) var user = UserService.getUser(userInfo.content.channelId)
if(user == null) {
user = UserService.saveUser(userInfo.content.channelName , userInfo.content.channelId)
}
call.sessions.set( call.sessions.set(
UserSession( UserSession(
session.state, session.state,
@ -199,7 +210,13 @@ val server = embeddedServer(Netty, port = 8080, ) {
listOf() listOf()
) )
) )
user?.let { UserService.setRefreshToken(it, tokenResponse.content.accessToken, tokenResponse.content.refreshToken ?: "") } UserService.setRefreshToken(user,
tokenResponse.content.accessToken,
tokenResponse.content.refreshToken ?: ""
)
dispatcher.post(UserRegisterEvent(user.token))
call.respondRedirect(getFrontendURL("")) call.respondRedirect(getFrontendURL(""))
} }
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -11,7 +11,10 @@ import space.mori.chzzk_bot.common.events.CoroutinesEventBus
import space.mori.chzzk_bot.common.services.SongConfigService import space.mori.chzzk_bot.common.services.SongConfigService
import space.mori.chzzk_bot.common.services.UserService import space.mori.chzzk_bot.common.services.UserService
import space.mori.chzzk_bot.webserver.UserSession import space.mori.chzzk_bot.webserver.UserSession
import space.mori.chzzk_bot.webserver.utils.ChzzkUserCache import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.withTimeoutOrNull
import space.mori.chzzk_bot.common.events.ChzzkUserFindEvent
import space.mori.chzzk_bot.common.events.ChzzkUserReceiveEvent
@Serializable @Serializable
data class GetUserDTO( data class GetUserDTO(
@ -36,6 +39,20 @@ data class GetSessionDTO(
fun Routing.apiRoutes() { fun Routing.apiRoutes() {
val dispatcher: CoroutinesEventBus by inject(CoroutinesEventBus::class.java) val dispatcher: CoroutinesEventBus by inject(CoroutinesEventBus::class.java)
suspend fun getChzzkUserWithId(uid: String): ChzzkUserReceiveEvent? {
val completableDeferred = CompletableDeferred<ChzzkUserReceiveEvent>()
dispatcher.subscribe(ChzzkUserReceiveEvent::class) { event ->
if (event.uid == uid) {
completableDeferred.complete(event)
}
}
val user = withTimeoutOrNull(5000) {
dispatcher.post(ChzzkUserFindEvent(uid))
completableDeferred.await()
}
return user
}
route("/") { route("/") {
get { get {
call.respondText("Hello World!", status = call.respondText("Hello World!", status =
@ -50,21 +67,21 @@ fun Routing.apiRoutes() {
route("/user/{uid}") { route("/user/{uid}") {
get { get {
val uid = call.parameters["uid"] val uid = call.parameters["uid"]
if(uid == null) { if(uid == null) {
call.respondText("Require UID", status = HttpStatusCode.NotFound) call.respondText("Require UID", status = HttpStatusCode.NotFound)
return@get return@get
} }
val user = ChzzkUserCache.getCachedUser(uid) val user = getChzzkUserWithId(uid)
if(user?.content == null) { if (user?.find == false) {
call.respondText("User not found", status = HttpStatusCode.NotFound) call.respondText("User not found", status = HttpStatusCode.NotFound)
return@get return@get
} else { } else {
call.respond(HttpStatusCode.OK, GetUserDTO( call.respond(HttpStatusCode.OK, GetUserDTO(
user.content!!.channel.channelId, user?.uid ?: "",
user.content!!.channel.channelName, user?.nickname ?: "",
user.content!!.status == "OPEN", user?.isStreamOn ?: false,
user.content!!.channel.channelImageUrl user?.avatarUrl ?: ""
)) ))
} }
} }
@ -82,7 +99,7 @@ fun Routing.apiRoutes() {
user = UserService.saveUser("임시닉네임", session.id) user = UserService.saveUser("임시닉네임", session.id)
} }
val songConfig = SongConfigService.getConfig(user) val songConfig = SongConfigService.getConfig(user)
val status = ChzzkUserCache.getCachedUser(session.id) val status = getChzzkUserWithId(user.token)
val returnUsers = mutableListOf<GetSessionDTO>() val returnUsers = mutableListOf<GetSessionDTO>()
if(status == null) { if(status == null) {
@ -91,14 +108,14 @@ fun Routing.apiRoutes() {
} }
if (user.username == "임시닉네임") { if (user.username == "임시닉네임") {
status.content?.channel?.let { it1 -> UserService.updateUser(user, it1.channelId, it1.channelName) } status.let { stats -> UserService.updateUser(user, stats.uid ?: "", stats.nickname ?: "") }
} }
returnUsers.add(GetSessionDTO( returnUsers.add(GetSessionDTO(
status.content?.channel?.channelId ?: user.username, status.uid ?: user.token,
status.content?.channel?.channelName ?: user.token, status.nickname ?: user.username,
status.content?.status == "OPEN", status.isStreamOn == true,
status.content?.channel?.channelImageUrl ?: "", status.avatarUrl ?: "",
songConfig.queueLimit, songConfig.queueLimit,
songConfig.personalLimit, songConfig.personalLimit,
songConfig.streamerOnly, songConfig.streamerOnly,
@ -109,15 +126,15 @@ fun Routing.apiRoutes() {
user.subordinates.toList() user.subordinates.toList()
} }
returnUsers.addAll(subordinates.map { returnUsers.addAll(subordinates.map {
val subStatus = ChzzkUserCache.getCachedUser(it.token) val subStatus = getChzzkUserWithId(it.token)
return@map if (subStatus?.content == null) { return@map if (subStatus == null) {
null null
} else { } else {
GetSessionDTO( GetSessionDTO(
subStatus.content!!.channel.channelId, subStatus.uid ?: "",
subStatus.content!!.channel.channelName, subStatus.nickname ?: "",
subStatus.content!!.status == "OPEN", subStatus.isStreamOn == true,
subStatus.content!!.channel.channelImageUrl, subStatus.avatarUrl ?: "",
0, 0,
0, 0,
false, false,

View File

@ -1,50 +0,0 @@
package space.mori.chzzk_bot.webserver.utils
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.slf4j.LoggerFactory
import space.mori.chzzk_bot.common.utils.IData
import space.mori.chzzk_bot.common.utils.IStreamInfo
import space.mori.chzzk_bot.common.utils.getStreamInfo
import space.mori.chzzk_bot.common.utils.getUserInfo
import java.time.Instant
import java.util.concurrent.ConcurrentHashMap
object ChzzkUserCache {
private val cache = ConcurrentHashMap<String, CachedUser>()
private const val EXP_SECONDS = 600L
private val mutex = Mutex()
private val logger = LoggerFactory.getLogger(this::class.java)
suspend fun getCachedUser(id: String): IData<IStreamInfo?>? {
val now = Instant.now()
var user = cache[id]
if(user == null || user.timestamp.plusSeconds(EXP_SECONDS).isBefore(now)) {
mutex.withLock {
if(user == null || user.timestamp.plusSeconds(EXP_SECONDS)?.isBefore(now) != false) {
var findUser = getStreamInfo(id)
if(findUser.content == null) {
val userInfo = getUserInfo(id)
if(userInfo.content == null) return null
findUser = IData(200, null, IStreamInfo(
channel = userInfo.content!!
))
}
user = CachedUser(findUser)
user.let { cache[id] = user }
}
}
}
return cache[id]?.user
}
}
data class CachedUser(
val user: IData<IStreamInfo?>,
val timestamp: Instant = Instant.now(),
)