Merge pull request #989 from mamoe/android_target

Android target
This commit is contained in:
Him188 2021-02-27 14:07:11 +08:00 committed by GitHub
commit ec140a0df2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 89780 additions and 505 deletions

View File

@ -0,0 +1,28 @@
/*
* Copyright 2019-2021 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
plugins {
kotlin("jvm")
kotlin("plugin.serialization")
id("kotlinx-atomicfu")
id("net.mamoe.kotlin-jvm-blocking-bridge")
}
description = "Mirai API binary compatibility validator"
tasks.withType(kotlinx.validation.KotlinApiBuildTask::class) {
inputClassesDirs =
files(inputClassesDirs.files, project(":mirai-core-api").buildDir.resolve("classes/kotlin/android/main"))
}
// tasks["apiDump"].dependsOn(project(":mirai-core-api").tasks["build"])
// this dependency is set in mirai-core-api since binary validator is configured before mirai-core-api

View File

@ -5918,7 +5918,7 @@ public final class net/mamoe/mirai/utils/SimpleLogger$LogPriority : java/lang/En
public static fun values ()[Lnet/mamoe/mirai/utils/SimpleLogger$LogPriority;
}
public final class net/mamoe/mirai/utils/SingleFileLogger : net/mamoe/mirai/utils/PlatformLogger {
public final class net/mamoe/mirai/utils/SingleFileLogger : net/mamoe/mirai/utils/PlatformLogger, net/mamoe/mirai/utils/MiraiLogger {
public fun <init> (Ljava/lang/String;)V
public fun <init> (Ljava/lang/String;Ljava/io/File;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/io/File;ILkotlin/jvm/internal/DefaultConstructorMarker;)V

View File

@ -9,15 +9,6 @@
@file:Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
/*
* Copyright 2019-2021 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
plugins {
kotlin("jvm")
kotlin("plugin.serialization")

View File

@ -10,11 +10,8 @@
@file:Suppress("UnstableApiUsage", "UNUSED_VARIABLE", "NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.jetbrains.kotlin.gradle.dsl.*
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
buildscript {
repositories {
@ -35,7 +32,7 @@ buildscript {
}
plugins {
kotlin("jvm") version Versions.kotlinCompiler
kotlin("jvm") // version Versions.kotlinCompiler
kotlin("plugin.serialization") version Versions.kotlinCompiler
// id("org.jetbrains.dokka") version Versions.dokka
id("net.mamoe.kotlin-jvm-blocking-bridge") version Versions.blockingBridge
@ -51,7 +48,8 @@ configure<kotlinx.validation.ApiValidationExtension> {
ignoredProjects.add(subproject.name)
}
ignoredProjects.remove("binary-compatibility-validator")
// Enable validator for module `binary-compatibility-validator` only.
ignoredProjects.remove("binary-compatibility-validator-android")
// Enable validator for module `binary-compatibility-validator` and `-android` only.
ignoredPackages.add("net.mamoe.mirai.internal")
@ -62,7 +60,6 @@ configure<kotlinx.validation.ApiValidationExtension> {
nonPublicMarkers.add("net.mamoe.mirai.MiraiExperimentalApi")
}
project.ext.set("isAndroidSDKAvailable", false)
GpgSigner.setup(project)
tasks.register("publishMiraiCoreArtifactsToMavenLocal") {
@ -74,24 +71,6 @@ tasks.register("publishMiraiCoreArtifactsToMavenLocal") {
)
}
// until
// https://youtrack.jetbrains.com/issue/KT-37152,
// are fixed.
/*
runCatching {
val keyProps = Properties().apply {
file("local.properties").takeIf { it.exists() }?.inputStream()?.use { load(it) }
}
if (keyProps.getProperty("sdk.dir", "").isNotEmpty()) {
project.ext.set("isAndroidSDKAvailable", true)
} else {
project.ext.set("isAndroidSDKAvailable", false)
}
}.exceptionOrNull()?.run {
project.ext.set("isAndroidSDKAvailable", false)
}*/
allprojects {
group = "net.mamoe"
version = Versions.project
@ -111,7 +90,6 @@ allprojects {
configureMppShadow()
configureEncoding()
configureKotlinTestSettings()
configureKotlinCompilerSettings()
configureKotlinExperimentalUsages()
runCatching {
@ -192,52 +170,34 @@ fun Project.configureDokka() {
// }
}
@Suppress("NOTHING_TO_INLINE") // or error
fun Project.configureJvmTarget() {
tasks.withType(KotlinJvmCompile::class.java) {
kotlinOptions.jvmTarget = "1.8"
}
kotlinTargets.orEmpty().filterIsInstance<KotlinJvmTarget>().forEach { target ->
target.compilations.all {
kotlinOptions.jvmTarget = "1.8"
kotlinOptions.languageVersion = "1.4"
}
target.testRuns["test"].executionTask.configure { useJUnitPlatform() }
}
extensions.findByType(JavaPluginExtension::class.java)?.run {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
fun Project.configureMppShadow() {
val kotlin =
runCatching {
(this as ExtensionAware).extensions.getByName("kotlin") as? KotlinMultiplatformExtension
}.getOrNull() ?: return
val shadowJvmJar by tasks.creating(ShadowJar::class) sd@{
group = "mirai"
archiveClassifier.set("-all")
if (project.configurations.findByName("jvmRuntimeClasspath") != null) {
val shadowJvmJar by tasks.creating(ShadowJar::class) sd@{
group = "mirai"
archiveClassifier.set("-all")
val compilations =
kotlin.targets.filter { it.platformType == KotlinPlatformType.jvm }
.map { it.compilations["main"] }
val compilations =
kotlin.targets.filter { it.platformType == KotlinPlatformType.jvm }
.map { it.compilations["main"] }
compilations.forEach {
dependsOn(it.compileKotlinTask)
from(it.output)
}
compilations.forEach {
dependsOn(it.compileKotlinTask)
from(it.output)
}
from(project.configurations.getByName("jvmRuntimeClasspath"))
from(project.configurations.findByName("jvmRuntimeClasspath"))
this.exclude { file ->
file.name.endsWith(".sf", ignoreCase = true)
}
this.exclude { file ->
file.name.endsWith(".sf", ignoreCase = true)
}
/*
/*
this.manifest {
this.attributes(
"Manifest-Version" to 1,
@ -246,109 +206,39 @@ fun Project.configureMppShadow() {
"Implementation-Version" to this.version.toString()
)
}*/
}
}
fun Project.configureEncoding() {
tasks.withType(JavaCompile::class.java) {
options.encoding = "UTF8"
}
}
fun Project.configureKotlinTestSettings() {
tasks.withType(Test::class) {
useJUnitPlatform()
}
when {
isKotlinJvmProject -> {
dependencies {
testImplementation(kotlin("test-junit5"))
testApi("org.junit.jupiter:junit-jupiter-api:5.2.0")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.2.0")
}
}
isKotlinMpp -> {
kotlinSourceSets?.forEach { sourceSet ->
if (sourceSet.name.endsWith("test", ignoreCase = true)) {
sourceSet.dependencies {
api(kotlin("test-junit5"))
api("org.junit.jupiter:junit-jupiter-api:5.2.0")
runtimeOnly("org.junit.jupiter:junit-jupiter-engine:5.2.0")
}
fun Project.configureEncoding() {
tasks.withType(JavaCompile::class.java) {
options.encoding = "UTF8"
}
}
fun Project.configureKotlinTestSettings() {
tasks.withType(Test::class) {
useJUnitPlatform()
}
when {
isKotlinJvmProject -> {
dependencies {
testImplementation(kotlin("test-junit5"))
testApi("org.junit.jupiter:junit-jupiter-api:5.2.0")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.2.0")
}
}
isKotlinMpp -> {
kotlinSourceSets?.forEach { sourceSet ->
if (sourceSet.name.endsWith("test", ignoreCase = true)) {
sourceSet.dependencies {
api(kotlin("test-junit5"))
api("org.junit.jupiter:junit-jupiter-api:5.2.0")
runtimeOnly("org.junit.jupiter:junit-jupiter-engine:5.2.0")
}
}
}
}
}
}
}
fun Project.configureKotlinCompilerSettings() {
val kotlinCompilations = kotlinCompilations ?: return
for (kotlinCompilation in kotlinCompilations) with(kotlinCompilation) {
if (isKotlinJvmProject) {
@Suppress("UNCHECKED_CAST")
this as KotlinCompilation<KotlinJvmOptions>
}
kotlinOptions.freeCompilerArgs += "-Xjvm-default=all"
}
}
val experimentalAnnotations = arrayOf(
"kotlin.RequiresOptIn",
"kotlin.contracts.ExperimentalContracts",
"kotlin.experimental.ExperimentalTypeInference",
"kotlin.ExperimentalUnsignedTypes",
"kotlin.time.ExperimentalTime",
"kotlin.io.path.ExperimentalPathApi",
"io.ktor.util.KtorExperimentalAPI",
"kotlinx.serialization.ExperimentalSerializationApi",
"net.mamoe.mirai.utils.MiraiInternalApi",
"net.mamoe.mirai.utils.MiraiExperimentalApi",
"net.mamoe.mirai.LowLevelApi",
"net.mamoe.mirai.utils.UnstableExternalImage",
"net.mamoe.mirai.message.data.ExperimentalMessageKey",
"net.mamoe.mirai.console.ConsoleFrontEndImplementation",
"net.mamoe.mirai.console.util.ConsoleInternalApi",
"net.mamoe.mirai.console.util.ConsoleExperimentalApi"
)
fun Project.configureKotlinExperimentalUsages() {
val sourceSets = kotlinSourceSets ?: return
for (target in sourceSets) {
target.languageSettings.progressiveMode = true
target.languageSettings.enableLanguageFeature("InlineClasses")
experimentalAnnotations.forEach { a ->
target.languageSettings.useExperimentalAnnotation(a)
}
}
}
fun Project.configureFlattenSourceSets() {
sourceSets {
findByName("main")?.apply {
resources.setSrcDirs(listOf(projectDir.resolve("resources")))
java.setSrcDirs(listOf(projectDir.resolve("src")))
}
findByName("test")?.apply {
resources.setSrcDirs(listOf(projectDir.resolve("resources")))
java.setSrcDirs(listOf(projectDir.resolve("test")))
}
}
}
val Project.kotlinSourceSets get() = extensions.findByName("kotlin").safeAs<KotlinProjectExtension>()?.sourceSets
val Project.kotlinTargets
get() =
extensions.findByName("kotlin").safeAs<KotlinSingleTargetExtension>()?.target?.let { listOf(it) }
?: extensions.findByName("kotlin").safeAs<KotlinMultiplatformExtension>()?.targets
val Project.isKotlinJvmProject: Boolean get() = extensions.findByName("kotlin") is KotlinJvmProjectExtension
val Project.isKotlinMpp: Boolean get() = extensions.findByName("kotlin") is KotlinMultiplatformExtension
val Project.kotlinCompilations
get() = kotlinTargets?.flatMap { it.compilations }
}

View File

@ -14,6 +14,8 @@ plugins {
repositories {
mavenLocal()
jcenter()
google()
mavenCentral()
}
kotlin {
@ -39,6 +41,9 @@ fun version(name: String): String {
}
dependencies {
val asmVersion = version("asm")
fun asm(module: String) = "org.ow2.asm:asm-$module:$asmVersion"
fun kotlinx(id: String, version: String) = "org.jetbrains.kotlinx:kotlinx-$id:$version"
fun ktor(id: String, version: String) = "io.ktor:ktor-$id:$version"
@ -46,6 +51,12 @@ dependencies {
api("com.jfrog.bintray.gradle", "gradle-bintray-plugin", version("bintray"))
api("com.github.jengelman.gradle.plugins", "shadow", version("shadow"))
api("org.jetbrains.kotlin", "kotlin-gradle-plugin", version("kotlinCompiler"))
api("org.jetbrains.kotlin", "kotlin-compiler-embeddable", version("kotlinCompiler"))
api("com.android.tools.build", "gradle", version("androidGradlePlugin"))
api(asm("tree"))
api(asm("util"))
api(asm("commons"))
api(gradleApi())
}

View File

@ -10,7 +10,7 @@
import org.gradle.api.NamedDomainObjectCollection
import org.gradle.api.NamedDomainObjectProvider
import org.gradle.api.Project
import org.gradle.kotlin.dsl.provideDelegate
import java.util.*
/*
* Copyright 2020 Mamoe Technologies and contributors.
@ -21,11 +21,33 @@ import org.gradle.kotlin.dsl.provideDelegate
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
val Project.isAndroidSDKAvailable: Boolean
get() {
val isAndroidSDKAvailable: Boolean by this
return isAndroidSDKAvailable
private object ProjectAndroidSdkAvailability {
val map: MutableMap<String, Boolean> = mutableMapOf()
@Suppress("UNUSED_PARAMETER", "UNREACHABLE_CODE")
@Synchronized
operator fun get(project: Project): Boolean {
return true
if (map[project.path] != null) return map[project.path]!!
val projectAvailable = project.runCatching {
val keyProps = Properties().apply {
file("local.properties").takeIf { it.exists() }?.inputStream()?.use { load(it) }
}
keyProps.getProperty("sdk.dir", "").isNotEmpty()
}.getOrElse { false }
fun impl(): Boolean {
if (project === project.rootProject) return projectAvailable
return projectAvailable || get(project.rootProject)
}
map[project.path] = impl()
return map[project.path]!!
}
}
val Project.isAndroidSDKAvailable: Boolean get() = ProjectAndroidSdkAvailability[this]
val <T> NamedDomainObjectCollection<T>.androidMain: NamedDomainObjectProvider<T>
get() = named("androidMain")
@ -43,16 +65,12 @@ val <T> NamedDomainObjectCollection<T>.commonMain: NamedDomainObjectProvider<T>
get() = named("commonMain")
fun Project.printAndroidNotInstalled() {
// println(
// """Android SDK 可能未安装.
// $name 的 Android 目标编译将不会进行.
// 这不会影响 Android 以外的平台的编译.
// """.trimIndent()
// )
// println(
// """Android SDK might not be installed.
// Android target of $name will not be compiled.
// It does no influence on the compilation of other platforms.
// """.trimIndent()
// )
println(
"""Android SDK 可能未安装. $name 的 Android 目标编译将不会进行. 这不会影响 Android 以外的平台的编译.
""".trimIndent()
)
println(
"""Android SDK might not be installed. Android target of $name will not be compiled. It does no influence on the compilation of other platforms.
""".trimIndent()
)
}

View File

@ -0,0 +1,168 @@
/*
* Copyright 2019-2021 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.api.tasks.testing.Test
import org.gradle.kotlin.dsl.*
import org.jetbrains.kotlin.gradle.dsl.*
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
fun Project.useIr() {
kotlinCompilations?.forEach { kotlinCompilation ->
kotlinCompilation.kotlinOptions.freeCompilerArgs += "-Xuse-ir"
}
}
fun Project.configureJvmTarget() {
val defaultVer = JavaVersion.VERSION_1_8
tasks.withType(KotlinJvmCompile::class.java) {
kotlinOptions.languageVersion = "1.4"
kotlinOptions.jvmTarget = defaultVer.toString()
kotlinOptions.freeCompilerArgs += "-Xjvm-default=all"
}
extensions.findByType(JavaPluginExtension::class.java)?.run {
sourceCompatibility = defaultVer
targetCompatibility = defaultVer
}
kotlinTargets.orEmpty().filterIsInstance<KotlinJvmTarget>().forEach { target ->
when (target.attributes.getAttribute(KotlinPlatformType.attribute)) { // mirai does magic, don't use target.platformType
KotlinPlatformType.androidJvm -> {
target.compilations.all {
/*
* Kotlin JVM compiler generates Long.hashCode witch is available since API 26 when targeting JVM 1.8 while IR prefer member function hashCode always.
*/
// kotlinOptions.useIR = true
// IR cannot compile mirai. We'll wait for Kotlin 1.5 for stable IR release.
}
}
else -> {
}
}
target.testRuns["test"].executionTask.configure { useJUnitPlatform() }
}
}
fun Project.configureEncoding() {
tasks.withType(JavaCompile::class.java) {
options.encoding = "UTF8"
}
}
fun Project.configureKotlinTestSettings() {
tasks.withType(Test::class) {
useJUnitPlatform()
}
when {
isKotlinJvmProject -> {
dependencies {
"testImplementation"(kotlin("test-junit5"))
"testApi"("org.junit.jupiter:junit-jupiter-api:5.2.0")
"testRuntimeOnly"("org.junit.jupiter:junit-jupiter-engine:5.2.0")
}
}
isKotlinMpp -> {
kotlinSourceSets?.forEach { sourceSet ->
if (sourceSet.name == "common") {
sourceSet.dependencies {
implementation(kotlin("test"))
implementation(kotlin("test-annotations-common"))
}
} else {
sourceSet.dependencies {
implementation(kotlin("test-junit5"))
implementation("org.junit.jupiter:junit-jupiter-api:5.2.0")
implementation("org.junit.jupiter:junit-jupiter-engine:5.2.0")
}
}
}
}
}
}
val experimentalAnnotations = arrayOf(
"kotlin.RequiresOptIn",
"kotlin.contracts.ExperimentalContracts",
"kotlin.experimental.ExperimentalTypeInference",
"kotlin.ExperimentalUnsignedTypes",
"kotlin.time.ExperimentalTime",
"kotlin.io.path.ExperimentalPathApi",
"io.ktor.util.KtorExperimentalAPI",
"kotlinx.serialization.ExperimentalSerializationApi",
"net.mamoe.mirai.utils.MiraiInternalApi",
"net.mamoe.mirai.utils.MiraiExperimentalApi",
"net.mamoe.mirai.LowLevelApi",
"net.mamoe.mirai.utils.UnstableExternalImage",
"net.mamoe.mirai.message.data.ExperimentalMessageKey",
"net.mamoe.mirai.console.ConsoleFrontEndImplementation",
"net.mamoe.mirai.console.util.ConsoleInternalApi",
"net.mamoe.mirai.console.util.ConsoleExperimentalApi"
)
fun Project.configureKotlinExperimentalUsages() {
val sourceSets = kotlinSourceSets ?: return
for (target in sourceSets) {
target.configureKotlinExperimentalUsages()
}
}
fun KotlinSourceSet.configureKotlinExperimentalUsages() {
languageSettings.progressiveMode = true
languageSettings.enableLanguageFeature("InlineClasses")
experimentalAnnotations.forEach { a ->
languageSettings.useExperimentalAnnotation(a)
}
}
fun Project.configureFlattenSourceSets() {
sourceSets {
findByName("main")?.apply {
resources.setSrcDirs(listOf(projectDir.resolve("resources")))
java.setSrcDirs(listOf(projectDir.resolve("src")))
}
findByName("test")?.apply {
resources.setSrcDirs(listOf(projectDir.resolve("resources")))
java.setSrcDirs(listOf(projectDir.resolve("test")))
}
}
}
inline fun <reified T> Any?.safeAs(): T? {
return this as? T
}
val Project.kotlinSourceSets get() = extensions.findByName("kotlin").safeAs<KotlinProjectExtension>()?.sourceSets
val Project.kotlinTargets
get() =
extensions.findByName("kotlin").safeAs<KotlinSingleTargetExtension>()?.target?.let { listOf(it) }
?: extensions.findByName("kotlin").safeAs<KotlinMultiplatformExtension>()?.targets
val Project.isKotlinJvmProject: Boolean get() = extensions.findByName("kotlin") is KotlinJvmProjectExtension
val Project.isKotlinMpp: Boolean get() = extensions.findByName("kotlin") is KotlinMultiplatformExtension
val Project.kotlinCompilations
get() = kotlinTargets?.flatMap { it.compilations }

View File

@ -12,7 +12,7 @@
import org.gradle.api.attributes.Attribute
object Versions {
const val project = "2.5.0-dev-2"
const val project = "2.5.0-dev-android-1"
const val core = project
const val console = project
@ -34,13 +34,15 @@ object Versions {
const val blockingBridge = "1.10.0"
const val androidGradlePlugin = "3.5.3"
const val androidGradlePlugin = "4.1.1"
const val android = "4.1.1.4"
const val bintray = "1.8.5"
const val shadow = "6.1.0"
const val slf4j = "1.7.30"
const val log4j = "2.13.3"
const val asm = "9.1"
// If you the versions below, you need to sync changes to mirai-console/buildSrc/src/main/kotlin/Versions.kt
@ -104,3 +106,5 @@ const val `jetbrains-annotations` = "org.jetbrains:annotations:19.0.0"
const val `caller-finder` = "io.github.karlatemp:caller:1.1.1"
const val `android-runtime` = "com.google.android:android:${Versions.android}"

View File

@ -0,0 +1,283 @@
/*
* Copyright 2019-2021 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("MemberVisibilityCanBePrivate")
package androidutil
import groovy.util.Node
import groovy.util.XmlParser
import org.gradle.api.Project
import org.objectweb.asm.ClassReader
import org.objectweb.asm.Type
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.FieldInsnNode
import org.objectweb.asm.tree.MethodInsnNode
import java.io.File
import java.net.URL
object AndroidApiLevelCheck {
data class ClassInfo(
val name: String,
val since: Int,
val superTypes: List<SuperInfo>,
val fieldInfos: Map<String, MemberInfo>,
val methodInfos: Map<String, MemberInfo>
) {
data class SuperInfo(
val name: String,
val since: Int?,
val removed: Int?
)
data class MemberInfo(
val name: String,
val since: Int?
)
}
class Analyzer(
val classesInfos: Map<String, ClassInfo>
) {
var path: String? = null
var context: String? = null
var file: File? = null
var apilevel = 0
var reported = false
inline fun withPath(path: String, block: Analyzer.() -> Unit) {
this.path = path
block(this)
this.path = null
}
inline fun withContext(context: String, block: Analyzer.() -> Unit) {
this.context = context
block(this)
this.context = null
}
fun report(prefix: String, message: String) {
reported = true
file?.let { file ->
println("> $file")
this.file = null
}
context?.let { context ->
println(" > $context")
this.context = null
}
path?.let { path ->
println(" > $path")
this.path = null
}
if (prefix.isBlank()) {
message
} else {
"$prefix: $message"
}.split('\n').forEach { println(" $it") }
}
fun needCheck(type: String): Boolean {
if (type.startsWith("android/")) return true
if (type.startsWith("androidx/")) return true
if (type.startsWith("java/")) return true
if (type.startsWith("javax/")) return true
return classesInfos.containsKey(type)
}
fun checkClass(prefix: String, name: String) {
if (!needCheck(name)) return
val info = classesInfos[name]
if (info == null) {
report(prefix, "$name not found in api-version.xml")
return
}
if (info.since > apilevel) {
report(prefix, "$name since api level ${info.since}")
}
}
fun checkFieldAccess(prefix: String, owner: String, name: String) {
if (!needCheck(owner)) return
val info = classesInfos[owner] ?: return
val field = info.fieldInfos[name]
if (field == null) {
report(prefix, "No field $owner.$name")
return
}
if ((field.since ?: 0) > apilevel) {
report(prefix, "$owner.$name since api level ${field.since}")
}
}
fun checkMethodAccess(prefix: String, owner: String, name: String) {
if (!needCheck(owner)) return
fun findMethod(type: String): ClassInfo.MemberInfo? {
val cinfo = classesInfos[type] ?: return null
return cinfo.methodInfos[name] ?: kotlin.run {
cinfo.superTypes.forEach { stype ->
if (stype.removed != null) {
if (apilevel >= stype.removed) return@forEach
}
if (stype.since != null) {
if (apilevel < stype.since) return@forEach
}
findMethod(stype.name)?.let { return it }
}
null
}
}
val method = findMethod(owner)
if (method == null) {
report(prefix, "No method $owner.$name")
return
}
if ((method.since ?: 0) > apilevel) {
report(prefix, "$owner.$name since api level ${method.since}")
}
}
private val Type.top: Type
get() = when (sort) {
Type.ARRAY -> elementType
else -> this
}
fun analyze(classNode: ClassNode, file: File) {
this.file = file
withContext("Check class") {
withPath("class checking") {
checkClass("Couldn't extend ${classNode.superName}", classNode.superName)
classNode.interfaces?.forEach { checkClass("Couldn't implements $it", it) }
}
}
classNode.fields?.forEach { field ->
withContext("Field ${field.name}: ${field.desc}") {
val type = Type.getType(field.desc).top.internalName
checkClass("Couldn't access $type", type)
}
}
classNode.methods?.forEach { method ->
withContext("Method ${method.name}${method.desc}") {
withPath("Checking method desc") {
val returnType = Type.getReturnType(method.desc).top.internalName
checkClass("Couldn't access $returnType", returnType)
Type.getArgumentTypes(method.desc).map { it.top.internalName }.forEach {
checkClass("Couldn't access $it", it)
}
}
method.instructions?.forEach { insn ->
when (insn) {
is FieldInsnNode -> {
withPath("Access field ${insn.owner}.${insn.name}: ${insn.desc}") {
val type = Type.getType(insn.desc)
val prefix = "Couldn't access ${insn.owner}.${insn.name}: ${insn.desc}"
checkClass(prefix, type.internalName)
checkFieldAccess(prefix, insn.owner, insn.name)
}
}
is MethodInsnNode -> {
withPath("Invoke method ${insn.owner}.${insn.name}${insn.desc}") {
checkClass("Couldn't access ${insn.owner}", insn.owner)
val returnType = Type.getReturnType(insn.desc).top.internalName
checkClass("Couldn't access $returnType", returnType)
Type.getArgumentTypes(insn.desc).map { it.top.internalName }.forEach {
checkClass("Couldn't access $it", it)
}
checkMethodAccess(
"Couldn't access ${insn.owner}.${insn.name}${insn.desc}",
insn.owner,
insn.name + insn.desc
)
}
}
}
}
}
}
}
}
@Suppress("UNCHECKED_CAST")
fun check(classes: File, level: Int, project: Project) {
val apiVersionsFile = project.rootProject.projectDir.resolve("buildSrc/src/main/resources/androidutil/api-versions.xml")
val classesInfos = mutableMapOf<String, ClassInfo>()
XmlParser().parse(apiVersionsFile).children().forEach { classNode ->
classNode as Node
if (classNode.name() == "class") {
val fieldInfos = mutableMapOf<String, ClassInfo.MemberInfo>()
val methodInfos = mutableMapOf<String, ClassInfo.MemberInfo>()
val cinfo = ClassInfo(
classNode.attribute("name").toString(),
classNode.attribute("since").toString().toInt(),
(classNode.children() as List<Node>).filter {
it.name() == "implements" || it.name() == "extends"
}.map {
ClassInfo.SuperInfo(
it.attribute("name").toString(),
it.attribute("since")?.toString()?.toInt(),
it.attribute("removed")?.toString()?.toInt()
)
},
fieldInfos, methodInfos
)
classesInfos[cinfo.name] = cinfo
classNode.children().forEach { memberNode ->
memberNode as Node
when (memberNode.name()) {
"method" -> {
val method = ClassInfo.MemberInfo(
memberNode.attribute("name").toString(),
memberNode.attribute("since")?.toString()?.toInt()
)
methodInfos[method.name] = method
}
"field" -> {
val field = ClassInfo.MemberInfo(
memberNode.attribute("name").toString(),
memberNode.attribute("since")?.toString()?.toInt()
)
fieldInfos[field.name] = field
}
}
}
}
}
val analyzer = Analyzer(classesInfos)
analyzer.apilevel = level
classes.walk()
.filter { it.isFile && it.extension == "class" }
.map { file ->
kotlin.runCatching {
val cnode = ClassNode()
file.inputStream().use {
ClassReader(it).accept(cnode, 0)
}
cnode
}.getOrNull() to file
}
.filter { it.first != null }
.map {
@Suppress("UNCHECKED_CAST")
it as Pair<ClassNode, File>
}
.forEach { (classNode, file) ->
analyzer.analyze(classNode, file)
}
if (analyzer.reported) {
error("Verify failed")
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -20,3 +20,5 @@ kotlin.native.enableDependencyPropagation=false
#kotlin.mpp.enableGranularSourceSetsMetadata=true
systemProp.org.gradle.internal.publish.checksums.insecure=true
gnsp.disableApplyOnlyOnRootProjectEnforcement=true
# We may target 15 with Kotlin 1.5 IR
mirai.android.target.api.level=24

View File

@ -12,7 +12,6 @@
plugins {
kotlin("jvm")
kotlin("plugin.serialization")
id("java")
`maven-publish`
id("com.jfrog.bintray")
id("net.mamoe.kotlin-jvm-blocking-bridge")

View File

@ -14,7 +14,7 @@ plugins {
kotlin("multiplatform")
kotlin("plugin.serialization")
id("kotlinx-atomicfu")
//id("kotlinx-atomicfu")
id("signing")
id("net.mamoe.kotlin-jvm-blocking-bridge")
@ -28,9 +28,13 @@ kotlin {
explicitApi()
if (isAndroidSDKAvailable) {
apply(from = rootProject.file("gradle/android.gradle"))
android("android") {
publishAllLibraryVariants()
// apply(from = rootProject.file("gradle/android.gradle"))
// android("android") {
// publishAllLibraryVariants()
// }
jvm("android") {
attributes.attribute(KotlinPlatformType.attribute, KotlinPlatformType.androidJvm)
// publishAllLibraryVariants()
}
} else {
printAndroidNotInstalled()
@ -47,7 +51,7 @@ kotlin {
// }
sourceSets {
commonMain {
val commonMain by getting {
dependencies {
implementation(project(":mirai-core-utils"))
api(kotlin("serialization"))
@ -76,16 +80,20 @@ kotlin {
}
if (isAndroidSDKAvailable) {
androidMain {
val androidMain by getting {
dependsOn(commonMain)
dependencies {
compileOnly(`android-runtime`)
api1(`ktor-client-android`)
}
}
}
val jvmMain by getting
val jvmMain by getting {
jvmTest {
}
val jvmTest by getting {
dependencies {
runtimeOnly(files("build/classes/kotlin/jvm/test")) // classpath is not properly set by IDE
}
@ -93,6 +101,18 @@ kotlin {
}
}
tasks.register("checkAndroidApiLevel") {
doFirst {
androidutil.AndroidApiLevelCheck.check(
buildDir.resolve("classes/kotlin/android/main"),
project.property("mirai.android.target.api.level")!!.toString().toInt(),
project
)
}
group = "verification"
this.mustRunAfter("androidMainClasses")
}
tasks.getByName("androidTest").dependsOn("checkAndroidApiLevel")
fun org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler.implementation1(dependencyNotation: String) =
implementation(dependencyNotation) {
@ -116,4 +136,5 @@ configureMppPublishing()
afterEvaluate {
project(":binary-compatibility-validator").tasks["apiBuild"].dependsOn(project(":mirai-core-api").tasks["build"])
project(":binary-compatibility-validator-android").tasks["apiBuild"].dependsOn(project(":mirai-core-api").tasks["build"])
}

View File

@ -0,0 +1,19 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai
import java.util.*
import kotlin.reflect.full.companionObjectInstance
@JvmSynthetic
internal actual fun findMiraiInstance(): IMirai {
return ServiceLoader.load(IMirai::class.java).firstOrNull()
?: Class.forName("net.mamoe.mirai.internal.MiraiImpl").kotlin.companionObjectInstance as IMirai
}

View File

@ -0,0 +1,78 @@
/*
* Copyright 2019-2021 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.utils
import net.mamoe.mirai.Bot
import net.mamoe.mirai.internal.utils.isSliderCaptchaSupportKind
import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.utils.LoginSolver.Companion.Default
/**
* 验证码, 设备锁解决器
*
* @see Default
* @see BotConfiguration.loginSolver
*/
public actual abstract class LoginSolver public actual constructor() {
/**
* 处理图片验证码.
*
* 返回 `null` 以表示无法处理验证码, 将会刷新验证码或重试登录.
* 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止
*
* @throws LoginFailedException
*/
public actual abstract suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String?
/**
* `true` 表示支持滑动验证码, 遇到滑动验证码时 mirai 会请求 [onSolveSliderCaptcha].
* 否则会跳过滑动验证码并告诉服务器此客户端不支持, 有可能导致登录失败
*/
public actual open val isSliderCaptchaSupported: Boolean
get() = isSliderCaptchaSupportKind ?: true
/**
* 处理滑动验证码.
*
* 返回 `null` 以表示无法处理验证码, 将会刷新验证码或重试登录.
* 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止
*
* @throws LoginFailedException
* @return 验证码解决成功后获得的 ticket.
*/
public actual abstract suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String?
/**
* 处理不安全设备验证.
*
* 返回值保留给将来使用. 目前在处理完成后返回任意内容 (包含 `null`) 均视为处理成功.
* 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止.
*
* @return 任意内容. 返回值保留以供未来更新.
* @throws LoginFailedException
*/
public actual abstract suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String?
public actual companion object {
/**
* 当前平台默认的 [LoginSolver]. Android 端没有默认验证码实现, [Default] 总为 `null`.
*/
@JvmField
public actual val Default: LoginSolver? = null
@Suppress("unused")
@Deprecated("Binary compatibility", level = DeprecationLevel.HIDDEN)
public actual fun getDefault(): LoginSolver = Default
?: error("LoginSolver is not provided by default on your platform. Please specify by BotConfiguration.loginSolver")
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("MemberVisibilityCanBePrivate")
package net.mamoe.mirai.utils
import android.util.Log
/**
* [Log] 日志实现
*
* @see MiraiLogger.create
* @see SingleFileLogger 使用单一文件记录日志
* @see DirectoryLogger 在一个目录中按日期存放文件记录日志, 自动清理过期日志
*/
@MiraiInternalApi
public actual open class PlatformLogger actual constructor(
public override val identity: String?,
) : MiraiLoggerPlatformBase() {
public override fun verbose0(message: String?) {
Log.v(identity, message)
}
public override fun verbose0(message: String?, e: Throwable?) {
Log.v(identity, message, e)
}
public override fun info0(message: String?) {
Log.i(identity, message)
}
public override fun info0(message: String?, e: Throwable?) {
Log.i(identity, message)
}
public override fun warning0(message: String?) {
Log.w(identity, message)
}
public override fun warning0(message: String?, e: Throwable?) {
Log.w(identity, message)
}
public override fun error0(message: String?) {
Log.e(identity, message)
}
public override fun error0(message: String?, e: Throwable?) {
Log.e(identity, message)
}
public override fun debug0(message: String?) {
Log.d(identity, message)
}
public override fun debug0(message: String?, e: Throwable?) {
Log.d(identity, message)
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright 2019-2021 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.utils
import net.mamoe.mirai.internal.utils.StdoutLogger
import java.io.File
/**
* 将日志写入('append')到特定文件.
*
* @see PlatformLogger 查看格式信息
*/
public actual class SingleFileLogger actual constructor(
identity: String,
file: File
) : MiraiLogger by StdoutLogger(identity, { file.appendText(it + "\n") }) {
public actual constructor(identity: String) : this(identity, File("$identity-${getCurrentDate()}.log"))
init {
file.createNewFile()
require(file.isFile) { "Log file must be a file: $file" }
require(file.canWrite()) { "Log file must be write: $file" }
}
}

View File

@ -131,9 +131,9 @@ internal fun Method.registerEventHandler(
} else {
// java methods
val paramType = this.parameters[0].type
check(this.parameterCount == 1 && Event::class.java.isAssignableFrom(paramType)) {
"Illegal method parameter. Required one exact Event subclass. found ${this.parameters.contentToString()}"
val paramType = this.parameterTypes[0]
check(this.parameterTypes.size == 1 && Event::class.java.isAssignableFrom(paramType)) {
"Illegal method parameter. Required one exact Event subclass. found ${this.parameterTypes.contentToString()}"
}
suspend fun callMethod(event: Event): Any? {
fun Method.invokeWithErrorReport(self: Any?, vararg args: Any?): Any? = try {

View File

@ -0,0 +1,131 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.internal.utils
import net.mamoe.mirai.utils.*
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
/**
* JVM 控制台日志实现
*
*
* 单条日志格式 (正则) :
* ```regex
* ^([\w-]*\s[\w:]*)\s(\w)\/(.*?):\s(.+)$
* ```
* 其中 group 分别为: 日期与时间, 严重程度, [identity], 消息内容.
*
* 示例:
* ```log
* 2020-05-21 19:51:09 V/Bot 1994701021: Send: OidbSvc.0x88d_7
* ```
*
* 日期时间格式为 `yyyy-MM-dd HH:mm:ss`,
*
* 严重程度为 V, I, W, E. 分别对应 verbose, info, warning, error
*
* @param isColored 是否添加 ANSI 颜色
*
* @see MiraiLogger.create
* @see SingleFileLogger 使用单一文件记录日志
* @see DirectoryLogger 在一个目录中按日期存放文件记录日志, 自动清理过期日志
*/
internal open class StdoutLogger constructor(
override val identity: String? = "Mirai",
/**
* 日志输出. 不会自动添加换行
*/
open val output: (String) -> Unit,
val isColored: Boolean = true
) : MiraiLoggerPlatformBase() {
constructor(identity: String?) : this(identity, ::println)
constructor(identity: String?, output: (String) -> Unit) : this(identity, output, true)
/**
* 输出一条日志. [message] 末尾可能不带换行符.
*/
protected open fun printLog(message: String?, priority: SimpleLogger.LogPriority) {
if (isColored) output("${priority.color}$currentTimeFormatted ${priority.simpleName}/$identity: $message${Color.RESET}")
else output("$currentTimeFormatted ${priority.simpleName}/$identity: $message")
}
/**
* 获取指定 [SimpleLogger.LogPriority] 的颜色
*/
protected open val SimpleLogger.LogPriority.color: Color
get() = when (this) {
SimpleLogger.LogPriority.VERBOSE -> Color.RESET
SimpleLogger.LogPriority.INFO -> Color.LIGHT_GREEN
SimpleLogger.LogPriority.WARNING -> Color.LIGHT_RED
SimpleLogger.LogPriority.ERROR -> Color.RED
SimpleLogger.LogPriority.DEBUG -> Color.LIGHT_CYAN
}
public override fun verbose0(message: String?): Unit = printLog(message, SimpleLogger.LogPriority.VERBOSE)
public override fun verbose0(message: String?, e: Throwable?) {
if (e != null) verbose((message ?: e.toString()) + "\n${e.stackTraceToString()}")
else verbose(message.toString())
}
public override fun info0(message: String?): Unit = printLog(message, SimpleLogger.LogPriority.INFO)
public override fun info0(message: String?, e: Throwable?) {
if (e != null) info((message ?: e.toString()) + "\n${e.stackTraceToString()}")
else info(message.toString())
}
public override fun warning0(message: String?): Unit = printLog(message, SimpleLogger.LogPriority.WARNING)
public override fun warning0(message: String?, e: Throwable?) {
if (e != null) warning((message ?: e.toString()) + "\n${e.stackTraceToString()}")
else warning(message.toString())
}
public override fun error0(message: String?): Unit = printLog(message, SimpleLogger.LogPriority.ERROR)
public override fun error0(message: String?, e: Throwable?) {
if (e != null) error((message ?: e.toString()) + "\n${e.stackTraceToString()}")
else error(message.toString())
}
public override fun debug0(message: String?): Unit = printLog(message, SimpleLogger.LogPriority.DEBUG)
public override fun debug0(message: String?, e: Throwable?) {
if (e != null) debug((message ?: e.toString()) + "\n${e.stackTraceToString()}")
else debug(message.toString())
}
protected open val timeFormat: DateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.SIMPLIFIED_CHINESE)
private val currentTimeFormatted get() = timeFormat.format(Date())
@MiraiExperimentalApi("This is subject to change.")
protected enum class Color(private val format: String) {
RESET("\u001b[0m"),
WHITE("\u001b[30m"),
RED("\u001b[31m"),
EMERALD_GREEN("\u001b[32m"),
GOLD("\u001b[33m"),
BLUE("\u001b[34m"),
PURPLE("\u001b[35m"),
GREEN("\u001b[36m"),
GRAY("\u001b[90m"),
LIGHT_RED("\u001b[91m"),
LIGHT_GREEN("\u001b[92m"),
LIGHT_YELLOW("\u001b[93m"),
LIGHT_BLUE("\u001b[94m"),
LIGHT_PURPLE("\u001b[95m"),
LIGHT_CYAN("\u001b[96m")
;
override fun toString(): String = format
}
}

View File

@ -91,7 +91,8 @@ private fun String.forEachMiraiCode(block: (origin: String, name: String?, args:
}
}
private object MiraiCodeParsers : Map<String, MiraiCodeParser> by mapOf(
@Suppress("DELEGATED_MEMBER_HIDES_SUPERTYPE_OVERRIDE")
private object MiraiCodeParsers: AbstractMap<String, MiraiCodeParser>(), Map<String, MiraiCodeParser> by mapOf(
"at" to MiraiCodeParser(Regex("""(\d*)""")) { (target) ->
At(target.toLong())
},

View File

@ -119,7 +119,7 @@ public sealed class CustomMessage : SingleMessage {
private val factories: ConcurrentLinkedQueue<Factory<*>> = ConcurrentLinkedQueue()
internal fun register(factory: Factory<out CustomMessage>) {
factories.removeIf { it::class == factory::class }
factories.removeAll { it::class == factory::class }
val exist = factories.firstOrNull { it.typeName == factory.typeName }
if (exist != null) {
error("CustomMessage.Factory typeName ${factory.typeName} is already registered by ${exist::class.qualifiedName}")

View File

@ -18,6 +18,7 @@ import net.mamoe.mirai.message.data.Image.Key.IMAGE_ID_REGEX
import net.mamoe.mirai.message.data.Image.Key.IMAGE_RESOURCE_ID_REGEX_1
import net.mamoe.mirai.message.data.Image.Key.IMAGE_RESOURCE_ID_REGEX_2
import net.mamoe.mirai.utils.MiraiExperimentalApi
import net.mamoe.mirai.utils.replaceAllKotlin
import kotlin.native.concurrent.SharedImmutable
// region image
@ -128,7 +129,7 @@ internal fun constrainSingleMessagesImpl(sequence: Sequence<SingleMessage>): Lis
if (singleMessage is ConstrainSingle) {
val key = singleMessage.key.topmostKey
val firstOccurrence = list.first { it != null && key.isInstance(it) } // may be singleMessage itself
list.replaceAll {
list.replaceAllKotlin {
when {
it == null -> null
it === firstOccurrence -> singleMessage

View File

@ -1,5 +1,5 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
* Copyright 2019-2021 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@ -13,26 +13,8 @@ import java.io.File
import java.text.SimpleDateFormat
import java.util.*
private val currentDay get() = Calendar.getInstance()[Calendar.DAY_OF_MONTH]
private val currentDate get() = SimpleDateFormat("yyyy-MM-dd").format(Date())
/**
* 将日志写入('append')到特定文件.
*
* @see PlatformLogger 查看格式信息
*/
public class SingleFileLogger @JvmOverloads constructor(
identity: String,
file: File = File("$identity-$currentDate.log")
) :
PlatformLogger(identity, { file.appendText(it + "\n") }) {
init {
file.createNewFile()
require(file.isFile) { "Log file must be a file: $file" }
require(file.canWrite()) { "Log file must be write: $file" }
}
}
internal fun getCurrentDay() = Calendar.getInstance()[Calendar.DAY_OF_MONTH]
internal fun getCurrentDate() = SimpleDateFormat("yyyy-MM-dd").format(Date())
private val STUB: (priority: SimpleLogger.LogPriority, message: String?, e: Throwable?) -> Unit =
{ _: SimpleLogger.LogPriority, _: String?, _: Throwable? -> error("stub") }
@ -61,15 +43,15 @@ public class DirectoryLogger @JvmOverloads constructor(
}
}
private var day = currentDay
private var day = getCurrentDay()
private var delegate: SingleFileLogger = SingleFileLogger(identity, File(directory, "$currentDate.log"))
private var delegate: SingleFileLogger = SingleFileLogger(identity, File(directory, "${getCurrentDate()}.log"))
get() {
val currentDay = currentDay
val currentDay = getCurrentDay()
if (day != currentDay) {
day = currentDay
checkOutdated()
field = SingleFileLogger(identity!!, File(directory, "$currentDate.log"))
field = SingleFileLogger(identity!!, File(directory, "${getCurrentDate()}.log"))
}
return field
}

View File

@ -9,28 +9,11 @@
package net.mamoe.mirai.utils
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.io.*
import kotlinx.coroutines.io.jvm.nio.copyTo
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import net.mamoe.mirai.Bot
import net.mamoe.mirai.internal.utils.SeleniumLoginSolver
import net.mamoe.mirai.internal.utils.isSliderCaptchaSupportKind
import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.network.NoStandardInputForCaptchaException
import net.mamoe.mirai.utils.DeviceInfo.Companion.loadAsDeviceInfo
import net.mamoe.mirai.utils.LoginSolver.Companion.Default
import net.mamoe.mirai.utils.StandardCharImageLoginSolver.Companion.createBlocking
import java.awt.Image
import java.awt.image.BufferedImage
import java.io.File
import java.io.RandomAccessFile
import javax.imageio.ImageIO
import kotlin.coroutines.CoroutineContext
/**
* 验证码, 设备锁解决器
@ -38,7 +21,7 @@ import kotlin.coroutines.CoroutineContext
* @see Default
* @see BotConfiguration.loginSolver
*/
public abstract class LoginSolver {
public expect abstract class LoginSolver() {
/**
* 处理图片验证码.
*
@ -54,7 +37,6 @@ public abstract class LoginSolver {
* 否则会跳过滑动验证码并告诉服务器此客户端不支持, 有可能导致登录失败
*/
public open val isSliderCaptchaSupported: Boolean
get() = isSliderCaptchaSupportKind ?: true
/**
* 处理滑动验证码.
@ -83,215 +65,25 @@ public abstract class LoginSolver {
* 当前平台默认的 [LoginSolver]
*
* 检测策略:
* 1. 检测 `android.util.Log`, 如果存在, 返回 `null`.
* 2. 检测 JVM 属性 `mirai.no-desktop`. 若存在, 返回 [StandardCharImageLoginSolver]
* 3. 检测 JVM 桌面环境, 若支持, 返回 [SwingSolver]
* 4. 返回 [StandardCharImageLoginSolver]
* 1. 若是 `mirai-core-api-android` `android.util.Log` 存在, 返回 `null`.
* 2. 检测 JVM 属性 `mirai.no-desktop`. 若存在, 返回 `StandardCharImageLoginSolver`
* 3. 检测 JVM 桌面环境, 若支持, 返回 `SwingSolver`
* 4. 返回 `StandardCharImageLoginSolver`
*
* @return [SwingSolver] [StandardCharImageLoginSolver] `null`
* @return `SwingSolver` `StandardCharImageLoginSolver` `null`
*/
@JvmField
public val Default: LoginSolver? = when (WindowHelperJvm.platformKind) {
WindowHelperJvm.PlatformKind.ANDROID -> null
WindowHelperJvm.PlatformKind.SWING -> {
when (isSliderCaptchaSupportKind) {
null, false -> SwingSolver
true -> SeleniumLoginSolver ?: SwingSolver
}
}
WindowHelperJvm.PlatformKind.CLI -> StandardCharImageLoginSolver()
}
public val Default: LoginSolver?
@Suppress("unused")
@Deprecated("Binary compatibility", level = DeprecationLevel.HIDDEN)
public fun getDefault(): LoginSolver = Default
?: error("LoginSolver is not provided by default on your platform. Please specify by BotConfiguration.loginSolver")
public fun getDefault(): LoginSolver
}
}
/**
* CLI 环境 [LoginSolver]. 将验证码图片转为字符画并通过 `output` 输出, [input] 获取用户输入.
*
* 使用字符图片展示验证码, 使用 [input] 获取输入, 使用 [loggerSupplier] 输出
*
* @see createBlocking
*/
public class StandardCharImageLoginSolver @JvmOverloads constructor(
input: suspend () -> String = { readLine() ?: throw NoStandardInputForCaptchaException() },
/**
* `null` 时使用 [Bot.logger]
*/
private val loggerSupplier: (bot: Bot) -> MiraiLogger = { it.logger }
) : LoginSolver() {
public constructor(
input: suspend () -> String = { readLine() ?: throw NoStandardInputForCaptchaException() },
overrideLogger: MiraiLogger?
) : this(input, { overrideLogger ?: it.logger })
private val input: suspend () -> String = suspend {
withContext(Dispatchers.IO) { input() }
}
override suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String? = loginSolverLock.withLock {
val logger = loggerSupplier(bot)
@Suppress("BlockingMethodInNonBlockingContext")
withContext(Dispatchers.IO) {
val tempFile: File = File.createTempFile("tmp", ".png").apply { deleteOnExit() }
tempFile.createNewFile()
logger.info { "[PicCaptcha] 需要图片验证码登录, 验证码为 4 字母" }
logger.info { "[PicCaptcha] Picture captcha required. Captcha consists of 4 letters." }
try {
tempFile.writeChannel().apply { writeFully(data); close() }
logger.info { "[PicCaptcha] 将会显示字符图片. 若看不清字符图片, 请查看文件 ${tempFile.absolutePath}" }
logger.info { "[PicCaptcha] Displaying char-image. If not clear, view file ${tempFile.absolutePath}" }
} catch (e: Exception) {
logger.warning("[PicCaptcha] 无法写出验证码文件, 请尝试查看以上字符图片", e)
logger.warning("[PicCaptcha] Failed to export captcha image. Please see the char-image.", e)
}
tempFile.inputStream().use { stream ->
try {
val img = ImageIO.read(stream)
if (img == null) {
logger.warning { "[PicCaptcha] 无法创建字符图片. 请查看文件" }
logger.warning { "[PicCaptcha] Failed to create char-image. Please see the file." }
} else {
logger.info { "[PicCaptcha] \n" + img.createCharImg() }
}
} catch (throwable: Throwable) {
logger.warning("[PicCaptcha] 创建字符图片时出错. 请查看文件.", throwable)
logger.warning("[PicCaptcha] Failed to create char-image. Please see the file.", throwable)
}
}
}
logger.info { "[PicCaptcha] 请输入 4 位字母验证码. 若要更换验证码, 请直接回车" }
logger.info { "[PicCaptcha] Please type 4-letter captcha. Press Enter directly to refresh." }
return input().takeUnless { it.isEmpty() || it.length != 4 }.also {
logger.info { "[PicCaptcha] 正在提交 $it..." }
logger.info { "[PicCaptcha] Submitting $it..." }
}
}
override suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String = loginSolverLock.withLock {
val logger = loggerSupplier(bot)
logger.info { "[SliderCaptcha] 需要滑动验证码, 请在浏览器中打开以下链接并完成验证码, 完成后请输入提示 ticket." }
logger.info { "[SliderCaptcha] Slider captcha required, please open the following link in any browser and solve the captcha. Type ticket here after completion." }
logger.info { "[SliderCaptcha] @see https://github.com/project-mirai/mirai-login-solver-selenium#%E4%B8%8B%E8%BD%BD-chrome-%E6%89%A9%E5%B1%95%E6%8F%92%E4%BB%B6" }
logger.info(url)
return input().also {
logger.info { "[SliderCaptcha] 正在提交中..." }
logger.info { "[SliderCaptcha] Submitting..." }
}
}
override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String = loginSolverLock.withLock {
val logger = loggerSupplier(bot)
logger.info { "[UnsafeLogin] 当前登录环境不安全,服务器要求账户认证。请在 QQ 浏览器打开 $url 并完成验证后输入任意字符。" }
logger.info { "[UnsafeLogin] Account verification required by the server. Please open $url in QQ browser and complete challenge, then type anything here to submit." }
return input().also {
logger.info { "[UnsafeLogin] 正在提交中..." }
logger.info { "[UnsafeLogin] Submitting..." }
}
}
public companion object {
/**
* 创建 Java 阻塞版 [input] [StandardCharImageLoginSolver]
*
* @param input 将在协程 IO 池执行, 可以有阻塞调用
*/
@JvmStatic
public fun createBlocking(input: () -> String, output: MiraiLogger?): StandardCharImageLoginSolver {
return StandardCharImageLoginSolver({ withContext(Dispatchers.IO) { input() } }, output)
}
/**
* 创建 Java 阻塞版 [input] [StandardCharImageLoginSolver]
*
* @param input 将在协程 IO 池执行, 可以有阻塞调用
*/
@JvmStatic
public fun createBlocking(input: () -> String): StandardCharImageLoginSolver {
return StandardCharImageLoginSolver({ withContext(Dispatchers.IO) { input() } })
}
}
}
///////////////////////////////
//////////////// internal
///////////////////////////////
internal fun BotConfiguration.getFileBasedDeviceInfoSupplier(file: () -> File): (Bot) -> DeviceInfo {
return {
file().loadAsDeviceInfo(json)
}
}
// Copied from Ktor CIO
private fun File.writeChannel(
coroutineContext: CoroutineContext = Dispatchers.IO
): ByteWriteChannel = GlobalScope.reader(CoroutineName("file-writer") + coroutineContext, autoFlush = true) {
@Suppress("BlockingMethodInNonBlockingContext")
RandomAccessFile(this@writeChannel, "rw").use { file ->
val copied = channel.copyTo(file.channel)
file.setLength(copied) // truncate tail that could remain from the previously written data
}
}.channel
private val loginSolverLock = Mutex()
/**
* @author NaturalHG
*/
private fun BufferedImage.createCharImg(outputWidth: Int = 100, ignoreRate: Double = 0.95): String {
val newHeight = (this.height * (outputWidth.toDouble() / this.width)).toInt()
val tmp = this.getScaledInstance(outputWidth, newHeight, Image.SCALE_SMOOTH)
val image = BufferedImage(outputWidth, newHeight, BufferedImage.TYPE_INT_ARGB)
val g2d = image.createGraphics()
g2d.drawImage(tmp, 0, 0, null)
fun gray(rgb: Int): Int {
val r = rgb and 0xff0000 shr 16
val g = rgb and 0x00ff00 shr 8
val b = rgb and 0x0000ff
return (r * 30 + g * 59 + b * 11 + 50) / 100
}
fun grayCompare(g1: Int, g2: Int): Boolean =
kotlin.math.min(g1, g2).toDouble() / kotlin.math.max(g1, g2) >= ignoreRate
val background = gray(image.getRGB(0, 0))
return buildString(capacity = height) {
val lines = mutableListOf<StringBuilder>()
var minXPos = outputWidth
var maxXPos = 0
for (y in 0 until image.height) {
val builderLine = StringBuilder()
for (x in 0 until image.width) {
val gray = gray(image.getRGB(x, y))
if (grayCompare(gray, background)) {
builderLine.append(" ")
} else {
builderLine.append("#")
if (x < minXPos) {
minXPos = x
}
if (x > maxXPos) {
maxXPos = x
}
}
}
if (builderLine.toString().isBlank()) {
continue
}
lines.add(builderLine)
}
for (line in lines) {
append(line.substring(minXPos, maxXPos)).append("\n")
}
}
}
}

View File

@ -222,8 +222,8 @@ public inline fun MiraiLogger.error(message: () -> String?, e: Throwable?) {
/**
* 当前平台的默认的日志记录器.
* _JVM 控制台_ 端的实现为 [println]
* _Android_ 端的实现为 `android.util.Log`
* - _JVM 控制台_ 端的实现为 [println]
* - _Android_ 端的实现为 `android.util.Log`
*
*
* 单条日志格式 (正则) :
@ -244,14 +244,9 @@ public inline fun MiraiLogger.error(message: () -> String?, e: Throwable?) {
* @see MiraiLogger.create
*/
@MiraiInternalApi
public expect open class PlatformLogger constructor(
public expect open class PlatformLogger @JvmOverloads constructor(
identity: String? = "Mirai",
output: (String) -> Unit, // TODO: 2020/11/30 review logs, currently it's just for compile
) : MiraiLoggerPlatformBase {
@JvmOverloads
public constructor(identity: String? = "Mirai")
}
) : MiraiLoggerPlatformBase
/**
* 不做任何事情的 logger, keep silent.

View File

@ -0,0 +1,28 @@
/*
* Copyright 2019-2021 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:JvmName("FileLoggerKt") // bin-comp
package net.mamoe.mirai.utils
import java.io.File
/**
* 将日志写入('append')到特定文件.
*
* @see PlatformLogger 查看格式信息
*/
public expect class SingleFileLogger : MiraiLogger {
public constructor(identity: String)
public constructor(identity: String, file: File = File("$identity-${getCurrentDate()}.log"))
// Implementation notes v2.5.0:
// default argument `file` to produce synthetic constructor with `DefaultConstructorMarker` for binary compatibility
// dedicated constructor with single parameter `identity` for the same reason.
}

View File

@ -0,0 +1,292 @@
/*
* Copyright 2019-2021 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.utils
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.io.ByteWriteChannel
import kotlinx.coroutines.io.close
import kotlinx.coroutines.io.jvm.nio.copyTo
import kotlinx.coroutines.io.reader
import kotlinx.coroutines.io.writeFully
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import net.mamoe.mirai.Bot
import net.mamoe.mirai.internal.utils.SeleniumLoginSolver
import net.mamoe.mirai.internal.utils.isSliderCaptchaSupportKind
import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.network.NoStandardInputForCaptchaException
import net.mamoe.mirai.utils.LoginSolver.Companion.Default
import net.mamoe.mirai.utils.StandardCharImageLoginSolver.Companion.createBlocking
import java.awt.Image
import java.awt.image.BufferedImage
import java.io.File
import java.io.RandomAccessFile
import javax.imageio.ImageIO
import kotlin.coroutines.CoroutineContext
/**
* 验证码, 设备锁解决器
*
* @see Default
* @see BotConfiguration.loginSolver
*/
public actual abstract class LoginSolver public actual constructor() {
/**
* 处理图片验证码.
*
* 返回 `null` 以表示无法处理验证码, 将会刷新验证码或重试登录.
* 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止
*
* @throws LoginFailedException
*/
public actual abstract suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String?
/**
* `true` 表示支持滑动验证码, 遇到滑动验证码时 mirai 会请求 [onSolveSliderCaptcha].
* 否则会跳过滑动验证码并告诉服务器此客户端不支持, 有可能导致登录失败
*/
public actual open val isSliderCaptchaSupported: Boolean
get() = isSliderCaptchaSupportKind ?: true
/**
* 处理滑动验证码.
*
* 返回 `null` 以表示无法处理验证码, 将会刷新验证码或重试登录.
* 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止
*
* @throws LoginFailedException
* @return 验证码解决成功后获得的 ticket.
*/
public actual abstract suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String?
/**
* 处理不安全设备验证.
*
* 返回值保留给将来使用. 目前在处理完成后返回任意内容 (包含 `null`) 均视为处理成功.
* 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止.
*
* @return 任意内容. 返回值保留以供未来更新.
* @throws LoginFailedException
*/
public actual abstract suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String?
public actual companion object {
/**
* 当前平台默认的 [LoginSolver]
*
* 检测策略:
* 1. 检测 `android.util.Log`, 如果存在, 返回 `null`.
* 2. 检测 JVM 属性 `mirai.no-desktop`. 若存在, 返回 [StandardCharImageLoginSolver]
* 3. 检测 JVM 桌面环境, 若支持, 返回 [SwingSolver]
* 4. 返回 [StandardCharImageLoginSolver]
*
* @return [SwingSolver] [StandardCharImageLoginSolver] `null`
*/
@JvmField
public actual val Default: LoginSolver? = when (WindowHelperJvm.platformKind) {
WindowHelperJvm.PlatformKind.ANDROID -> null
WindowHelperJvm.PlatformKind.SWING -> {
when (isSliderCaptchaSupportKind) {
null, false -> SwingSolver
true -> SeleniumLoginSolver ?: SwingSolver
}
}
WindowHelperJvm.PlatformKind.CLI -> StandardCharImageLoginSolver()
}
@Suppress("unused")
@Deprecated("Binary compatibility", level = DeprecationLevel.HIDDEN)
public actual fun getDefault(): LoginSolver = Default
?: error("LoginSolver is not provided by default on your platform. Please specify by BotConfiguration.loginSolver")
}
}
/**
* CLI 环境 [LoginSolver]. 将验证码图片转为字符画并通过 `output` 输出, [input] 获取用户输入.
*
* 使用字符图片展示验证码, 使用 [input] 获取输入, 使用 [loggerSupplier] 输出
*
* @see createBlocking
*/
public class StandardCharImageLoginSolver @JvmOverloads constructor(
input: suspend () -> String = { readLine() ?: throw NoStandardInputForCaptchaException() },
/**
* `null` 时使用 [Bot.logger]
*/
private val loggerSupplier: (bot: Bot) -> MiraiLogger = { it.logger }
) : LoginSolver() {
public constructor(
input: suspend () -> String = { readLine() ?: throw NoStandardInputForCaptchaException() },
overrideLogger: MiraiLogger?
) : this(input, { overrideLogger ?: it.logger })
private val input: suspend () -> String = suspend {
withContext(Dispatchers.IO) { input() }
}
override suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String? = loginSolverLock.withLock {
val logger = loggerSupplier(bot)
@Suppress("BlockingMethodInNonBlockingContext")
(withContext(Dispatchers.IO) {
val tempFile: File = File.createTempFile("tmp", ".png").apply { deleteOnExit() }
tempFile.createNewFile()
logger.info { "[PicCaptcha] 需要图片验证码登录, 验证码为 4 字母" }
logger.info { "[PicCaptcha] Picture captcha required. Captcha consists of 4 letters." }
try {
tempFile.writeChannel().apply { writeFully(data); close() }
logger.info { "[PicCaptcha] 将会显示字符图片. 若看不清字符图片, 请查看文件 ${tempFile.absolutePath}" }
logger.info { "[PicCaptcha] Displaying char-image. If not clear, view file ${tempFile.absolutePath}" }
} catch (e: Exception) {
logger.warning("[PicCaptcha] 无法写出验证码文件, 请尝试查看以上字符图片", e)
logger.warning("[PicCaptcha] Failed to export captcha image. Please see the char-image.", e)
}
tempFile.inputStream().use { stream ->
try {
val img = ImageIO.read(stream)
if (img == null) {
logger.warning { "[PicCaptcha] 无法创建字符图片. 请查看文件" }
logger.warning { "[PicCaptcha] Failed to create char-image. Please see the file." }
} else {
logger.info { "[PicCaptcha] \n" + img.createCharImg() }
}
} catch (throwable: Throwable) {
logger.warning("[PicCaptcha] 创建字符图片时出错. 请查看文件.", throwable)
logger.warning("[PicCaptcha] Failed to create char-image. Please see the file.", throwable)
}
}
})
logger.info { "[PicCaptcha] 请输入 4 位字母验证码. 若要更换验证码, 请直接回车" }
logger.info { "[PicCaptcha] Please type 4-letter captcha. Press Enter directly to refresh." }
return input().takeUnless { it.isEmpty() || it.length != 4 }.also {
logger.info { "[PicCaptcha] 正在提交 $it..." }
logger.info { "[PicCaptcha] Submitting $it..." }
}
}
override suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String = loginSolverLock.withLock {
val logger = loggerSupplier(bot)
logger.info { "[SliderCaptcha] 需要滑动验证码, 请在浏览器中打开以下链接并完成验证码, 完成后请输入提示 ticket." }
logger.info { "[SliderCaptcha] Slider captcha required, please open the following link in any browser and solve the captcha. Type ticket here after completion." }
logger.info { "[SliderCaptcha] @see https://github.com/project-mirai/mirai-login-solver-selenium#%E4%B8%8B%E8%BD%BD-chrome-%E6%89%A9%E5%B1%95%E6%8F%92%E4%BB%B6" }
logger.info(url)
return input().also {
logger.info { "[SliderCaptcha] 正在提交中..." }
logger.info { "[SliderCaptcha] Submitting..." }
}
}
override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String = loginSolverLock.withLock {
val logger = loggerSupplier(bot)
logger.info { "[UnsafeLogin] 当前登录环境不安全,服务器要求账户认证。请在 QQ 浏览器打开 $url 并完成验证后输入任意字符。" }
logger.info { "[UnsafeLogin] Account verification required by the server. Please open $url in QQ browser and complete challenge, then type anything here to submit." }
return input().also {
logger.info { "[UnsafeLogin] 正在提交中..." }
logger.info { "[UnsafeLogin] Submitting..." }
}
}
public companion object {
/**
* 创建 Java 阻塞版 [input] [StandardCharImageLoginSolver]
*
* @param input 将在协程 IO 池执行, 可以有阻塞调用
*/
@JvmStatic
public fun createBlocking(input: () -> String, output: MiraiLogger?): StandardCharImageLoginSolver {
return StandardCharImageLoginSolver({ withContext(Dispatchers.IO) { input() } }, output)
}
/**
* 创建 Java 阻塞版 [input] [StandardCharImageLoginSolver]
*
* @param input 将在协程 IO 池执行, 可以有阻塞调用
*/
@JvmStatic
public fun createBlocking(input: () -> String): StandardCharImageLoginSolver {
return StandardCharImageLoginSolver({ withContext(Dispatchers.IO) { input() } })
}
}
}
// Copied from Ktor CIO
private fun File.writeChannel(
coroutineContext: CoroutineContext = Dispatchers.IO
): ByteWriteChannel = GlobalScope.reader(CoroutineName("file-writer") + coroutineContext, autoFlush = true) {
@Suppress("BlockingMethodInNonBlockingContext")
RandomAccessFile(this@writeChannel, "rw").use { file ->
val copied = channel.copyTo(file.channel)
file.setLength(copied) // truncate tail that could remain from the previously written data
}
}.channel
private val loginSolverLock = Mutex()
/**
* @author NaturalHG
*/
private fun BufferedImage.createCharImg(outputWidth: Int = 100, ignoreRate: Double = 0.95): String {
val newHeight = (this.height * (outputWidth.toDouble() / this.width)).toInt()
val tmp = this.getScaledInstance(outputWidth, newHeight, Image.SCALE_SMOOTH)
val image = BufferedImage(outputWidth, newHeight, BufferedImage.TYPE_INT_ARGB)
val g2d = image.createGraphics()
g2d.drawImage(tmp, 0, 0, null)
fun gray(rgb: Int): Int {
val r = rgb and 0xff0000 shr 16
val g = rgb and 0x00ff00 shr 8
val b = rgb and 0x0000ff
return (r * 30 + g * 59 + b * 11 + 50) / 100
}
fun grayCompare(g1: Int, g2: Int): Boolean =
kotlin.math.min(g1, g2).toDouble() / kotlin.math.max(g1, g2) >= ignoreRate
val background = gray(image.getRGB(0, 0))
return buildString(capacity = height) {
val lines = mutableListOf<StringBuilder>()
var minXPos = outputWidth
var maxXPos = 0
for (y in 0 until image.height) {
val builderLine = StringBuilder()
for (x in 0 until image.width) {
val gray = gray(image.getRGB(x, y))
if (grayCompare(gray, background)) {
builderLine.append(" ")
} else {
builderLine.append("#")
if (x < minXPos) {
minXPos = x
}
if (x > maxXPos) {
maxXPos = x
}
}
}
if (builderLine.toString().isBlank()) {
continue
}
lines.add(builderLine)
}
for (line in lines) {
append(line.substring(minXPos, maxXPos)).append("\n")
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
* Copyright 2019-2021 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@ -41,7 +41,7 @@ import java.util.*
* @see DirectoryLogger 在一个目录中按日期存放文件记录日志, 自动清理过期日志
*/
@MiraiInternalApi
public actual open class PlatformLogger constructor(
public actual open class PlatformLogger constructor( // same as StdoutLogger but doesn't matter
public override val identity: String? = "Mirai",
/**
* 日志输出. 不会自动添加换行
@ -49,8 +49,11 @@ public actual open class PlatformLogger constructor(
public open val output: (String) -> Unit,
public val isColored: Boolean = true
) : MiraiLoggerPlatformBase() {
// PlatformLogger("") resolves to this one.
public actual constructor(identity: String?) : this(identity, ::println)
public actual constructor(identity: String?, output: (String) -> Unit) : this(identity, output, true)
public constructor(identity: String?, output: (String) -> Unit = ::println) : this(identity, output, true)
/**
* 输出一条日志. [message] 末尾可能不带换行符.
@ -75,33 +78,34 @@ public actual open class PlatformLogger constructor(
public override fun verbose0(message: String?): Unit = printLog(message, SimpleLogger.LogPriority.VERBOSE)
public override fun verbose0(message: String?, e: Throwable?) {
if (e != null) verbose((message ?: e.toString()) + "\n${e.stackTraceString}")
if (e != null) verbose((message ?: e.toString()) + "\n${e.stackTraceToString()}")
else verbose(message.toString())
}
public override fun info0(message: String?): Unit = printLog(message, SimpleLogger.LogPriority.INFO)
public override fun info0(message: String?, e: Throwable?) {
if (e != null) info((message ?: e.toString()) + "\n${e.stackTraceString}")
if (e != null) info((message ?: e.toString()) + "\n${e.stackTraceToString()}")
else info(message.toString())
}
public override fun warning0(message: String?): Unit = printLog(message, SimpleLogger.LogPriority.WARNING)
public override fun warning0(message: String?, e: Throwable?) {
if (e != null) warning((message ?: e.toString()) + "\n${e.stackTraceString}")
if (e != null) warning((message ?: e.toString()) + "\n${e.stackTraceToString()}")
else warning(message.toString())
}
public override fun error0(message: String?): Unit = printLog(message, SimpleLogger.LogPriority.ERROR)
public override fun error0(message: String?, e: Throwable?) {
if (e != null) error((message ?: e.toString()) + "\n${e.stackTraceString}")
if (e != null) error((message ?: e.toString()) + "\n${e.stackTraceToString()}")
else error(message.toString())
}
public override fun debug0(message: String?): Unit = printLog(message, SimpleLogger.LogPriority.DEBUG)
public override fun debug0(message: String?, e: Throwable?) {
if (e != null) debug((message ?: e.toString()) + "\n${e.stackTraceString}")
if (e != null) debug((message ?: e.toString()) + "\n${e.stackTraceToString()}")
else debug(message.toString())
}
protected open val timeFormat: DateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.SIMPLIFIED_CHINESE)
private val currentTimeFormatted get() = timeFormat.format(Date())
@ -129,8 +133,4 @@ public actual open class PlatformLogger constructor(
override fun toString(): String = format
}
}
@get:JvmSynthetic
internal val Throwable.stackTraceString
get() = this.stackTraceToString()
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2019-2021 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("unused")
package net.mamoe.mirai.utils
import java.io.File
/**
* 将日志写入('append')到特定文件.
*
* @see PlatformLogger 查看格式信息
*/
public actual class SingleFileLogger actual constructor(
identity: String,
file: File
) : MiraiLogger, PlatformLogger(identity, { file.appendText(it + "\n") }) {
// Implementation notes v2.5.0:
// Extending `PlatformLogger` for binary compatibility for JVM target only.
// See actual declaration in androidMain for a better impl (implements `MiraiLogger` only)
public actual constructor(identity: String) : this(identity, File("$identity-${getCurrentDate()}.log"))
init {
file.createNewFile()
require(file.isFile) { "Log file must be a file: $file" }
require(file.canWrite()) { "Log file must be write: $file" }
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
* Copyright 2019-2021 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.

View File

@ -15,7 +15,7 @@ plugins {
kotlin("multiplatform")
kotlin("plugin.serialization")
id("kotlinx-atomicfu")
//id("kotlinx-atomicfu")
id("net.mamoe.kotlin-jvm-blocking-bridge")
`maven-publish`
id("com.jfrog.bintray")
@ -23,29 +23,20 @@ plugins {
description = "mirai-core utilities"
val isAndroidSDKAvailable: Boolean by project
kotlin {
explicitApi()
if (isAndroidSDKAvailable) {
apply(from = rootProject.file("gradle/android.gradle"))
android("android") {
publishAllLibraryVariants()
// apply(from = rootProject.file("gradle/android.gradle"))
// android("android") {
// publishAllLibraryVariants()
// }
jvm("android") {
attributes.attribute(KotlinPlatformType.attribute, KotlinPlatformType.androidJvm)
// publishAllLibraryVariants()
}
} else {
println(
"""Android SDK 可能未安装.
$name 的 Android 目标编译将不会进行.
这不会影响 Android 以外的平台的编译.
""".trimIndent()
)
println(
"""Android SDK might not be installed.
Android target of $name will not be compiled.
It does no influence on the compilation of other platforms.
""".trimIndent()
)
printAndroidNotInstalled()
}
jvm("common") {
@ -79,8 +70,10 @@ kotlin {
}
if (isAndroidSDKAvailable) {
androidMain {
val androidMain by getting {
//
dependencies {
compileOnly(`android-runtime`)
api1(`ktor-client-android`)
}
}
@ -96,6 +89,19 @@ kotlin {
}
}
tasks.register("checkAndroidApiLevel") {
doFirst {
androidutil.AndroidApiLevelCheck.check(
buildDir.resolve("classes/kotlin/android/main"),
project.property("mirai.android.target.api.level")!!.toString().toInt(),
project
)
}
group = "verification"
this.mustRunAfter("androidMainClasses")
}
tasks.getByName("androidTest").dependsOn("checkAndroidApiLevel")
fun org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler.implementation1(dependencyNotation: String) =
implementation(dependencyNotation) {
exclude("org.jetbrains.kotlin", "kotlin-stdlib")

View File

@ -0,0 +1,20 @@
/*
* Copyright 2019-2021 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:JvmMultifileClass
@file:Suppress("NOTHING_TO_INLINE")
package net.mamoe.mirai.utils
import android.util.Base64
public actual fun ByteArray.encodeToBase64(): String {
return Base64.encodeToString(this, Base64.DEFAULT)
}

View File

@ -152,8 +152,7 @@ public fun UByteArray.toUHexString(separator: String = " ", offset: Int = 0, len
public inline fun ByteArray.encodeToString(offset: Int = 0, charset: Charset = Charsets.UTF_8): String =
kotlinx.io.core.String(this, charset = charset, offset = offset, length = this.size - offset)
public inline fun ByteArray.encodeToBase64(): String =
Base64.getEncoder().encodeToString(this)
public expect fun ByteArray.encodeToBase64(): String
public inline fun ByteArray.toReadPacket(offset: Int = 0, length: Int = this.size - offset): ByteReadPacket =
ByteReadPacket(this, offset = offset, length = length)

View File

@ -131,7 +131,7 @@ public fun Input.copyTo(out: OutputStream, bufferSize: Int = DEFAULT_BUFFER_SIZE
return bytesCopied
}
public inline fun <I : AutoCloseable, O : AutoCloseable, R> I.withOut(output: O, block: I.(output: O) -> R): R {
public inline fun <I : Closeable, O : Closeable, R> I.withOut(output: O, block: I.(output: O) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}

View File

@ -157,3 +157,10 @@ public inline fun <R> runCatchingExceptions(block: () -> R): Result<R> {
Result.failure(e)
}
}
public inline fun <E> MutableList<E>.replaceAllKotlin(operator: (E) -> E) {
val li: MutableListIterator<E> = this.listIterator()
while (li.hasNext()) {
li.set(operator(li.next()))
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2019-2021 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:JvmMultifileClass
@file:Suppress("NOTHING_TO_INLINE")
package net.mamoe.mirai.utils
import java.util.*
public actual fun ByteArray.encodeToBase64(): String {
return Base64.getEncoder().encodeToString(this)
}

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2019-2020 Mamoe Technologies and contributors.
~
~ 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
~ Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
~
~ https://github.com/mamoe/mirai/blob/master/LICENSE
-->
<manifest package="net.mamoe.mirai" xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
</manifest>

View File

@ -18,13 +18,10 @@ plugins {
id("net.mamoe.kotlin-jvm-blocking-bridge")
`maven-publish`
id("com.jfrog.bintray")
java
}
description = "Mirai Protocol implementation for QQ Android"
val isAndroidSDKAvailable: Boolean by project
afterEvaluate {
tasks.getByName("compileKotlinCommon").enabled = false
tasks.getByName("compileTestKotlinCommon").enabled = false
@ -37,9 +34,13 @@ kotlin {
explicitApi()
if (isAndroidSDKAvailable) {
apply(from = rootProject.file("gradle/android.gradle"))
android("android") {
publishAllLibraryVariants()
// apply(from = rootProject.file("gradle/android.gradle"))
// android("android") {
// publishAllLibraryVariants()
// }
jvm("android") {
attributes.attribute(KotlinPlatformType.attribute, KotlinPlatformType.androidJvm)
// publishAllLibraryVariants()
}
} else {
printAndroidNotInstalled()
@ -58,7 +59,7 @@ kotlin {
sourceSets.apply {
commonMain {
val commonMain by getting {
dependencies {
api(project(":mirai-core-api"))
implementation(project(":mirai-core-utils"))
@ -81,12 +82,13 @@ kotlin {
}
if (isAndroidSDKAvailable) {
androidMain {
val androidMain by getting {
dependsOn(commonMain)
dependencies {
compileOnly(`android-runtime`)
}
}
androidTest {
val androidTest by getting {
dependencies {
implementation(kotlin("test", Versions.kotlinCompiler))
implementation(kotlin("test-junit", Versions.kotlinCompiler))
@ -96,22 +98,35 @@ kotlin {
}
}
jvmMain {
val jvmMain by getting {
dependencies {
implementation("org.bouncycastle:bcprov-jdk15on:1.64")
// api(kotlinx("coroutines-debug", Versions.coroutines))
}
}
jvmTest {
val jvmTest by getting {
dependencies {
implementation("org.pcap4j:pcap4j-distribution:1.8.2")
// implementation("net.mamoe:mirai-login-solver-selenium:1.0-dev-14")
// implementation("net.mamoe:mirai-login-solver-selenium:1.0-dev-14")
}
}
}
}
tasks.register("checkAndroidApiLevel") {
doFirst {
androidutil.AndroidApiLevelCheck.check(
buildDir.resolve("classes/kotlin/android/main"),
project.property("mirai.android.target.api.level")!!.toString().toInt(),
project
)
}
group = "verification"
this.mustRunAfter("androidMainClasses")
}
tasks.getByName("androidTest").dependsOn("checkAndroidApiLevel")
fun org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler.implementation1(dependencyNotation: String) =
implementation(dependencyNotation) {
exclude("org.jetbrains.kotlin", "kotlin-stdlib")

View File

@ -0,0 +1,94 @@
/*
* Copyright 2019-2021 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.internal.utils.crypto
import net.mamoe.mirai.utils.md5
import java.security.*
import java.security.spec.ECGenParameterSpec
import java.security.spec.X509EncodedKeySpec
import javax.crypto.KeyAgreement
@Suppress("ACTUAL_WITHOUT_EXPECT")
internal actual typealias ECDHPrivateKey = PrivateKey
@Suppress("ACTUAL_WITHOUT_EXPECT")
internal actual typealias ECDHPublicKey = PublicKey
internal actual class ECDHKeyPairImpl(
private val delegate: KeyPair
) : ECDHKeyPair {
override val privateKey: ECDHPrivateKey get() = delegate.private
override val publicKey: ECDHPublicKey get() = delegate.public
override val initialShareKey: ByteArray by lazy { ECDH.calculateShareKey(privateKey, initialPublicKey) }
}
internal actual fun ECDH() = ECDH(ECDH.generateKeyPair())
internal actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
actual companion object {
private const val curveName = "secp192k1" // p-256
actual val isECDHAvailable: Boolean
init {
isECDHAvailable = kotlin.runCatching {
fun testECDH() {
ECDHKeyPairImpl(
KeyPairGenerator.getInstance("ECDH")
.also { it.initialize(ECGenParameterSpec(curveName)) }
.genKeyPair()).let {
calculateShareKey(it.privateKey, it.publicKey)
}
}
if (kotlin.runCatching { testECDH() }.isSuccess) {
return@runCatching
}
testECDH()
}.onFailure {
it.printStackTrace()
}.isSuccess
}
actual fun generateKeyPair(): ECDHKeyPair {
if (!isECDHAvailable) {
return ECDHKeyPair.DefaultStub
}
return ECDHKeyPairImpl(
KeyPairGenerator.getInstance("ECDH")
.also { it.initialize(ECGenParameterSpec(curveName)) }
.genKeyPair())
}
actual fun calculateShareKey(
privateKey: ECDHPrivateKey,
publicKey: ECDHPublicKey
): ByteArray {
val instance = KeyAgreement.getInstance("ECDH", "BC")
instance.init(privateKey)
instance.doPhase(publicKey, true)
return instance.generateSecret().md5()
}
actual fun constructPublicKey(key: ByteArray): ECDHPublicKey {
return KeyFactory.getInstance("EC", "BC").generatePublic(X509EncodedKeySpec(key))
}
}
actual fun calculateShareKeyByPeerPublicKey(peerPublicKey: ECDHPublicKey): ByteArray {
return calculateShareKey(keyPair.privateKey, peerPublicKey)
}
actual override fun toString(): String {
return "ECDH(keyPair=$keyPair)"
}
}

View File

@ -29,6 +29,8 @@ include(":mirai-core")
include(":mirai-core-all")
include(":binary-compatibility-validator")
include(":binary-compatibility-validator-android")
project(":binary-compatibility-validator-android").projectDir = file("binary-compatibility-validator/android")
include(":ci-release-helper")