@file:Suppress("unused") package net.lamgc.scext.oraclemanager import com.google.common.base.Strings import com.google.common.cache.Cache import com.google.common.cache.CacheBuilder import com.google.gson.Gson import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.annotations.SerializedName import com.oracle.bmc.ConfigFileReader.ConfigFile import com.oracle.bmc.OCID import com.oracle.bmc.Region import com.oracle.bmc.auth.AuthenticationDetailsProvider import org.apache.hc.core5.http.HttpEntityContainer import org.apache.hc.core5.http.HttpResponse import org.bouncycastle.asn1.pkcs.PrivateKeyInfo import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator import org.bouncycastle.util.io.pem.PemWriter import org.telegram.abilitybots.api.bot.BaseAbilityBot import org.telegram.abilitybots.api.objects.Reply import org.telegram.abilitybots.api.sender.MessageSender import org.telegram.abilitybots.api.sender.SilentSender import org.telegram.telegrambots.meta.api.methods.BotApiMethod import org.telegram.telegrambots.meta.api.methods.GetFile import org.telegram.telegrambots.meta.api.objects.CallbackQuery import org.telegram.telegrambots.meta.api.objects.Update import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup.InlineKeyboardMarkupBuilder import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton.InlineKeyboardButtonBuilder import org.telegram.telegrambots.meta.bots.AbsSender import java.io.File import java.io.InputStream import java.io.StringWriter import java.nio.charset.StandardCharsets import java.security.KeyFactory import java.security.MessageDigest import java.security.PrivateKey import java.security.interfaces.RSAPrivateCrtKey import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.RSAPublicKeySpec import java.util.* import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicReference import java.util.function.Predicate import java.util.regex.Pattern fun BaseAbilityBot.getFileUrl(fileId: String, apiServer: String = "https://api.telegram.org"): String { val file = execute(GetFile(fileId)) return "$apiServer/file/bot${botToken}/${file.filePath}" } fun T.isSuccess(): Boolean = code == 200 fun T.hasContent(): Boolean = this is HttpEntityContainer fun T.validate(): Boolean { return OCID.isValid(userId) && OCID.isValid(tenantId) && !Strings.isNullOrEmpty(fingerprint) && fingerprintCheckPattern.matcher(fingerprint).matches() } private val fingerprintCheckPattern = Pattern.compile("^[\\da-zA-Z]{2}(:[\\da-zA-Z]{2}){15}\$") fun ConfigFile.validate(requireKeyPath: Boolean = false): Boolean { val result = OCID.isValid(this["user"]) && OCID.isValid(this["tenancy"]) && this["fingerprint"] != null && fingerprintCheckPattern.matcher(this["fingerprint"]).matches() this["region"] != null && (!requireKeyPath || this["key_file"] != null) if (result) { try { Region.fromRegionId(this["region"]) return true } catch (_: IllegalArgumentException) { } } return false } fun ConfigFile.toOracleAccountProfile(telegramUserId: Long, name: String = generateRandomName()): OracleAccountProfile { if (!validate()) { throw IllegalStateException("Invalid BMC ConfigFile") } return OracleAccountProfile( this["user"], this["tenancy"], this["region"], clearFingerprintSeparator(this["fingerprint"]), telegramUserId, name ) } private fun clearFingerprintSeparator(source: String): String { return source.replace(":", "") } fun Boolean.not(action: () -> Unit) { if (!this) { action() } } private const val BEGIN_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----" private const val END_PRIVATE_KEY = "-----END PRIVATE KEY-----" fun loadPkcs8PrivateKeyFromStream(input: InputStream): PrivateKey { val pemContent = input.readAllBytes() .toString(StandardCharsets.UTF_8) .trim { it == '\n' || it == '\r' || it == ' ' } if (!pemContent.startsWith(BEGIN_PRIVATE_KEY) || !pemContent.endsWith(END_PRIVATE_KEY) ) { throw IllegalArgumentException("Incorrect private key format.") } val keyString = pemContent .replace(BEGIN_PRIVATE_KEY, "") .replace(END_PRIVATE_KEY, "") .replace("\n", "") .replace("\r", "") val keyBytes = Base64.getDecoder().decode(keyString) return KeyFactory.getInstance("RSA").generatePrivate(PKCS8EncodedKeySpec(keyBytes)) } fun getKeyPairFingerprint(privateKey: RSAPrivateCrtKey, separator: String = ""): String { val keySpec = RSAPublicKeySpec(privateKey.modulus, privateKey.publicExponent) val keyFactory = KeyFactory.getInstance("RSA") val publicKey = keyFactory.generatePublic(keySpec) val digest = MessageDigest.getInstance("MD5") val digestBytes = digest.digest(publicKey.encoded) return digestBytes.joinToString(separator = separator) { eachByte -> "%02x".format(eachByte) } } /** * 将 PrivateKeyInfo 转换成 PrivateKey. * @param info 要转换的 PrivateKeyInfo. * @return 从 PrivateKeyInfo 转换出来的 PrivateKey 对象. */ private fun privateKeyInfoToPrivateKey(info: PrivateKeyInfo): PrivateKey = JcaPEMKeyConverter().getPrivateKey(info) fun T.toPemString(): String { val writer = StringWriter() val pemWriter = PemWriter(writer) pemWriter.writeObject(JcaPKCS8Generator(this, null)) pemWriter.flush() return writer.toString() } private val internalDataDirectory = AtomicReference() var dataDirectory: File set(value) { internalDataDirectory.compareAndSet(null, value) } get() = internalDataDirectory.get() class InlineKeyboardGroupBuilder { private val builder = InlineKeyboardMarkup.builder() fun configure(block: InlineKeyboardMarkupBuilder.() -> Unit): InlineKeyboardGroupBuilder { builder.block() return this } fun newRow(): InlineKeyboardRowBuilder { return InlineKeyboardRowBuilder(this) } fun addRow(row: List) { builder.keyboardRow(row) } fun rowButton(apply: InlineKeyboardButtonBuilder.() -> Unit): InlineKeyboardGroupBuilder { return newRow().addButton(apply).then() } fun build(): InlineKeyboardMarkup = builder.build() } class InlineKeyboardRowBuilder(private val groupBuilder: InlineKeyboardGroupBuilder) { private val row = mutableListOf() fun addButton(button: InlineKeyboardButton): InlineKeyboardRowBuilder { row.add(button) return this } fun addButton(buttonBuilder: InlineKeyboardButtonBuilder): InlineKeyboardRowBuilder { addButton(buttonBuilder.build()) return this } fun addButton(apply: InlineKeyboardButtonBuilder.() -> Unit): InlineKeyboardRowBuilder { val builder = InlineKeyboardButton.builder() builder.apply() addButton(builder) return this } fun then(): InlineKeyboardGroupBuilder { groupBuilder.addRow(row) return groupBuilder } fun newRow(): InlineKeyboardRowBuilder { return then().newRow() } fun build(): InlineKeyboardMarkup { return then().build() } } fun InlineKeyboardGroupBuilder.addBackButton(callback: InlineKeyboardCallback) { rowButton { text("<<< 返回上一级") callbackData(callback) } } val CallbackQuery.callbackData: InlineKeyboardCallback get() { val refJson = gson.fromJson(data, JsonObject::class.java) return if (refJson.has("rcode") && refJson.get("rcode").isJsonPrimitive) { val refCode = refJson.get("rcode").asString callbackCache.getIfPresent(refCode) ?: throw IllegalStateException("CallbackData has expired.") } else { throw IllegalStateException("RefCode is invalid.") } } val CallbackQuery.callbackDataOrNull: InlineKeyboardCallback? get() { return try { callbackData } catch (e: Exception) { null } } fun callbackQueryAt(actionName: String): (Update) -> Boolean { return { it.hasCallbackQuery() && it.callbackQuery.callbackDataOrNull != null && actionName == it.callbackQuery.callbackData.action } } fun checkCallbackQueryIsProfileOwner(): (Update) -> Boolean = { upd -> upd.hasCallbackQuery() && upd.callbackQuery.callbackDataOrNull != null && upd.callbackQuery.from.id == getProfileByCallback(upd.callbackQuery.callbackData).telegramUserId } private val callbackCache: Cache = CacheBuilder.newBuilder() .expireAfterAccess(10, TimeUnit.MINUTES) .softValues() .build() fun InlineKeyboardButtonBuilder.callbackData(callback: InlineKeyboardCallback): InlineKeyboardButtonBuilder { var cacheReferenceCode: String do { cacheReferenceCode = Random().randomString(32) } while (callbackCache.getIfPresent(cacheReferenceCode) != null) callbackCache.put(cacheReferenceCode, callback) callbackData("{\"rcode\":\"$cacheReferenceCode\"}") return this } fun InlineKeyboardButtonBuilder.callbackData( action: String, extraData: JsonObject? = null ): InlineKeyboardButtonBuilder { callbackData(InlineKeyboardCallback(action, extraData ?: JsonObject())) return this } fun createPromptKeyboard( yesCallback: InlineKeyboardCallback, noCallback: InlineKeyboardCallback, yesMsg: String = "确认", noMsg: String = "取消" ): InlineKeyboardMarkup { return InlineKeyboardGroupBuilder() .rowButton { text(yesMsg) callbackData(yesCallback) } .rowButton { text(noMsg) callbackData(noCallback) } .build() } fun , Sender : AbsSender> Method.execute(sender: Sender): T { return sender.execute(this) } fun , Sender : MessageSender> Method.execute(sender: Sender): T { return sender.execute(this) } fun , Sender : SilentSender> Method.execute(sender: Sender): Optional? { return sender.execute(this) } val gson = Gson() @Suppress("MemberVisibilityCanBePrivate") data class InlineKeyboardCallback( @SerializedName("a") val action: String, @SerializedName("d") val extraData: JsonObject = JsonObject() ) { fun toJson(): String { return gson.toJson(this) } /** * 创建下一动作的回调数据对象. * @param newAction 下一个动作名称. * @param newExtraData 新的回调额外数据, 如果不为 null, 将会跟原本的 JsonObject 合并, 重复项将使用新 JsonObject 的值. * @return 返回新的回调数据对象. */ fun next( newAction: String, newExtraData: JsonObject? = null, replaceData: Boolean = false ): InlineKeyboardCallback { if (newExtraData != null) { if (replaceData) { return InlineKeyboardCallback(newAction, newExtraData) } val nextExtraData = this.extraData.deepCopy() for (key in newExtraData.keySet()) { nextExtraData.add(key, newExtraData[key]) } return InlineKeyboardCallback(newAction, nextExtraData) } return InlineKeyboardCallback(newAction, this.extraData) } companion object { @JvmStatic fun fromJson(json: String): InlineKeyboardCallback { return gson.fromJson(json, InlineKeyboardCallback::class.java) } } } fun getStatsName(action: String, subActonName: String): String { return "$action::action::$subActonName" } fun Random.randomString(length: Int): String { val builder = StringBuilder() for (i in 1..length) { val charNumber = nextInt(62) val char = when (charNumber) { in 0..25 -> (charNumber + 65) in 26..51 -> (charNumber % 26 + 97) else -> (charNumber - 52 + 48) }.toChar() builder.append(char) } return builder.toString() } fun callbackQueryOf( action: String, checkProfileOwner: Boolean = true, block: (BaseAbilityBot, Update) -> Unit ): Reply { return Reply.of(block, mutableListOf?>().apply { add(callbackQueryAt(action)) if (checkProfileOwner) { add(checkCallbackQueryIsProfileOwner()) } }.toList()) } fun getProfileByCallback(callback: InlineKeyboardCallback): OracleAccountProfile { return OracleAccountProfile.fromJson(callback.extraData[JsonFields.AccountProfile].asJsonObject) } class JsonObjectBuilder(private val jsonObject: JsonObject) { operator fun String.plusAssign(json: JsonElement) { jsonObject.add(this, json) } operator fun String.plusAssign(value: String) { jsonObject.addProperty(this, value) } operator fun String.plusAssign(value: Boolean) { jsonObject.addProperty(this, value) } operator fun String.plusAssign(value: Char) { jsonObject.addProperty(this, value) } operator fun String.plusAssign(value: Number) { jsonObject.addProperty(this, value) } operator fun String.plusAssign(value: Any) { jsonObject.add(this, gson.toJsonTree(value)) } } fun jsonObjectOf(jsonObject: JsonObject = JsonObject(), block: JsonObjectBuilder.() -> Unit): JsonObject { JsonObjectBuilder(jsonObject).block() return jsonObject } object JsonFields { /** * [OracleAccountProfile] 字段 */ const val AccountProfile = "account_profile" /** * [ServerInstance] */ const val ServerInstance = "server_instance" }