Merge pull request #120 from dalbodeule/develop

login method changed to chzzk login
This commit is contained in:
JinU Choi 2025-01-08 23:17:45 +09:00 committed by GitHub
commit 65fae33467
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 175 additions and 118 deletions

View File

@ -39,7 +39,7 @@ object ChzzkHandler {
fun enable() {
botUid = chzzk.loggedUser.userId
UserService.getAllUsers().map {
if(it.token != null && !it.isDisabled)
if(!it.isDisabled)
chzzk.getChannel(it.token)?.let { token -> addUser(token, it) }
}

View File

@ -42,7 +42,7 @@ class MessageHandler(
if(it.type == SongType.STREAM_OFF) {
val user = UserService.getUser(channel.channelId)
if(! user?.let { usr -> SongListService.getSong(usr) }.isNullOrEmpty()) {
SongListService.deleteUser(user!!)
SongListService.deleteUser(user)
}
}
}
@ -161,7 +161,7 @@ class MessageHandler(
CoroutineScope(Dispatchers.Default).launch {
dispatcher.post(
TimerEvent(
user.token!!,
user.token,
TimerType.UPTIME,
getUptime(handler.streamStartTime!!)
)
@ -171,7 +171,7 @@ class MessageHandler(
"삭제" -> {
logger.debug("${user.token} / 삭제")
CoroutineScope(Dispatchers.Default).launch {
dispatcher.post(TimerEvent(user.token!!, TimerType.REMOVE, ""))
dispatcher.post(TimerEvent(user.token, TimerType.REMOVE, ""))
}
}
"설정" -> {
@ -195,7 +195,7 @@ class MessageHandler(
val timestamp = currentTime.plus(time.toLong(), ChronoUnit.MINUTES)
CoroutineScope(Dispatchers.Default).launch {
dispatcher.post(TimerEvent(user.token!!, TimerType.TIMER, timestamp.toString()))
dispatcher.post(TimerEvent(user.token, TimerType.TIMER, timestamp.toString()))
}
} catch (e: NumberFormatException) {
listener.sendChat("!타이머/숫자 형식으로 적어주세요! 단위: 분")
@ -267,7 +267,7 @@ class MessageHandler(
CoroutineScope(Dispatchers.Default).launch {
dispatcher.post(
SongEvent(
user.token!!,
user.token,
SongType.ADD,
msg.userId,
null,

View File

@ -8,9 +8,8 @@ import org.jetbrains.exposed.dao.id.IntIdTable
object Users: IntIdTable("users") {
val username = varchar("username", 255)
val token = varchar("token", 64).nullable()
val token = varchar("token", 64)
val discord = long("discord").nullable()
val naverId = varchar("naver_id", 128)
val liveAlertGuild = long("live_alert_guild").nullable()
val liveAlertChannel = long("live_alert_channel").nullable()
val liveAlertMessage = text("live_alert_message").nullable()
@ -24,7 +23,6 @@ class User(id: EntityID<Int>) : IntEntity(id) {
var username by Users.username
var token by Users.token
var discord by Users.discord
var naverId by Users.naverId
var liveAlertGuild by Users.liveAlertGuild
var liveAlertChannel by Users.liveAlertChannel
var liveAlertMessage by Users.liveAlertMessage

View File

@ -6,11 +6,11 @@ import space.mori.chzzk_bot.common.models.User
import space.mori.chzzk_bot.common.models.Users
object UserService {
fun saveUser(username: String, naverId: String): User {
fun saveUser(username: String, token: String): User {
return transaction {
User.new {
this.username = username
this.naverId = naverId
this.token = token
}
}
}
@ -64,14 +64,6 @@ object UserService {
}
}
fun getUserWithNaverId(naverId: String): User? {
return transaction {
val user = User.find{ Users.naverId eq naverId }.firstOrNull()
user?.load(User::subordinates, User::managers)
user
}
}
fun getAllUsers(): List<User> {
return transaction {
User.all().toList()

View File

@ -26,6 +26,8 @@ import space.mori.chzzk_bot.common.services.UserService
import space.mori.chzzk_bot.webserver.routes.*
import space.mori.chzzk_bot.webserver.utils.DiscordRatelimits
import wsSongListRoutes
import java.math.BigInteger
import java.security.SecureRandom
import java.time.Duration
import kotlin.time.toKotlinDuration
@ -33,14 +35,12 @@ val dotenv = dotenv {
ignoreIfMissing = true
}
const val naverMeAPIURL = "https://openapi.naver.com/v1/nid/me"
val redirects = mutableMapOf<String, String>()
val server = embeddedServer(Netty, port = 8080, ) {
install(WebSockets) {
pingPeriod = Duration.ofSeconds(15).toKotlinDuration()
timeout = Duration.ofSeconds(15).toKotlinDuration()
timeout = Duration.ofSeconds(100).toKotlinDuration()
maxFrameSize = Long.MAX_VALUE
masking = false
contentConverter = KotlinxWebsocketSerializationConverter(Json)
@ -56,26 +56,6 @@ val server = embeddedServer(Netty, port = 8080, ) {
cookie<UserSession>("user_session", storage = MariadbSessionStorage()) {}
}
install(Authentication) {
oauth("auth-oauth-naver") {
urlProvider = { "${dotenv["HOST"]}/auth/callback" }
providerLookup = { OAuthServerSettings.OAuth2ServerSettings(
name = "naver",
authorizeUrl = "https://nid.naver.com/oauth2.0/authorize",
accessTokenUrl = "https://nid.naver.com/oauth2.0/token",
requestMethod = HttpMethod.Post,
clientId = dotenv["NAVER_CLIENT_ID"],
clientSecret = dotenv["NAVER_CLIENT_SECRET"],
defaultScopes = listOf(""),
extraAuthParameters = listOf(),
onStateCreated = { call, state ->
//saves new state with redirect url value
call.request.queryParameters["redirectUrl"]?.let {
redirects[state] = it
}
}
)}
client = applicationHttpClient
}
oauth("auth-oauth-discord") {
urlProvider = { "${dotenv["HOST"]}/auth/callback/discord" }
providerLookup = { OAuthServerSettings.OAuth2ServerSettings(
@ -112,7 +92,7 @@ val server = embeddedServer(Netty, port = 8080, ) {
try {
val principal = call.principal<OAuthAccessTokenResponse.OAuth2>()
val session = call.sessions.get<UserSession>()
val user = session?.id?.let { UserService.getUserWithNaverId(it) }
val user = session?.id?.let { UserService.getUser(it) }
if(principal != null && session != null && user != null) {
try {
@ -150,31 +130,80 @@ val server = embeddedServer(Netty, port = 8080, ) {
}
// naver login
authenticate("auth-oauth-naver") {
get("/login") {
get("/login") {
val state = generateSecureRandomState()
// 세션에 상태 값 저장
call.sessions.set(UserSession(
state,
"",
listOf(),
))
// OAuth 제공자의 인증 URL 구성
val authUrl = URLBuilder("https://chzzk.naver.com/account-interlock").apply {
parameters.append("clientId", dotenv["NAVER_CLIENT_ID"]) // 비표준 파라미터 이름
parameters.append("redirectUri", "${dotenv["HOST"]}/auth/callback")
parameters.append("state", state)
// 추가적인 파라미터가 필요하면 여기에 추가
}.build().toString()
// 사용자에게 인증 페이지로 리다이렉트
call.respondRedirect(authUrl)
}
get("/callback") {
val receivedState = call.parameters["state"]
val code = call.parameters["code"]
// 세션에서 상태 값 가져오기
val session = call.sessions.get<UserSession>()
if (session == null || session.state != receivedState) {
call.respond(HttpStatusCode.BadRequest, "Invalid state parameter")
return@get
}
get("/callback") {
val currentPrincipal = call.principal<OAuthAccessTokenResponse.OAuth2>()
currentPrincipal?.let { principal ->
principal.state?.let { state ->
val userInfo: NaverAPI<NaverMeAPI> = applicationHttpClient.get(naverMeAPIURL) {
headers {
append(HttpHeaders.Authorization, "Bearer ${principal.accessToken}")
}
}.body()
call.sessions.set(userInfo.response?.let { profile ->
UserSession(state, profile.id, listOf())
})
if (code == null) {
call.respond(HttpStatusCode.BadRequest, "Missing code parameter")
return@get
}
try {
// Access Token 요청
val tokenRequest = TokenRequest(
grantType = "authorization_code",
state = session.state,
code = code,
clientId = dotenv["NAVER_CLIENT_ID"],
clientSecret = dotenv["NAVER_CLIENT_SECRET"]
)
redirects[state]?.let { redirect ->
call.respondRedirect(redirect)
return@get
}
}
val response = applicationHttpClient.post("https://chzzk.naver.com/auth/v1/token") {
contentType(ContentType.Application.Json)
setBody(tokenRequest)
}
call.respondRedirect(getFrontendURL(""))
val tokenResponse = response.body<TokenResponse>()
if(tokenResponse.content == null) {
call.respond(HttpStatusCode.InternalServerError, "Failed to obtain access token")
return@get
}
// Access Token 사용: 예를 들어, 사용자 정보 요청
val userInfo = getChzzkUser(tokenResponse.content.accessToken)
if(userInfo.content != null) {
call.sessions.set(
UserSession(
session.state,
userInfo.content.channelId,
listOf()
)
)
call.respondRedirect(getFrontendURL(""))
}
} catch (e: Exception) {
e.printStackTrace()
call.respond(HttpStatusCode.InternalServerError, "Failed to obtain access token")
}
}
@ -231,16 +260,33 @@ fun getFrontendURL(path: String)
data class UserSession(
val state: String,
val id: String,
val discordGuildList: List<String>
val discordGuildList: List<String>,
)
@Serializable
data class NaverMeAPI(
val id: String
data class TokenRequest(
val grantType: String,
val state: String,
val code: String,
val clientId: String,
val clientSecret: String
)
@Serializable
data class NaverAPI<T>(val resultcode: String, val message: String, val response: T?)
data class TokenResponse(
val code: Int,
val message: String?,
val content: TokenResponseBody?
)
@Serializable
data class TokenResponseBody(
val accessToken: String,
val tokenType: String,
val expiresIn: Int,
val refreshToken: String? = null
)
@Serializable
data class DiscordMeAPI(
@ -352,4 +398,32 @@ suspend fun getUserGuilds(accessToken: String): List<DiscordGuildListAPI> {
DiscordRatelimits.setRateLimit(rateLimit, remaining, resetAfter)
return response.body<List<DiscordGuildListAPI>>()
}
@Serializable
data class ChzzkMeApi(
val channelId: String,
val channelName: String,
val nickname: String,
)
@Serializable
data class ChzzkApi<T>(
val code: Int,
val message: String?,
val content: T?
)
suspend fun getChzzkUser(accessToken: String): ChzzkApi<ChzzkMeApi> {
val response = applicationHttpClient.get("https://openapi.chzzk.naver.com/open/v1/users/me") {
headers {
append(HttpHeaders.Authorization, "Bearer $accessToken")
}
}
return response.body<ChzzkApi<ChzzkMeApi>>()
}
fun generateSecureRandomState(): String {
return BigInteger(130, SecureRandom()).toString(32)
}

View File

@ -1,7 +1,6 @@
package space.mori.chzzk_bot.webserver.routes
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
@ -58,7 +57,7 @@ fun Routing.apiCommandRoutes() {
val managers = transaction {
user.managers.toList()
}
if(!managers.any { it.naverId == session?.id } && user.naverId != session?.id) {
if(!managers.any { it.token == session?.id } && user.token != session?.id) {
call.respond(HttpStatusCode.BadRequest, "User does not exist")
return@put
}
@ -70,7 +69,7 @@ fun Routing.apiCommandRoutes() {
)
CoroutineScope(Dispatchers.Default).launch {
for(i: Int in 0..3) {
dispatcher.post(CommandReloadEvent(user.token ?: ""))
dispatcher.post(CommandReloadEvent(user.token))
}
}
call.respond(HttpStatusCode.OK)
@ -93,7 +92,7 @@ fun Routing.apiCommandRoutes() {
val managers = transaction {
user.managers.toList()
}
if(!managers.any { it.naverId == session?.id } && user.naverId != session?.id) {
if(!managers.any { it.token == session?.id } && user.token != session?.id) {
call.respond(HttpStatusCode.BadRequest, "User does not exist")
return@post
}
@ -107,7 +106,7 @@ fun Routing.apiCommandRoutes() {
)
CoroutineScope(Dispatchers.Default).launch {
for(i: Int in 0..3) {
dispatcher.post(CommandReloadEvent(user.token ?: ""))
dispatcher.post(CommandReloadEvent(user.token))
}
}
call.respond(HttpStatusCode.OK)
@ -133,7 +132,7 @@ fun Routing.apiCommandRoutes() {
val managers = transaction {
user.managers.toList()
}
if(!managers.any { it.naverId == session?.id } && user.naverId != session?.id) {
if(!managers.any { it.token == session?.id } && user.token != session?.id) {
call.respond(HttpStatusCode.BadRequest, "User does not exist")
return@delete
}
@ -142,7 +141,7 @@ fun Routing.apiCommandRoutes() {
CommandService.removeCommand(user, commandRequest.label)
CoroutineScope(Dispatchers.Default).launch {
for(i: Int in 0..3) {
dispatcher.post(CommandReloadEvent(user.token ?: ""))
dispatcher.post(CommandReloadEvent(user.token))
}
}
call.respond(HttpStatusCode.OK)

View File

@ -1,7 +1,6 @@
package space.mori.chzzk_bot.webserver.routes
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
@ -34,7 +33,7 @@ fun Route.apiDiscordRoutes() {
val managers = transaction {
user.managers.toList()
}
if(!managers.any { it.naverId == session?.id } && user.naverId != session?.id) {
if(!managers.any { it.token == session?.id } && user.token != session?.id) {
call.respond(HttpStatusCode.BadRequest, "User does not exist")
return@get
}
@ -67,7 +66,7 @@ fun Route.apiDiscordRoutes() {
val managers = transaction {
user.managers.toList()
}
if(!managers.any { it.naverId == session?.id } && user.naverId != session?.id) {
if(!managers.any { it.token == session?.id } && user.token != session?.id) {
call.respond(HttpStatusCode.BadRequest, "User does not exist")
return@post
}
@ -86,7 +85,7 @@ fun Route.apiDiscordRoutes() {
call.respond(HttpStatusCode.BadRequest, "Session is required")
return@get
}
val user = UserService.getUserWithNaverId(session.id)
val user = UserService.getUser(session.id)
if(user == null) {
call.respond(HttpStatusCode.BadRequest, "User does not exist")
return@get

View File

@ -96,20 +96,20 @@ fun Routing.apiRoutes() {
call.respondText("No session found", status = HttpStatusCode.Unauthorized)
return@get
}
var user = UserService.getUserWithNaverId(session.id)
var user = UserService.getUser(session.id)
if(user == null) {
user = UserService.saveUser("임시닉네임", session.id)
}
val songConfig = SongConfigService.getConfig(user)
val status = user.token?.let { it1 -> getStreamInfo(it1) }
val status = getStreamInfo(user.token)
val returnUsers = mutableListOf<GetSessionDTO>()
if (user.username == "임시닉네임") {
status?.content?.channel?.let { it1 -> UserService.updateUser(user, it1.channelId, it1.channelName) }
status.content?.channel?.let { it1 -> UserService.updateUser(user, it1.channelId, it1.channelName) }
}
if(status?.content == null) {
call.respondText(user.naverId, status = HttpStatusCode.NotFound)
if(status.content == null) {
call.respondText(user.token, status = HttpStatusCode.NotFound)
return@get
}
@ -128,8 +128,8 @@ fun Routing.apiRoutes() {
user.subordinates.toList()
}
returnUsers.addAll(subordinates.map {
val subStatus = it.token?.let { token -> ChzzkUserCache.getCachedUser(token) }
return@map if (it.token == null || subStatus?.content == null) {
val subStatus = it.token.let { token -> ChzzkUserCache.getCachedUser(token) }
return@map if (subStatus?.content == null) {
null
} else {
GetSessionDTO(
@ -156,7 +156,7 @@ fun Routing.apiRoutes() {
val body: RegisterChzzkUserDTO = call.receive()
val user = UserService.getUserWithNaverId(session.id)
val user = UserService.getUser(session.id)
if(user == null) {
call.respondText("No session found", status = HttpStatusCode.Unauthorized)
return@post
@ -197,7 +197,7 @@ fun Routing.apiRoutes() {
return@get
}
val user = UserService.getUserWithNaverId(session.id)
val user = UserService.getUser(session.id)
if(user == null) {
call.respondText("No session found", status = HttpStatusCode.Unauthorized)
return@get
@ -216,7 +216,7 @@ fun Routing.apiRoutes() {
val body: GetSettingDTO = call.receive()
val user = UserService.getUserWithNaverId(session.id)
val user = UserService.getUser(session.id)
if(user == null) {
call.respondText("No session found", status = HttpStatusCode.Unauthorized)
return@post

View File

@ -1,7 +1,6 @@
package space.mori.chzzk_bot.webserver.routes
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
@ -31,7 +30,7 @@ fun Routing.apiTimerRoutes() {
val managers = transaction {
user.managers.toList()
}
if(!managers.any { it.naverId == session?.id } && user.naverId != session?.id) {
if(!managers.any { it.token == session?.id } && user.token != session?.id) {
call.respond(HttpStatusCode.BadRequest, "User does not exist")
return@get
}
@ -58,7 +57,7 @@ fun Routing.apiTimerRoutes() {
val managers = transaction {
user.managers.toList()
}
if(!managers.any { it.naverId == session?.id } && user.naverId != session?.id) {
if(!managers.any { it.token == session?.id } && user.token != session?.id) {
call.respond(HttpStatusCode.BadRequest, "User does not exist")
return@put
}

View File

@ -109,7 +109,7 @@ fun Routing.wsSongListRoutes() {
webSocket("/songlist") {
val session = call.sessions.get<UserSession>()
val user = session?.id?.let { UserService.getUserWithNaverId(it) }
val user = session?.id?.let { UserService.getUser(it) }
if (user == null) {
close(CloseReason(CloseReason.Codes.CANNOT_ACCEPT, "Invalid SID"))
return@webSocket
@ -117,7 +117,7 @@ fun Routing.wsSongListRoutes() {
val uid = user.token
addSession(uid!!, this)
addSession(uid, this)
if (status[uid] == SongType.STREAM_OFF) {
CoroutineScope(Dispatchers.Default).launch {
@ -161,18 +161,16 @@ fun Routing.wsSongListRoutes() {
CoroutineScope(Dispatchers.Default).launch {
val user = UserService.getUser(it.uid)
if (user != null) {
user.token?.let { token ->
sendWithRetry(
token, SongResponse(
it.type.value,
it.uid,
it.reqUid,
it.current?.toSerializable(),
it.next?.toSerializable(),
it.delUrl
)
sendWithRetry(
user.token, SongResponse(
it.type.value,
it.uid,
it.reqUid,
it.current?.toSerializable(),
it.next?.toSerializable(),
it.delUrl
)
}
)
}
}
}
@ -182,17 +180,15 @@ fun Routing.wsSongListRoutes() {
CoroutineScope(Dispatchers.Default).launch {
val user = UserService.getUser(it.uid)
if (user != null) {
user.token?.let { token ->
sendWithRetry(
token, SongResponse(
it.type.value,
it.uid,
null,
null,
null,
)
sendWithRetry(
user.token, SongResponse(
it.type.value,
it.uid,
null,
null,
null,
)
}
)
}
}
}
@ -219,7 +215,7 @@ suspend fun handleSongRequest(
CoroutineScope(Dispatchers.Default).launch {
SongListService.saveSong(
user,
user.token!!,
user.token,
url,
youtubeVideo.name,
youtubeVideo.author,
@ -228,7 +224,7 @@ suspend fun handleSongRequest(
)
dispatcher.post(
SongEvent(
user.token!!,
user.token,
SongType.ADD,
user.token,
CurrentSong.getSong(user),
@ -251,7 +247,7 @@ suspend fun handleSongRequest(
}
dispatcher.post(
SongEvent(
user.token!!,
user.token,
SongType.REMOVE,
null,
null,
@ -281,7 +277,7 @@ suspend fun handleSongRequest(
}
dispatcher.post(
SongEvent(
user.token!!,
user.token,
SongType.NEXT,
song?.uid,
youtubeVideo