feat: add mclogin command

This commit is contained in:
2024-10-11 20:49:32 +08:00
parent 2ac7d3b20b
commit 217364ffb8
13 files changed
+331 -4

No files matched your search

+3 -2
View File
@@ -80,7 +80,8 @@ val DEFAULT_CONFIG = Config(
cloudflareR2BucketName = "fancybot", cloudflareR2BucketName = "fancybot",
cloudflareR2PublicUrl = "https://114514.com", cloudflareR2PublicUrl = "https://114514.com",
apiSpaceKey = "114514", apiSpaceKey = "114514",
apiRtastKey = "114514" apiRtastKey = "114514",
azureAppClientId = "114514"
) )
val configManager = ConfigManager() val configManager = ConfigManager()
@@ -123,5 +124,5 @@ val commands = listOf(
ZDJDCommand(), ScanQRCodeCommand(), TTSCommand(), SlimeChunkHelperCommand(), ZDJDCommand(), ScanQRCodeCommand(), TTSCommand(), SlimeChunkHelperCommand(),
SupportMeCommand(), TheCatCommand(), TheHistoryOfTodayCommand(), MCVersionCommand(), SupportMeCommand(), TheCatCommand(), TheHistoryOfTodayCommand(), MCVersionCommand(),
MinecraftWikiCommand(), GaoKaoDaysRemainCommand(), IdiomExplainCommand(), NiuziManagerCommand(), 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.annotations.CommandDescription
import cn.rtast.fancybot.commands.lookup.WikipediaCommand.Companion.extractPlainTextFromHtml import cn.rtast.fancybot.commands.lookup.WikipediaCommand.Companion.extractPlainTextFromHtml
import cn.rtast.fancybot.configManager 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.DecodedSkin
import cn.rtast.fancybot.entity.mc.MCVersion import cn.rtast.fancybot.entity.mc.MCVersion
import cn.rtast.fancybot.entity.mc.MCWikiPageResponse import cn.rtast.fancybot.entity.mc.MCWikiPageResponse
import cn.rtast.fancybot.entity.mc.MCWikiSearchResponse import cn.rtast.fancybot.entity.mc.MCWikiSearchResponse
import cn.rtast.fancybot.entity.mc.Skin import cn.rtast.fancybot.entity.mc.Skin
import cn.rtast.fancybot.entity.mc.Username 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.enums.mc.MCVersionType
import cn.rtast.fancybot.util.Http import cn.rtast.fancybot.util.Http
import cn.rtast.fancybot.util.Logger 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.decodeToString
import cn.rtast.fancybot.util.str.encodeToBase64 import cn.rtast.fancybot.util.str.encodeToBase64
import cn.rtast.fancybot.util.str.fromJson import cn.rtast.fancybot.util.str.fromJson
import cn.rtast.fancybot.util.str.toJson
import cn.rtast.fancybot.util.str.uriEncode import cn.rtast.fancybot.util.str.uriEncode
import cn.rtast.motdpinger.BedrockPing import cn.rtast.motdpinger.BedrockPing
import cn.rtast.motdpinger.JavaPing import cn.rtast.motdpinger.JavaPing
import cn.rtast.rob.entity.GroupMessage import cn.rtast.rob.entity.GroupMessage
import cn.rtast.rob.entity.PrivateMessage
import cn.rtast.rob.util.BaseCommand import cn.rtast.rob.util.BaseCommand
import cn.rtast.rob.util.ob.MessageChain import cn.rtast.rob.util.ob.MessageChain
import cn.rtast.rob.util.ob.OneBotListener import cn.rtast.rob.util.ob.OneBotListener
import cn.rtast.rob.util.ob.asMessageChain import cn.rtast.rob.util.ob.asMessageChain
import cn.rtast.rob.util.ob.asNode import cn.rtast.rob.util.ob.asNode
import okhttp3.FormBody
import java.awt.AlphaComposite import java.awt.AlphaComposite
import java.awt.Color import java.awt.Color
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
@@ -376,3 +388,118 @@ 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 cloudflareR2BucketName: String,
val cloudflareR2PublicUrl: String, val cloudflareR2PublicUrl: String,
val apiSpaceKey: 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.ActionTable
import cn.rtast.fancybot.db.BlackListTable import cn.rtast.fancybot.db.BlackListTable
import cn.rtast.fancybot.db.JrrpTable import cn.rtast.fancybot.db.JrrpTable
import cn.rtast.fancybot.db.MinecraftAccessTokenTable
import cn.rtast.fancybot.db.NiuziBankTable import cn.rtast.fancybot.db.NiuziBankTable
import cn.rtast.fancybot.db.NiuziTable import cn.rtast.fancybot.db.NiuziTable
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -26,6 +27,7 @@ suspend fun initDatabase() {
SchemaUtils.createMissingTablesAndColumns(NiuziTable) SchemaUtils.createMissingTablesAndColumns(NiuziTable)
SchemaUtils.createMissingTablesAndColumns(ActionTable) SchemaUtils.createMissingTablesAndColumns(ActionTable)
SchemaUtils.createMissingTablesAndColumns(BlackListTable) SchemaUtils.createMissingTablesAndColumns(BlackListTable)
SchemaUtils.createMissingTablesAndColumns(MinecraftAccessTokenTable)
} }
} }
@@ -47,4 +47,5 @@ class ConfigManager : JsonFileHandler<Config>("config.json") {
val cloudflareR2PublicUrl = config.cloudflareR2PublicUrl val cloudflareR2PublicUrl = config.cloudflareR2PublicUrl
val apiSpaceKey = config.apiSpaceKey val apiSpaceKey = config.apiSpaceKey
val apiRtastKey = config.apiRtastKey val apiRtastKey = config.apiRtastKey
val azureAppClientId = config.azureAppClientId
} }
+2 -1
View File
@@ -38,5 +38,6 @@
"cloudflareR2BucketName": "fancybot", "cloudflareR2BucketName": "fancybot",
"cloudflareR2PublicUrl": "https://114514.com", "cloudflareR2PublicUrl": "https://114514.com",
"apiSpaceKey": "114514", "apiSpaceKey": "114514",
"apiRtastKey": "114514" "apiRtastKey": "114514",
"azureAppClientId": "114514"
} }