mirror of
https://github.com/dalbodeule/chibot-chzzk-bot.git
synced 2025-08-08 05:11:12 +00:00
login method changed to chzzk login
This commit is contained in:
@@ -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)
|
||||
}
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user