Merge pull request #32 from dalbodeule/develop

Fix /user/{uid} endoints.
This commit is contained in:
JinU Choi 2024-08-04 20:32:40 +09:00 committed by GitHub
commit 04f6b14daa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 47 additions and 138 deletions

View File

@ -14,8 +14,7 @@ import space.mori.chzzk_bot.common.models.User
import space.mori.chzzk_bot.common.services.LiveStatusService import space.mori.chzzk_bot.common.services.LiveStatusService
import space.mori.chzzk_bot.common.services.TimerConfigService 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.convertChzzkDateToLocalDateTime import space.mori.chzzk_bot.common.utils.*
import space.mori.chzzk_bot.common.utils.getUptime
import xyz.r2turntrue.chzzk4j.chat.ChatEventListener import xyz.r2turntrue.chzzk4j.chat.ChatEventListener
import xyz.r2turntrue.chzzk4j.chat.ChatMessage import xyz.r2turntrue.chzzk4j.chat.ChatMessage
import xyz.r2turntrue.chzzk4j.chat.ChzzkChat import xyz.r2turntrue.chzzk4j.chat.ChzzkChat
@ -42,7 +41,7 @@ object ChzzkHandler {
handlers.forEach { handler -> handlers.forEach { handler ->
val streamInfo = getStreamInfo(handler.listener.channelId) val streamInfo = getStreamInfo(handler.listener.channelId)
if (streamInfo.content.status == "OPEN") handler.isActive(true, streamInfo) if (streamInfo.content?.status == "OPEN") handler.isActive(true, streamInfo)
} }
} }
@ -76,8 +75,8 @@ object ChzzkHandler {
if (!running) return@forEach if (!running) return@forEach
try { try {
val streamInfo = getStreamInfo(it.channel.channelId) val streamInfo = getStreamInfo(it.channel.channelId)
if (streamInfo.content.status == "OPEN" && !it.isActive) it.isActive(true, streamInfo) if (streamInfo.content?.status == "OPEN" && !it.isActive) it.isActive(true, streamInfo)
if (streamInfo.content.status == "CLOSE" && it.isActive) it.isActive(false, streamInfo) if (streamInfo.content?.status == "CLOSE" && it.isActive) it.isActive(false, streamInfo)
} catch(e: SocketTimeoutException) { } catch(e: SocketTimeoutException) {
logger.info("Timeout: ${it.channel.channelName} / ${e.stackTraceToString()}") logger.info("Timeout: ${it.channel.channelName} / ${e.stackTraceToString()}")
} catch (e: Exception) { } catch (e: Exception) {
@ -152,14 +151,14 @@ 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: IData<IStreamInfo?>) {
if(value) { if(value) {
logger.info("${user.username} is live.") logger.info("${user.username} is live.")
logger.info("ChzzkChat connecting... ${channel.channelName} - ${channel.channelId}") logger.info("ChzzkChat connecting... ${channel.channelName} - ${channel.channelId}")
listener.connectBlocking() listener.connectBlocking()
streamStartTime = convertChzzkDateToLocalDateTime(status.content.openDate) streamStartTime = status.content?.openDate?.let { convertChzzkDateToLocalDateTime(it) }
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
when(TimerConfigService.getConfig(UserService.getUser(channel.channelId)!!)?.option) { when(TimerConfigService.getConfig(UserService.getUser(channel.channelId)!!)?.option) {

View File

@ -1,16 +1,7 @@
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.CoroutineScope
import kotlinx.coroutines.Dispatchers
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.CoroutinesEventBus
import space.mori.chzzk_bot.common.events.GetUserEvents
import space.mori.chzzk_bot.common.events.GetUserType
import space.mori.chzzk_bot.common.services.LiveStatusService
import space.mori.chzzk_bot.common.services.UserService
import xyz.r2turntrue.chzzk4j.Chzzk import xyz.r2turntrue.chzzk4j.Chzzk
import xyz.r2turntrue.chzzk4j.ChzzkBuilder import xyz.r2turntrue.chzzk4j.ChzzkBuilder
import xyz.r2turntrue.chzzk4j.types.channel.ChzzkChannel import xyz.r2turntrue.chzzk4j.types.channel.ChzzkChannel
@ -24,34 +15,10 @@ object Connector {
.withAuthorization(dotenv["NID_AUT"], dotenv["NID_SES"]) .withAuthorization(dotenv["NID_AUT"], dotenv["NID_SES"])
.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? = chzzk.getChannel(channelId) fun getChannel(channelId: String): ChzzkChannel? = chzzk.getChannel(channelId)
init { init {
logger.info("chzzk logged: ${chzzk.isLoggedIn} / ${chzzk.loggedUser?.nickname ?: "----"}") logger.info("chzzk logged: ${chzzk.isLoggedIn} / ${chzzk.loggedUser?.nickname ?: "----"}")
dispatcher.subscribe(GetUserEvents::class) {
if (it.type == GetUserType.REQUEST) {
CoroutineScope(Dispatchers.Default).launch {
val channel = getChannel(it.uid ?: "")
if(channel == null) dispatcher.post(GetUserEvents(
GetUserType.NOTFOUND, null, null, null, null
))
else {
val user = UserService.getUser(channel.channelId)
dispatcher.post(
GetUserEvents(
GetUserType.RESPONSE,
channel.channelId,
channel.channelName,
LiveStatusService.getLiveStatus(user!!)?.status ?: false,
channel.channelImageUrl
)
)
}
}
}
}
} }
} }

View File

@ -7,6 +7,7 @@ import org.koin.java.KoinJavaComponent.inject
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.* import space.mori.chzzk_bot.common.services.*
import space.mori.chzzk_bot.common.utils.getFollowDate
import space.mori.chzzk_bot.common.utils.getUptime import space.mori.chzzk_bot.common.utils.getUptime
import space.mori.chzzk_bot.common.utils.getYoutubeVideo import space.mori.chzzk_bot.common.utils.getYoutubeVideo
import xyz.r2turntrue.chzzk4j.chat.ChatMessage import xyz.r2turntrue.chzzk4j.chat.ChatMessage
@ -280,7 +281,7 @@ class MessageHandler(
result = followPattern.replace(result) { _ -> result = followPattern.replace(result) { _ ->
try { try {
val followingDate = getFollowDate(listener.chatId, msg.userId) val followingDate = getFollowDate(listener.chatId, msg.userId)
.content.streamingProperty.following?.followDate .content?.streamingProperty?.following?.followDate
val period = followingDate?.let { val period = followingDate?.let {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")

View File

@ -12,8 +12,8 @@ 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.chatbot.chzzk.IData import space.mori.chzzk_bot.common.utils.IData
import space.mori.chzzk_bot.chatbot.chzzk.IStreamInfo 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 space.mori.chzzk_bot.common.services.ManagerService import space.mori.chzzk_bot.common.services.ManagerService
@ -33,18 +33,19 @@ class Discord: ListenerAdapter() {
internal fun getChannel(guildId: Long, channelId: Long) = internal fun getChannel(guildId: Long, channelId: Long) =
bot.getGuildById(guildId)?.getTextChannelById(channelId) bot.getGuildById(guildId)?.getTextChannelById(channelId)
fun sendDiscord(user: User, status: IData<IStreamInfo>) { fun sendDiscord(user: User, status: IData<IStreamInfo?>) {
if(status.content == null) return
if(user.liveAlertMessage != "" && user.liveAlertGuild != null && user.liveAlertChannel != null) { if(user.liveAlertMessage != "" && user.liveAlertGuild != null && user.liveAlertChannel != null) {
val channel = getChannel(user.liveAlertGuild!!, user.liveAlertChannel!!) ?: throw RuntimeException("${user.liveAlertChannel} is not valid.") val channel = getChannel(user.liveAlertGuild!!, user.liveAlertChannel!!) ?: 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.content!!.liveTitle, "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}", status.content!!.channel.channelImageUrl)
embed.addField("카테고리", status.content.liveCategoryValue, true) embed.addField("카테고리", status.content!!.liveCategoryValue, true)
embed.addField("태그", status.content.tags.joinToString(", "), true) embed.addField("태그", status.content!!.tags.joinToString(", "), true)
embed.setImage(status.content.liveImageUrl.replace("{type}", "1080")) embed.setImage(status.content!!.liveImageUrl.replace("{type}", "1080"))
channel.sendMessage( channel.sendMessage(
MessageCreateBuilder() MessageCreateBuilder()

View File

@ -1,17 +0,0 @@
package space.mori.chzzk_bot.common.events
enum class GetUserType(var value: Int) {
REQUEST(0),
RESPONSE(1),
NOTFOUND(2)
}
class GetUserEvents(
val type: GetUserType,
val uid: String?,
val nickname: String?,
val isStreamOn: Boolean?,
val avatarUrl: String?,
): Event {
var TAG = javaClass.simpleName
}

View File

@ -1,4 +1,4 @@
package space.mori.chzzk_bot.chatbot.chzzk package space.mori.chzzk_bot.common.utils
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
@ -108,7 +108,7 @@ val client = OkHttpClient.Builder()
.build() .build()
val gson = Gson() val gson = Gson()
fun getFollowDate(chatID: String, userId: String) : IData<IFollowContent> { fun getFollowDate(chatID: String, userId: String) : IData<IFollowContent?> {
val url = "https://comm-api.game.naver.com/nng_main/v1/chats/$chatID/users/$userId/profile-card?chatType=STREAMING" val url = "https://comm-api.game.naver.com/nng_main/v1/chats/$chatID/users/$userId/profile-card?chatType=STREAMING"
val request = Request.Builder() val request = Request.Builder()
.url(url) .url(url)
@ -118,7 +118,7 @@ fun getFollowDate(chatID: String, userId: String) : IData<IFollowContent> {
try { try {
if(!response.isSuccessful) throw IOException("Unexpected code ${response.code}") if(!response.isSuccessful) throw IOException("Unexpected code ${response.code}")
val body = response.body?.string() val body = response.body?.string()
val follow = gson.fromJson(body, object: TypeToken<IData<IFollowContent>>() {}) val follow = gson.fromJson(body, object: TypeToken<IData<IFollowContent?>>() {})
return follow return follow
} catch(e: Exception) { } catch(e: Exception) {
@ -128,7 +128,7 @@ fun getFollowDate(chatID: String, userId: String) : IData<IFollowContent> {
} }
} }
fun getStreamInfo(userId: String) : IData<IStreamInfo> { fun getStreamInfo(userId: String) : IData<IStreamInfo?> {
val url = "https://api.chzzk.naver.com/service/v2/channels/${userId}/live-detail" val url = "https://api.chzzk.naver.com/service/v2/channels/${userId}/live-detail"
val request = Request.Builder() val request = Request.Builder()
.url(url) .url(url)
@ -138,7 +138,7 @@ fun getStreamInfo(userId: String) : IData<IStreamInfo> {
try { try {
if(!response.isSuccessful) throw IOException("Unexpected code ${response.code}") if(!response.isSuccessful) throw IOException("Unexpected code ${response.code}")
val body = response.body?.string() val body = response.body?.string()
val follow = gson.fromJson(body, object: TypeToken<IData<IStreamInfo>>() {}) val follow = gson.fromJson(body, object: TypeToken<IData<IStreamInfo?>>() {})
return follow return follow
} catch(e: Exception) { } catch(e: Exception) {

View File

@ -1,10 +1,8 @@
package space.mori.chzzk_bot.common.utils package space.mori.chzzk_bot.common.utils
import com.google.gson.Gson
import com.google.gson.JsonObject import com.google.gson.JsonObject
import io.github.cdimascio.dotenv.dotenv import io.github.cdimascio.dotenv.dotenv
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import java.io.IOException import java.io.IOException
@ -18,9 +16,6 @@ data class YoutubeVideo(
val regex = ".*(?:youtu.be/|v/|u/\\w/|embed/|watch\\?v=|&v=)([^#&?]*).*".toRegex() val regex = ".*(?:youtu.be/|v/|u/\\w/|embed/|watch\\?v=|&v=)([^#&?]*).*".toRegex()
val durationRegex = """PT(\d+H)?(\d+M)?(\d+S)?""".toRegex() val durationRegex = """PT(\d+H)?(\d+M)?(\d+S)?""".toRegex()
val client = OkHttpClient()
val gson = Gson()
val dotenv = dotenv { val dotenv = dotenv {
ignoreIfMissing = true ignoreIfMissing = true
} }

View File

@ -4,16 +4,8 @@ import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.koin.java.KoinJavaComponent.inject import space.mori.chzzk_bot.common.utils.getStreamInfo
import space.mori.chzzk_bot.common.events.CoroutinesEventBus
import space.mori.chzzk_bot.common.events.GetUserEvents
import space.mori.chzzk_bot.common.events.GetUserType
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
@Serializable @Serializable
data class GetUserDTO( data class GetUserDTO(
@ -23,33 +15,11 @@ data class GetUserDTO(
val avatarUrl: String, val avatarUrl: String,
) )
fun GetUserEvents.toDTO(): GetUserDTO {
return GetUserDTO(
this.uid!!,
this.nickname!!,
this.isStreamOn!!,
this.avatarUrl!!
)
}
fun Routing.apiRoutes() { fun Routing.apiRoutes() {
val dispatcher: CoroutinesEventBus by inject(CoroutinesEventBus::class.java)
val callMap = ConcurrentHashMap<String, ConcurrentLinkedQueue<ApplicationCall>>()
fun addCall(uid: String, call: ApplicationCall) {
callMap.computeIfAbsent(uid) { ConcurrentLinkedQueue() }.add(call)
}
fun removeCall(uid: String, call: ApplicationCall) {
callMap[uid]?.remove(call)
if(callMap[uid]?.isEmpty() == true) {
callMap.remove(uid)
}
}
route("/") { route("/") {
get { get {
call.respondText("Hello World!", status = HttpStatusCode.OK) call.respondText("Hello World!", status =
HttpStatusCode.OK)
} }
} }
route("/health") { route("/health") {
@ -57,38 +27,31 @@ fun Routing.apiRoutes() {
call.respondText("OK", status= HttpStatusCode.OK) call.respondText("OK", status= HttpStatusCode.OK)
} }
} }
route("/user/{uid}") {
get {
val uid = call.parameters["uid"]
if(uid == null) {
call.respondText("Require UID", status = HttpStatusCode.NotFound)
return@get
}
val user = getStreamInfo(uid)
if(user.content == null) {
call.respondText("User not found", status = HttpStatusCode.NotFound)
return@get
} else {
call.respond(HttpStatusCode.OK, GetUserDTO(
user.content!!.channel.channelId,
user.content!!.channel.channelName,
user.content!!.status == "OPEN",
user.content!!.channel.channelImageUrl
))
}
}
}
route("/user") { route("/user") {
get { get {
call.respondText("Require UID", status = HttpStatusCode.NotFound) call.respondText("Require UID", status = HttpStatusCode.NotFound)
} }
get("{uid}") {
val uid = call.parameters["uid"]
if(uid != null) {
addCall(uid, call)
if(!callMap.containsKey(uid)) {
CoroutineScope(Dispatchers.Default).launch {
dispatcher.post(GetUserEvents(GetUserType.REQUEST, null, null, null, null))
}
}
}
}
}
dispatcher.subscribe(GetUserEvents::class) {
if(it.type == GetUserType.REQUEST) return@subscribe
CoroutineScope(Dispatchers.Default). launch {
if (it.type == GetUserType.NOTFOUND) {
callMap[it.uid]?.forEach { call ->
call.respondText("User not found", status = HttpStatusCode.NotFound)
removeCall(it.uid ?: "", call)
}
return@launch
}
callMap[it.uid]?.forEach { call ->
call.respond(HttpStatusCode.OK, it.toDTO())
removeCall(it.uid ?: "", call)
}
}
} }
} }