mirror of
https://github.com/LamGC/oracle-manager.git
synced 2025-04-29 22:27:33 +00:00
418 lines
19 KiB
Kotlin
418 lines
19 KiB
Kotlin
package net.lamgc.scext.oraclemanager
|
||
|
||
import com.oracle.bmc.ConfigFileReader
|
||
import com.oracle.bmc.identity.IdentityClient
|
||
import com.oracle.bmc.model.BmcException
|
||
import mu.KotlinLogging
|
||
import org.apache.hc.client5.http.classic.methods.HttpGet
|
||
import org.apache.hc.client5.http.impl.classic.HttpClients
|
||
import org.apache.hc.core5.http.HttpHost
|
||
import org.telegram.abilitybots.api.bot.BaseAbilityBot
|
||
import org.telegram.abilitybots.api.objects.*
|
||
import org.telegram.abilitybots.api.util.AbilityExtension
|
||
import org.telegram.telegrambots.meta.api.methods.send.SendMessage
|
||
import org.telegram.telegrambots.meta.api.methods.updatingmessages.EditMessageReplyMarkup
|
||
import org.telegram.telegrambots.meta.api.methods.updatingmessages.EditMessageText
|
||
import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup
|
||
import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton
|
||
import java.io.ByteArrayInputStream
|
||
import java.security.interfaces.RSAPrivateCrtKey
|
||
|
||
private val httpClient = HttpClients.custom()
|
||
.setProxy(HttpHost("127.0.0.1", 1080))
|
||
.build()
|
||
|
||
@Suppress("unused")
|
||
class OracleAccountManagerExtension(private val bot: BaseAbilityBot) : AbilityExtension {
|
||
|
||
/**
|
||
* 添加 Oracle 租户并关联 Telegram 账号.
|
||
*/
|
||
fun addAccount(): Ability = Ability.builder()
|
||
.name("oc_account_add")
|
||
.info("关联新的 Oracle 账号")
|
||
.locality(Locality.USER)
|
||
.privacy(Privacy.PUBLIC)
|
||
.enableStats()
|
||
.action {
|
||
it.bot().silent().send("发送 API 配置文件,或复制配置文件内容直接发送。", it.chatId())
|
||
}
|
||
.reply(ReplyFlow.builder(bot.db())
|
||
.action { bot, upd ->
|
||
val configFile = if (upd.message.hasDocument()) {
|
||
val configUrl = bot.getFileUrl(upd.message.document.fileId)
|
||
val httpResponse = httpClient.execute(HttpGet(configUrl))
|
||
ConfigFileReader.parse(httpResponse.entity.content, "DEFAULT")
|
||
} else if (upd.message.hasText()) {
|
||
ConfigFileReader.parse(ByteArrayInputStream(upd.message.text.toByteArray()), "DEFAULT")
|
||
} else {
|
||
bot.silent().send("上传的配置不包括 DEFAULT 节。", upd.message.chatId)
|
||
return@action
|
||
}
|
||
if (!configFile.validate()) {
|
||
bot.silent().send("配置文件无效,请重新发送。", upd.message.chatId)
|
||
return@action
|
||
}
|
||
|
||
|
||
val profile = configFile.toOracleAccountProfile(upd.message.from.id)
|
||
val existProfile = OracleAccountManage.getOracleAccountByOracleUserId(profile.userId)
|
||
if (existProfile != null) {
|
||
bot.silent().send(
|
||
"Oracle 账号已经被${
|
||
if (existProfile.telegramUserId == upd.message.chatId) "你" else "其他人"
|
||
}绑定,请重新上传其他账号的 Oracle。", upd.message.chatId
|
||
)
|
||
return@action
|
||
}
|
||
|
||
if (!OracleAccessKeyManager.accessKeyContains(profile.keyFingerprint)) {
|
||
bot.db().getVar<String>("oc_account_add::${upd.message.chatId}::profile").set(
|
||
profile.toJsonString()
|
||
)
|
||
bot.silent().send(
|
||
"OK,配置文件检查通过,现在需要发送相应的私钥(机器人的所有人将对密钥的安全性负责)," +
|
||
"所需密钥的指纹是:\n${configFile["fingerprint"]}", upd.message.chatId
|
||
)
|
||
} else {
|
||
OracleAccountManage.addOracleAccount(profile)
|
||
bot.silent().execute(
|
||
createSuccessfulMsgWithChangeName(
|
||
"密钥已存在,Oracle 账号绑定成功!\n" +
|
||
"账号名称:${profile.name}",
|
||
upd.message.chatId, profile
|
||
)
|
||
)
|
||
}
|
||
}.onlyIf {
|
||
it.hasMessage() && (
|
||
it.message.hasDocument() &&
|
||
it.message.document.fileName.endsWith(".ini", ignoreCase = true)
|
||
||
|
||
it.message.hasText() && it.message.text.trim().startsWith(
|
||
"[DEFAULT]"
|
||
))
|
||
}
|
||
.enableStats(getStatsName("oc_account_add", "add_profile"))
|
||
.next(
|
||
ReplyFlow.builder(bot.db())
|
||
.action { bot, upd ->
|
||
try {
|
||
val privateKey = try {
|
||
val keyUrl = bot.getFileUrl(upd.message.document.fileId)
|
||
val response = httpClient.execute(HttpGet(keyUrl))
|
||
loadPkcs8PrivateKeyFromStream(response.entity.content)
|
||
} catch (e: Exception) {
|
||
logger.error(e) { "接收密钥文件时发生错误." }
|
||
bot.silent().send("接收密钥时发生错误,请重试一次。", upd.message.chatId)
|
||
return@action
|
||
}
|
||
if (privateKey !is RSAPrivateCrtKey) {
|
||
logger.warn { "用户上传的密钥不符合要求." }
|
||
bot.silent().send("密钥不符合要求,请重新生成密钥后重新添加 Oracle 账号。", upd.message.chatId)
|
||
return@action
|
||
}
|
||
|
||
val profileEntry = bot.db().getVar<String>("oc_account_add::${upd.message.chatId}::profile")
|
||
val profile = OracleAccountProfile.fromJson(profileEntry.get())
|
||
val uploadedKeyFingerprint = try {
|
||
getKeyPairFingerprint(privateKey)
|
||
} catch (e: Exception) {
|
||
logger.error(e) { "计算密钥指纹时发生错误." }
|
||
bot.silent().send("密钥指纹计算失败,请确保上传了正确的密钥。", upd.message.chatId)
|
||
return@action
|
||
}
|
||
if (!profile.keyFingerprint.contentEquals(uploadedKeyFingerprint, ignoreCase = true)) {
|
||
bot.silent().send("上传的私钥与认证配置的密钥指纹不符。", upd.message.chatId)
|
||
return@action
|
||
}
|
||
|
||
OracleAccessKeyManager.addAccessKey(privateKey)
|
||
OracleAccountManage.addOracleAccount(profile)
|
||
bot.silent().execute(
|
||
createSuccessfulMsgWithChangeName(
|
||
"密钥已确定,Oracle 账号绑定成功!\n" +
|
||
"账号名称:${profile.name}", upd.message.chatId, profile
|
||
)
|
||
)
|
||
profileEntry.set(null)
|
||
} catch (e: Exception) {
|
||
logger.error(e) { "处理密钥时发生错误." }
|
||
bot.silent().send("处理密钥时出现未知错误!请联系机器人管理员。", upd.message.chatId)
|
||
}
|
||
}
|
||
.onlyIf {
|
||
it.hasMessage() && bot.db().getVar<String>("oc_account_add::${it.message.chatId}::profile")
|
||
.get() != null &&
|
||
it.message.hasDocument() &&
|
||
it.message.document.fileName.endsWith(".pem", ignoreCase = true)
|
||
}
|
||
.enableStats(getStatsName("oc_account_add", "add_private_key"))
|
||
.build())
|
||
.build())
|
||
.build()
|
||
|
||
fun listOracleAccount(): Ability = Ability.builder()
|
||
.name("oc_account_list")
|
||
.info("列出关联的 Oracle 账号")
|
||
.privacy(Privacy.PUBLIC)
|
||
.locality(Locality.USER)
|
||
.action {
|
||
doListOracleAccount(it.bot(), it.chatId(), it.user().id)
|
||
}
|
||
.build()
|
||
|
||
private fun createSuccessfulMsgWithChangeName(
|
||
msg: String,
|
||
chatId: Long,
|
||
profile: OracleAccountProfile,
|
||
changeNameMsg: String = "点此更改机器人的名称"
|
||
): SendMessage {
|
||
val markup = InlineKeyboardGroupBuilder()
|
||
.configure {
|
||
}
|
||
.rowButton {
|
||
text(changeNameMsg)
|
||
callbackData(
|
||
action = "oc_account_change_name",
|
||
extraData = jsonObjectOf {
|
||
JsonFields.AccountProfile += profile
|
||
}
|
||
)
|
||
}
|
||
.build()
|
||
return SendMessage.builder()
|
||
.chatId(chatId.toString())
|
||
.text(msg)
|
||
.replyMarkup(markup)
|
||
.build()
|
||
}
|
||
|
||
fun listOracleAccountReply(): Reply = Reply.of({ bot, upd ->
|
||
doListOracleAccount(
|
||
bot,
|
||
upd.callbackQuery.message.chatId,
|
||
upd.callbackQuery.from.id,
|
||
upd.callbackQuery.message.messageId
|
||
)
|
||
}, callbackQueryAt("oc_account_list"))
|
||
|
||
private fun doListOracleAccount(bot: BaseAbilityBot, chatId: Long, userId: Long, messageId: Int? = null) {
|
||
val accounts = OracleAccountManage.getOracleAccountsByTelegramUserId(userId)
|
||
if (accounts.isEmpty()) {
|
||
bot.silent().send("你还没有绑定任何 Oracle 账号,请使用【/oc_account_add】绑定一个 Oracle 账号。", chatId)
|
||
return
|
||
}
|
||
// TODO: 要弄个页面, 防止账号太多刷爆了
|
||
val keyboardGroup = InlineKeyboardMarkup.builder()
|
||
for (account in accounts) {
|
||
val provider = account.getAuthenticationDetailsProvider()
|
||
val identityClient = IdentityClient(provider)
|
||
val text = try {
|
||
val user = identityClient.getUser(provider.userId)
|
||
"${account.name} / ${user.name}(${user.email})【${account.regionId}】"
|
||
} catch (e: BmcException) {
|
||
"${account.name} / null 【${account.regionId}】"
|
||
}
|
||
keyboardGroup.keyboardRow(
|
||
listOf(
|
||
InlineKeyboardButton.builder()
|
||
.text(text)
|
||
.callbackData(
|
||
action = "oc_account_manager",
|
||
extraData = jsonObjectOf {
|
||
JsonFields.AccountProfile += account
|
||
}
|
||
)
|
||
.build()
|
||
)
|
||
)
|
||
}
|
||
val text = """
|
||
当前 Telegram 用户已绑定以下 Oracle 账号
|
||
(账号没有名字只有邮箱是因为通过 API 获取名字失败)
|
||
""".trimIndent()
|
||
|
||
if (messageId == null) {
|
||
SendMessage.builder()
|
||
.chatId(chatId.toString())
|
||
.text(text)
|
||
.replyMarkup(keyboardGroup.build())
|
||
.build().execute(bot.silent())
|
||
} else {
|
||
EditMessageText.builder()
|
||
.text(text)
|
||
.messageId(messageId)
|
||
.chatId(chatId.toString())
|
||
.replyMarkup(keyboardGroup.build())
|
||
.build().execute(bot.silent())
|
||
}
|
||
}
|
||
|
||
fun manageOracleAccount(): Reply = Reply.of({ bot, upd ->
|
||
val keyboardCallback = upd.callbackQuery.callbackData
|
||
val profile = getProfileByCallback(upd.callbackQuery.callbackData)
|
||
val identityClient = IdentityClient(profile.getAuthenticationDetailsProvider())
|
||
val user = try {
|
||
identityClient.getUser(profile.userId)
|
||
} catch (e: Exception) {
|
||
logger.warn(e) { "Oracle 账号信息获取失败. (UserId: ${profile.userId})" }
|
||
null
|
||
}
|
||
val newKeyboardMarkup = InlineKeyboardGroupBuilder()
|
||
.rowButton {
|
||
text("服务器列表")
|
||
callbackData(keyboardCallback.next("oc_server_list"))
|
||
}
|
||
.newRow()
|
||
.addButton {
|
||
text("账号管理")
|
||
callbackData(keyboardCallback.next("oc_account_edit"))
|
||
}
|
||
.rowButton {
|
||
text("<<< 返回上一级")
|
||
callbackData(action = "oc_account_list")
|
||
}.build()
|
||
|
||
val editMessageText = EditMessageText.builder()
|
||
.chatId(upd.callbackQuery.message.chatId.toString())
|
||
.messageId(upd.callbackQuery.message.messageId)
|
||
.text(
|
||
"""
|
||
${profile.name}
|
||
账号名(甲骨文上面的账号名): ${user?.name}
|
||
区域 Id:${profile.regionId}
|
||
邮箱:${user?.email}
|
||
当前状态:${user?.lifecycleState?.value}
|
||
(账号名、邮箱和当前状态为 null 不一定是因为封号,也可能是服务器网络问题)
|
||
""".trimIndent()
|
||
)
|
||
.replyMarkup(newKeyboardMarkup)
|
||
.build()
|
||
bot.silent().execute(editMessageText)
|
||
}, callbackQueryAt("oc_account_manager"))
|
||
|
||
fun editOracleAccount(): Reply = Reply.of({ bot, upd ->
|
||
val keyboardCallback = upd.callbackQuery.callbackData
|
||
val newKeyboardMarkup = InlineKeyboardGroupBuilder()
|
||
.rowButton {
|
||
text("更改名称")
|
||
callbackData(action = "oc_account_change_name", keyboardCallback.extraData)
|
||
}
|
||
.rowButton {
|
||
text("解绑 Oracle 账号")
|
||
callbackData(action = "oc_account_remove", keyboardCallback.extraData)
|
||
}
|
||
.rowButton {
|
||
text("<<< 返回上一级")
|
||
callbackData(action = "oc_account_manager", keyboardCallback.extraData)
|
||
}.build()
|
||
|
||
val editMessageReplyMarkup = EditMessageReplyMarkup.builder()
|
||
.chatId(upd.callbackQuery.message.chatId.toString())
|
||
.messageId(upd.callbackQuery.message.messageId)
|
||
.replyMarkup(newKeyboardMarkup)
|
||
.build()
|
||
bot.silent().execute(editMessageReplyMarkup)
|
||
}, callbackQueryAt("oc_account_edit"))
|
||
|
||
fun removeOracleAccount(): Reply = ReplyFlow.builder(bot.db())
|
||
.action { bot, upd ->
|
||
val profile = getProfileByCallback(upd.callbackQuery.callbackData)
|
||
val keyboardCallback = upd.callbackQuery.callbackData
|
||
EditMessageText.builder()
|
||
.chatId(upd.callbackQuery.message.chatId.toString())
|
||
.messageId(upd.callbackQuery.message.messageId)
|
||
.text(
|
||
"""
|
||
${profile.name} (${profile.regionId})
|
||
你确定解绑这个 Oracle 账号吗?
|
||
""".trimIndent()
|
||
)
|
||
.replyMarkup(
|
||
createPromptKeyboard(
|
||
yesCallback = keyboardCallback.next("oc_account_remove_yes"),
|
||
noCallback = keyboardCallback.next("oc_account_edit")
|
||
)
|
||
)
|
||
.build()
|
||
.execute(bot.silent())
|
||
}
|
||
.onlyIf(callbackQueryAt("oc_account_remove"))
|
||
.next(Reply.of({ bot, upd ->
|
||
val profile = getProfileByCallback(upd.callbackQuery.callbackData)
|
||
val result =
|
||
OracleAccountManage.removeOracleAccountByOracleUserId(profile.userId, upd.callbackQuery.from.id)
|
||
val msg = if (result) {
|
||
"Oracle 账号 ${profile.userId} 已成功解除绑定。"
|
||
} else {
|
||
"Oracle 账号 ${profile.userId} 已成功解除绑定。"
|
||
}
|
||
EditMessageText.builder()
|
||
.text(msg)
|
||
.chatId(upd.callbackQuery.message.chatId.toString())
|
||
.messageId(upd.callbackQuery.message.messageId)
|
||
.replyMarkup(InlineKeyboardMarkup.builder().clearKeyboard().build())
|
||
.build()
|
||
.execute(bot.silent())
|
||
}, callbackQueryAt("oc_account_remove_yes")))
|
||
.build()
|
||
|
||
fun changeOracleAccountName(): Reply = ReplyFlow.builder(bot.db())
|
||
.enableStats(getStatsName("oc_account_change_name", "query_name"))
|
||
.action { bot, upd ->
|
||
if (upd.callbackQuery.data == null || upd.callbackQuery.data.trim().isEmpty()) {
|
||
logger.error { "存在未传递 Profile 的 CallbackQuery 路径,请检查!" }
|
||
bot.silent().send("出现未知错误,请联系机器人管理员。", upd.callbackQuery.message.chatId)
|
||
return@action
|
||
}
|
||
val profile = getProfileByCallback(upd.callbackQuery.callbackData)
|
||
val entryName = "oc_account_change_name::cache::" +
|
||
"chat_${upd.callbackQuery.message.chatId}::user_${upd.callbackQuery.from.id}::profile"
|
||
logger.debug { "询问名称 - Profile 键名称:$entryName" }
|
||
|
||
bot.db().getVar<String>(entryName).set(getProfileByCallback(upd.callbackQuery.callbackData).toJsonString())
|
||
bot.silent().send(
|
||
"当前机器人的名称为:\n${profile.name}\n请发送机器人的新名称。",
|
||
upd.callbackQuery.message.chatId
|
||
)
|
||
}
|
||
.onlyIf(callbackQueryAt("oc_account_change_name"))
|
||
.next(Reply.of({ bot, upd ->
|
||
val entryName = "oc_account_change_name::cache::" +
|
||
"chat_${upd.message.chatId}::user_${upd.message.from.id}::profile"
|
||
|
||
val profileJson = bot.db().getVar<String>(entryName).get()
|
||
if (profileJson == null || profileJson.trim().isEmpty()) {
|
||
bot.silent().send("会话已过期,请重试一次。", upd.message.chatId)
|
||
return@of
|
||
}
|
||
|
||
val profile = OracleAccountProfile.fromJson(profileJson)
|
||
profile.name = upd.message.text.trim()
|
||
try {
|
||
OracleAccountManage.updateOracleAccount(profile)
|
||
bot.silent().send("Oracle 账号名称已更新成功。", upd.message.chatId)
|
||
} catch (e: Exception) {
|
||
logger.error(e) { "更新 Oracle 账号时发生错误." }
|
||
bot.silent().send("更新 Oracle 账号名称时发生错误,请联系机器人管理员。", upd.message.chatId)
|
||
}
|
||
}, { upd -> upd.hasMessage() && upd.message.hasText() }))
|
||
.build()
|
||
|
||
fun clearUnusedAccessKey(): Ability = Ability.builder()
|
||
.name("oc_clear_key")
|
||
.info("清除未使用的 API 访问密钥")
|
||
.locality(Locality.USER)
|
||
.privacy(Privacy.ADMIN)
|
||
.action {
|
||
val count = OracleAccessKeyManager.cleanUnusedAccessKey()
|
||
it.bot().silent().send("已清理 $count 个未使用的访问密钥。", it.chatId())
|
||
}
|
||
.build()
|
||
|
||
companion object {
|
||
@JvmStatic
|
||
private val logger = KotlinLogging.logger { }
|
||
}
|
||
} |