feat: add mclogin command
This commit is contained in:
13 files changed
+331
-4
No files matched your search
@@ -80,7 +80,8 @@ val DEFAULT_CONFIG = Config(
|
||||
cloudflareR2BucketName = "fancybot",
|
||||
cloudflareR2PublicUrl = "https://114514.com",
|
||||
apiSpaceKey = "114514",
|
||||
apiRtastKey = "114514"
|
||||
apiRtastKey = "114514",
|
||||
azureAppClientId = "114514"
|
||||
)
|
||||
|
||||
val configManager = ConfigManager()
|
||||
@@ -123,5 +124,5 @@ val commands = listOf(
|
||||
ZDJDCommand(), ScanQRCodeCommand(), TTSCommand(), SlimeChunkHelperCommand(),
|
||||
SupportMeCommand(), TheCatCommand(), TheHistoryOfTodayCommand(), MCVersionCommand(),
|
||||
MinecraftWikiCommand(), GaoKaoDaysRemainCommand(), IdiomExplainCommand(), NiuziManagerCommand(),
|
||||
TodayEatCommand(), TodayDrinkCommand()
|
||||
TodayEatCommand(), TodayDrinkCommand(), MCLoginCommand()
|
||||
)
|
||||
@@ -11,12 +11,21 @@ package cn.rtast.fancybot.commands.lookup
|
||||
import cn.rtast.fancybot.annotations.CommandDescription
|
||||
import cn.rtast.fancybot.commands.lookup.WikipediaCommand.Companion.extractPlainTextFromHtml
|
||||
import cn.rtast.fancybot.configManager
|
||||
import cn.rtast.fancybot.db.deleteAccessTokenById
|
||||
import cn.rtast.fancybot.db.getAccessTokenById
|
||||
import cn.rtast.fancybot.db.insertNewAccessToken
|
||||
import cn.rtast.fancybot.entity.mc.DecodedSkin
|
||||
import cn.rtast.fancybot.entity.mc.MCVersion
|
||||
import cn.rtast.fancybot.entity.mc.MCWikiPageResponse
|
||||
import cn.rtast.fancybot.entity.mc.MCWikiSearchResponse
|
||||
import cn.rtast.fancybot.entity.mc.Skin
|
||||
import cn.rtast.fancybot.entity.mc.Username
|
||||
import cn.rtast.fancybot.entity.mc.oauth.DeviceCodeResponse
|
||||
import cn.rtast.fancybot.entity.mc.oauth.LoginXboxLivePayload
|
||||
import cn.rtast.fancybot.entity.mc.oauth.LoginXboxLiveResponse
|
||||
import cn.rtast.fancybot.entity.mc.oauth.ObtainXSTSPayload
|
||||
import cn.rtast.fancybot.entity.mc.oauth.ObtainXSTSResponse
|
||||
import cn.rtast.fancybot.entity.mc.oauth.RedeemCodeResponse
|
||||
import cn.rtast.fancybot.enums.mc.MCVersionType
|
||||
import cn.rtast.fancybot.util.Http
|
||||
import cn.rtast.fancybot.util.Logger
|
||||
@@ -28,15 +37,18 @@ import cn.rtast.fancybot.util.str.convertToHTML
|
||||
import cn.rtast.fancybot.util.str.decodeToString
|
||||
import cn.rtast.fancybot.util.str.encodeToBase64
|
||||
import cn.rtast.fancybot.util.str.fromJson
|
||||
import cn.rtast.fancybot.util.str.toJson
|
||||
import cn.rtast.fancybot.util.str.uriEncode
|
||||
import cn.rtast.motdpinger.BedrockPing
|
||||
import cn.rtast.motdpinger.JavaPing
|
||||
import cn.rtast.rob.entity.GroupMessage
|
||||
import cn.rtast.rob.entity.PrivateMessage
|
||||
import cn.rtast.rob.util.BaseCommand
|
||||
import cn.rtast.rob.util.ob.MessageChain
|
||||
import cn.rtast.rob.util.ob.OneBotListener
|
||||
import cn.rtast.rob.util.ob.asMessageChain
|
||||
import cn.rtast.rob.util.ob.asNode
|
||||
import okhttp3.FormBody
|
||||
import java.awt.AlphaComposite
|
||||
import java.awt.Color
|
||||
import java.awt.image.BufferedImage
|
||||
@@ -375,4 +387,119 @@ class MCPingCommand : BaseCommand() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MCLoginCommand : BaseCommand() {
|
||||
override val commandNames = listOf("/mclogin")
|
||||
|
||||
private val deviceCodeUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"
|
||||
private val redeemCodeUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"
|
||||
private val xboxLiveAuthUrl = "https://user.auth.xboxlive.com/user/authenticate"
|
||||
private val obtainXSTSUrl = "https://xsts.auth.xboxlive.com/xsts/authorize"
|
||||
private val clientId = configManager.azureAppClientId
|
||||
private val waitingList = mutableListOf<Long>()
|
||||
private val deviceCodeMap = mutableMapOf<Long, String>()
|
||||
|
||||
/**
|
||||
* 获取user code 和device code 和验证url
|
||||
*/
|
||||
private fun getDeviceCode(): Triple<String, String, String> {
|
||||
val form = FormBody.Builder()
|
||||
.add("client_id", clientId)
|
||||
.add("scope", "XboxLive.signin")
|
||||
.build()
|
||||
val response = Http.post<DeviceCodeResponse>(deviceCodeUrl, form)
|
||||
return Triple(response.userCode, response.deviceCode, response.verificationUri)
|
||||
}
|
||||
|
||||
/**
|
||||
* 将这个code兑换成一个access token
|
||||
*/
|
||||
private fun redeemCode(deviceCode: String): String {
|
||||
val form = FormBody.Builder()
|
||||
.add("client_id", clientId)
|
||||
.add("grant_type", "urn:ietf:params:oauth:grant-type:device_code")
|
||||
.add("device_code", deviceCode)
|
||||
.build()
|
||||
val response = Http.post<RedeemCodeResponse>(redeemCodeUrl, form)
|
||||
return response.accessToken
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录Xbox
|
||||
*/
|
||||
private fun loginXboxLive(accessToken: String): Pair<String, String> {
|
||||
val payload = LoginXboxLivePayload(properties = LoginXboxLivePayload.Properties("d=$accessToken"))
|
||||
val response = Http.post<LoginXboxLiveResponse>(xboxLiveAuthUrl, payload.toJson())
|
||||
val token = response.token
|
||||
val uhs = response.displayClaims.xui.first().uhs
|
||||
return token to uhs
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最后的mc access token, 这个access token可以直接用于登录游戏
|
||||
*/
|
||||
private fun obtainXSTSToken(token: String, uhs: String): String {
|
||||
val payload = ObtainXSTSPayload(properties = ObtainXSTSPayload.Properties(listOf(token)))
|
||||
val response = Http.post<ObtainXSTSResponse>(obtainXSTSUrl, payload.toJson())
|
||||
return response.token
|
||||
}
|
||||
|
||||
override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List<String>) {
|
||||
if (args.isEmpty()) {
|
||||
message.reply("这个命令可以获取你的MC登录Token发送 `/mclogin login`进行操作吧")
|
||||
return
|
||||
}
|
||||
deleteAccessTokenById(message.sender.userId)
|
||||
val action = args.first()
|
||||
if (action == "确认" || action == "confirm") {
|
||||
if (message.sender.userId !in waitingList) {
|
||||
message.reply("你还没开始登陆呢")
|
||||
} else {
|
||||
waitingList.removeIf { it == message.sender.userId }
|
||||
val deviceCode = deviceCodeMap[message.sender.userId]!!
|
||||
deviceCodeMap.remove(message.sender.userId)
|
||||
val accessToken = this.redeemCode(deviceCode)
|
||||
val (token, uhs) = this.loginXboxLive(accessToken)
|
||||
val minecraftAccessToken = this.obtainXSTSToken(token, uhs)
|
||||
insertNewAccessToken(minecraftAccessToken, message.sender.userId)
|
||||
message.reply("你的账号token已经被保存到了数据库中, 请加机器人好友然后发送`/mclogin`来获取这个Token, Token会在你获取之后从数据库中删除")
|
||||
}
|
||||
} else {
|
||||
if (message.sender.userId !in waitingList) {
|
||||
waitingList.add(message.sender.userId)
|
||||
val deviceInfo = getDeviceCode()
|
||||
val msg = MessageChain.Builder()
|
||||
.addText("你的用户代码是: ${deviceInfo.first}")
|
||||
.addNewLine()
|
||||
.addText("请前往: ${deviceInfo.third}进行验证")
|
||||
.addNewLine()
|
||||
.addText("登陆完成后发送`/mclogin confirm` 或 `/mclogin 确认`")
|
||||
.build()
|
||||
deviceCodeMap[message.sender.userId] = deviceInfo.second
|
||||
message.reply(msg)
|
||||
} else {
|
||||
waitingList.removeIf { it == message.sender.userId }
|
||||
deviceCodeMap.remove(message.sender.userId)
|
||||
message.reply("回复错误已结束本次登录流程")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun executePrivate(listener: OneBotListener, message: PrivateMessage, args: List<String>) {
|
||||
val token = getAccessTokenById(message.sender.userId)
|
||||
if (token == null) {
|
||||
message.reply("你还没登录呢!")
|
||||
} else {
|
||||
val msg = MessageChain.Builder()
|
||||
.addText("你的AccessToken如下: ")
|
||||
.addNewLine()
|
||||
.addText(token.token)
|
||||
.addNewLine()
|
||||
.addText("这个Token是敏感信息请妥善保管")
|
||||
.build()
|
||||
deleteAccessTokenById(message.sender.userId)
|
||||
message.reply(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright © 2024 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2024/10/11
|
||||
*/
|
||||
|
||||
|
||||
package cn.rtast.fancybot.db
|
||||
|
||||
import cn.rtast.fancybot.db.MinecraftAccessTokenTable.timestamp
|
||||
import cn.rtast.fancybot.db.MinecraftAccessTokenTable.token
|
||||
import cn.rtast.fancybot.db.MinecraftAccessTokenTable.userId
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import cn.rtast.fancybot.util.suspendedTransaction
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import java.time.Instant
|
||||
|
||||
object MinecraftAccessTokenTable : Table("mc_access_token") {
|
||||
val id = integer("id").autoIncrement()
|
||||
val token = text("token")
|
||||
val userId = long("user_id")
|
||||
val timestamp = long("timestamp")
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
}
|
||||
|
||||
data class MCAccessTokenEntity(
|
||||
val token: String,
|
||||
val userId: Long,
|
||||
val timestamp: Long
|
||||
)
|
||||
|
||||
suspend fun insertNewAccessToken(accessToken: String, id: Long) {
|
||||
suspendedTransaction {
|
||||
MinecraftAccessTokenTable.insert {
|
||||
it[token] = accessToken
|
||||
it[userId] = id
|
||||
it[timestamp] = Instant.now().epochSecond
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getAccessTokenById(id: Long): MCAccessTokenEntity? {
|
||||
return suspendedTransaction {
|
||||
MinecraftAccessTokenTable.selectAll()
|
||||
.where { userId eq id }
|
||||
.map {
|
||||
MCAccessTokenEntity(
|
||||
it[token],
|
||||
it[userId],
|
||||
it[timestamp]
|
||||
)
|
||||
}.singleOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteAccessTokenById(id: Long) {
|
||||
suspendedTransaction {
|
||||
MinecraftAccessTokenTable.deleteWhere { userId eq id }
|
||||
}
|
||||
}
|
||||
@@ -46,5 +46,6 @@ data class Config(
|
||||
val cloudflareR2BucketName: String,
|
||||
val cloudflareR2PublicUrl: String,
|
||||
val apiSpaceKey: String,
|
||||
val apiRtastKey: String
|
||||
val apiRtastKey: String,
|
||||
val azureAppClientId: String,
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright © 2024 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2024/10/11
|
||||
*/
|
||||
|
||||
|
||||
package cn.rtast.fancybot.entity.mc.oauth
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class DeviceCodeResponse(
|
||||
@SerializedName("user_code")
|
||||
val userCode: String,
|
||||
@SerializedName("device_code")
|
||||
val deviceCode: String,
|
||||
@SerializedName("verification_uri")
|
||||
val verificationUri: String
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright © 2024 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2024/10/11
|
||||
*/
|
||||
|
||||
|
||||
package cn.rtast.fancybot.entity.mc.oauth
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class LoginXboxLivePayload(
|
||||
@SerializedName("Properties")
|
||||
val properties: Properties,
|
||||
@SerializedName("RelyingParty")
|
||||
val relyingParty: String = "http://auth.xboxlive.com",
|
||||
@SerializedName("TokenType")
|
||||
val tokenType: String = "JWT",
|
||||
) {
|
||||
data class Properties(
|
||||
@SerializedName("RpsTicket")
|
||||
val rpsTicket: String,
|
||||
@SerializedName("AuthMethod")
|
||||
val authMethod: String = "RPS",
|
||||
@SerializedName("SiteName")
|
||||
val siteName: String = "user.auth.xboxlive.com",
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright © 2024 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2024/10/11
|
||||
*/
|
||||
|
||||
|
||||
package cn.rtast.fancybot.entity.mc.oauth
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class LoginXboxLiveResponse(
|
||||
@SerializedName("Token")
|
||||
val token: String,
|
||||
@SerializedName("DisplayClaims")
|
||||
val displayClaims: DisplayClaims,
|
||||
) {
|
||||
data class DisplayClaims(
|
||||
val xui: List<XUI>
|
||||
)
|
||||
|
||||
data class XUI(
|
||||
val uhs: String
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright © 2024 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2024/10/11
|
||||
*/
|
||||
|
||||
|
||||
package cn.rtast.fancybot.entity.mc.oauth
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class ObtainXSTSPayload(
|
||||
@SerializedName("Properties")
|
||||
val properties: Properties,
|
||||
@SerializedName("RelyingParty")
|
||||
val relyingParty: String = "rp://api.minecraftservices.com/",
|
||||
@SerializedName("TokenType")
|
||||
val tokenType: String = "JWT",
|
||||
) {
|
||||
data class Properties(
|
||||
@SerializedName("UserTokens")
|
||||
val userTokens: List<String>,
|
||||
@SerializedName("SandboxId")
|
||||
val sandboxId: String = "RETAIL",
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright © 2024 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2024/10/11
|
||||
*/
|
||||
|
||||
|
||||
package cn.rtast.fancybot.entity.mc.oauth
|
||||
|
||||
import cn.rtast.fancybot.entity.mc.oauth.LoginXboxLiveResponse.DisplayClaims
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class ObtainXSTSResponse(
|
||||
@SerializedName("Token")
|
||||
val token: String,
|
||||
@SerializedName("DisplayClaims")
|
||||
val displayClaims: DisplayClaims,
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright © 2024 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2024/10/11
|
||||
*/
|
||||
|
||||
|
||||
package cn.rtast.fancybot.entity.mc.oauth
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class RedeemCodeResponse(
|
||||
@SerializedName("access_token")
|
||||
val accessToken: String,
|
||||
)
|
||||
@@ -11,6 +11,7 @@ import cn.rtast.fancybot.ROOT_PATH
|
||||
import cn.rtast.fancybot.db.ActionTable
|
||||
import cn.rtast.fancybot.db.BlackListTable
|
||||
import cn.rtast.fancybot.db.JrrpTable
|
||||
import cn.rtast.fancybot.db.MinecraftAccessTokenTable
|
||||
import cn.rtast.fancybot.db.NiuziBankTable
|
||||
import cn.rtast.fancybot.db.NiuziTable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -26,6 +27,7 @@ suspend fun initDatabase() {
|
||||
SchemaUtils.createMissingTablesAndColumns(NiuziTable)
|
||||
SchemaUtils.createMissingTablesAndColumns(ActionTable)
|
||||
SchemaUtils.createMissingTablesAndColumns(BlackListTable)
|
||||
SchemaUtils.createMissingTablesAndColumns(MinecraftAccessTokenTable)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,4 +47,5 @@ class ConfigManager : JsonFileHandler<Config>("config.json") {
|
||||
val cloudflareR2PublicUrl = config.cloudflareR2PublicUrl
|
||||
val apiSpaceKey = config.apiSpaceKey
|
||||
val apiRtastKey = config.apiRtastKey
|
||||
val azureAppClientId = config.azureAppClientId
|
||||
}
|
||||
@@ -38,5 +38,6 @@
|
||||
"cloudflareR2BucketName": "fancybot",
|
||||
"cloudflareR2PublicUrl": "https://114514.com",
|
||||
"apiSpaceKey": "114514",
|
||||
"apiRtastKey": "114514"
|
||||
"apiRtastKey": "114514",
|
||||
"azureAppClientId": "114514"
|
||||
}
|
||||
Reference in New Issue
Block a user