diff --git a/build.gradle b/build.gradle index ce4dfd3b8..a9f99a31b 100644 --- a/build.gradle +++ b/build.gradle @@ -2,12 +2,12 @@ buildscript { ext.kotlin_version = '1.3.50' repositories { - google() + mavenLocal() + jcenter() + mavenCentral() maven { url "https://mirrors.huaweicloud.com/repository/maven/" } } - apply from: rootProject.file('dependencies.gradle') - dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlinx:atomicfu-gradle-plugin:$atomicfu_version" @@ -16,14 +16,12 @@ buildscript { allprojects { group = "net.mamoe" - version = "1.0" + version = getProperty("mirai_version") repositories { - jcenter()//klock - google() + mavenLocal() + mavenCentral() + jcenter() maven { url "https://mirrors.huaweicloud.com/repository/maven/" } } - - apply from: rootProject.file('dependencies.gradle') -} - +} \ No newline at end of file diff --git a/dependencies.gradle b/dependencies.gradle deleted file mode 100644 index fbe4285a8..000000000 --- a/dependencies.gradle +++ /dev/null @@ -1,44 +0,0 @@ -ext { - // dependencies - - // kotlin - kotlinJvm = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - kotlinCommon = "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlin_version" - kotlinNative = "org.jetbrains.kotlin:kotlin-stdlib-native:$kotlin_version" - - // kotlinx.coroutine - coroutine_version = "1.3.0" - coroutine = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version" - coroutineCommon = "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:$coroutine_version" - coroutineNative = "org.jetbrains.kotlinx:kotlinx-coroutines-core-native:$coroutine_version" - coroutineAndroid = "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version" - coroutineJs = "org.jetbrains.kotlinx:kotlinx-coroutines-core-js:$coroutine_version" - - coroutineIo = "org.jetbrains.kotlinx:kotlinx-coroutines-io:0.24.0" - - // kotlin.reflect - reflect = "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - - // kotlinx.atomicfu - atomicfu_version = "0.13.1" - atomicFUCommon = "org.jetbrains.kotlinx:atomicfu-common:$atomicfu_version" - - // kotlinx.io - kotlinx_io_version = "0.1.15" - kotlinxIOJvm = "org.jetbrains.kotlinx:kotlinx-io-jvm:$kotlinx_io_version" - kotlinxIOCommon = "org.jetbrains.kotlinx:kotlinx-io:$kotlinx_io_version" - kotlinxIOJS = "org.jetbrains.kotlinx:kotlinx-io-js:$kotlinx_io_version" - kotlinxIONative = "org.jetbrains.kotlinx:kotlinx-io-native:$kotlinx_io_version" - - // klock - klock = "com.soywiz.korlibs.klock:klock:1.7.0" - - // ktor - ktor_version = "1.2.4" - ktorClientCore = "io.ktor:ktor-client-core:$ktor_version" - ktorClientCoreNative = "io.ktor:ktor-client-core-native:$ktor_version" - ktorClientCoreJvm = "io.ktor:ktor-client-core-jvm:$ktor_version" - ktorClientCio = "io.ktor:ktor-client-cio:$ktor_version" - ktorHttp = "io.ktor:ktor-http:$ktor_version" - ktorHttpCio = "io.ktor:ktor-http-cio:$ktor_version" -} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..a6068cb82 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,18 @@ +# style guide +kotlin.code.style=official +# config +mirai_version=1.0.0 +kotlin.incremental.multiplatform=true +kotlin.parallel.tasks.in.project=true +# kotlin +kotlin_version=1.3.50 +# kotlin libraries +serialization_version=0.13.0 +coroutines_version=1.3.2 +atomicfu_version=0.13.0 +ktor_version=1.2.4 +ktorio_version=1.3.0-beta-1 +klock_version=1.7.0 +kotlinxio_version=0.1.15 +coroutinesio_version=0.24.0 +# utility diff --git a/mirai-console/build.gradle b/mirai-console/build.gradle index c5a2254b4..19daa8a75 100644 --- a/mirai-console/build.gradle +++ b/mirai-console/build.gradle @@ -1,18 +1,6 @@ apply plugin: "kotlin" -apply plugin: "application" apply plugin: "java" dependencies { - compile project(':mirai-core') - - compile rootProject.ext.kotlinCommon - compile rootProject.ext.kotlinJvm - compile rootProject.ext.reflect - compile rootProject.ext.coroutine -} - -sourceCompatibility = "11" - -tasks.withType(JavaCompile) { - options.encoding = "UTF-8" + implementation project(':mirai-core') } diff --git a/mirai-core/build.gradle b/mirai-core/build.gradle index 8c5b56260..6e3d69b36 100644 --- a/mirai-core/build.gradle +++ b/mirai-core/build.gradle @@ -23,24 +23,21 @@ kotlin { sourceSets { commonMain { dependencies { - // https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-reflect - implementation rootProject.ext.kotlinCommon + api group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-common', version: kotlin_version + api group: 'org.jetbrains.kotlin', name: 'kotlin-reflect', version: kotlin_version + api group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core-common', version: coroutines_version + api group: 'org.jetbrains.kotlinx', name: 'atomicfu-common', version: atomicfu_version + api group: 'org.jetbrains.kotlinx', name: 'kotlinx-io', version: kotlinxio_version + api group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-io', version: coroutinesio_version - implementation rootProject.ext.reflect + implementation "com.soywiz.korlibs.klock:klock:$klock_version" - //implementation rootProject.ext.coroutine - implementation rootProject.ext.coroutineCommon - implementation rootProject.ext.coroutineIo - - implementation rootProject.ext.atomicFUCommon - - implementation rootProject.ext.kotlinxIOCommon - implementation rootProject.ext.klock - - implementation rootProject.ext.ktorClientCore - implementation rootProject.ext.ktorClientCio - implementation rootProject.ext.ktorHttp - implementation rootProject.ext.ktorHttpCio + api group: 'io.ktor', name: 'ktor-client-core', version: ktor_version + //api group: 'io.ktor', name: 'ktor-client-cio', version: ktor_version + //api group: 'io.ktor', name: 'ktor-client', version: ktor_version + api group: 'io.ktor', name: 'ktor-http', version: ktor_version + //api group: 'io.ktor', name: 'ktor-utils', version: ktor_version + //api group: 'io.ktor', name: 'ktor-io', version: ktorio_version } } @@ -48,22 +45,23 @@ kotlin { apply plugin: 'java' dependencies { - implementation rootProject.ext.kotlinJvm - implementation rootProject.ext.reflect - implementation rootProject.ext.coroutine + api group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib', version: kotlin_version + api group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8', version: kotlin_version + api group: 'org.jetbrains.kotlin', name: 'kotlin-reflect', version: kotlin_version + api group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: coroutines_version + api group: 'org.jetbrains.kotlinx', name: 'atomicfu', version: atomicfu_version + api group: 'org.jetbrains.kotlinx', name: 'kotlinx-io', version: kotlinxio_version + // api group: 'org.jetbrains.kotlinx', name: 'kotlinx-io-jvm', version: kotlinxio_version + api group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-io', version: coroutinesio_version - implementation rootProject.ext.kotlinxIOJvm + api group: 'io.ktor', name: 'ktor-http-cio', version: ktor_version + api group: 'io.ktor', name: 'ktor-http', version: ktor_version + api group: 'io.ktor', name: 'ktor-client-core-jvm', version: ktor_version + api group: 'io.ktor', name: 'ktor-client-cio', version: ktor_version implementation 'org.yaml:snakeyaml:1.18' implementation 'org.jsoup:jsoup:1.12.1' implementation 'org.ini4j:ini4j:0.5.2' - implementation rootProject.ext.klock - - implementation rootProject.ext.ktorClientCore - implementation rootProject.ext.ktorClientCoreJvm - implementation rootProject.ext.ktorClientCio - implementation rootProject.ext.ktorHttp - implementation rootProject.ext.ktorHttpCio } } diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt index 89efdd9f9..a83aac3dc 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt @@ -110,7 +110,7 @@ class Bot(val account: BotAccount, val logger: MiraiLogger) { } } - fun close() { + suspend fun close() { this.network.close() this.contacts.groups.clear() this.contacts.qqs.clear() diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/Event.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/Event.kt index 6a43300c1..266d3eaf4 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/Event.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/Event.kt @@ -4,13 +4,14 @@ package net.mamoe.mirai.event import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.newCoroutineContext import kotlinx.coroutines.withContext import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.event.internal.broadcastInternal import net.mamoe.mirai.network.BotNetworkHandler import net.mamoe.mirai.utils.log import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext import kotlin.jvm.JvmOverloads /** @@ -57,17 +58,8 @@ interface Cancellable { */ @Suppress("UNCHECKED_CAST") @JvmOverloads -suspend fun <E : Event> E.broadcast(context: CoroutineContext? = CoroutineExceptionHandler { _, e -> e.log() }): E { - var ctx = EventScope.coroutineContext - if (context == null) { - ctx += CoroutineExceptionHandler { _, e -> e.log() } - } else { - ctx += context - if (context[CoroutineExceptionHandler] == null) { - ctx += CoroutineExceptionHandler { _, e -> e.log() } - } - } - return withContext(ctx) { this@broadcast.broadcastInternal() } +suspend fun <E : Event> E.broadcast(context: CoroutineContext = EmptyCoroutineContext): E { + return withContext(EventScope.newCoroutineContext(context)) { this@broadcast.broadcastInternal() } } /** @@ -77,4 +69,6 @@ suspend fun <E : Event> E.broadcast(context: CoroutineContext? = CoroutineExcept * 然而, 若在事件处理过程中使用到 [Contact.sendMessage] 等会 [发送数据包][BotNetworkHandler.sendPacket] 的方法, * 发送过程将会通过 [withContext] 将协程切换到 [BotNetworkHandler.NetworkScope] */ -object EventScope : CoroutineScope by CoroutineScope(Dispatchers.Default)//todo may change \ No newline at end of file +object EventScope : CoroutineScope { + override val coroutineContext: CoroutineContext = EmptyCoroutineContext +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/Subscribers.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/Subscribers.kt index 250130ac3..ce953b997 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/Subscribers.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/Subscribers.kt @@ -21,44 +21,72 @@ enum class ListeningStatus { // region 顶层方法 -inline fun <reified E : Event> subscribe(noinline handler: suspend (E) -> ListeningStatus) = E::class.subscribe(handler) +suspend inline fun <reified E : Event> subscribe(noinline handler: suspend (E) -> ListeningStatus) = + E::class.subscribe(handler) -inline fun <reified E : Event> subscribeAlways(noinline listener: suspend (E) -> Unit) = E::class.subscribeAlways(listener) +suspend inline fun <reified E : Event> subscribeAlways(noinline listener: suspend (E) -> Unit) = + E::class.subscribeAlways(listener) -inline fun <reified E : Event> subscribeOnce(noinline listener: suspend (E) -> Unit) = E::class.subscribeOnce(listener) +suspend inline fun <reified E : Event> subscribeOnce(noinline listener: suspend (E) -> Unit) = + E::class.subscribeOnce(listener) -inline fun <reified E : Event, T> subscribeUntil(valueIfStop: T, noinline listener: suspend (E) -> T) = E::class.subscribeUntil(valueIfStop, listener) -inline fun <reified E : Event> subscribeUntilFalse(noinline listener: suspend (E) -> Boolean) = E::class.subscribeUntilFalse(listener) -inline fun <reified E : Event> subscribeUntilTrue(noinline listener: suspend (E) -> Boolean) = E::class.subscribeUntilTrue(listener) -inline fun <reified E : Event> subscribeUntilNull(noinline listener: suspend (E) -> Any?) = E::class.subscribeUntilNull(listener) +suspend inline fun <reified E : Event, T> subscribeUntil(valueIfStop: T, noinline listener: suspend (E) -> T) = + E::class.subscribeUntil(valueIfStop, listener) + +suspend inline fun <reified E : Event> subscribeUntilFalse(noinline listener: suspend (E) -> Boolean) = + E::class.subscribeUntilFalse(listener) + +suspend inline fun <reified E : Event> subscribeUntilTrue(noinline listener: suspend (E) -> Boolean) = + E::class.subscribeUntilTrue(listener) + +suspend inline fun <reified E : Event> subscribeUntilNull(noinline listener: suspend (E) -> Any?) = + E::class.subscribeUntilNull(listener) -inline fun <reified E : Event, T> subscribeWhile(valueIfContinue: T, noinline listener: suspend (E) -> T) = E::class.subscribeWhile(valueIfContinue, listener) -inline fun <reified E : Event> subscribeWhileFalse(noinline listener: suspend (E) -> Boolean) = E::class.subscribeWhileFalse(listener) -inline fun <reified E : Event> subscribeWhileTrue(noinline listener: suspend (E) -> Boolean) = E::class.subscribeWhileTrue(listener) -inline fun <reified E : Event> subscribeWhileNull(noinline listener: suspend (E) -> Any?) = E::class.subscribeWhileNull(listener) +suspend inline fun <reified E : Event, T> subscribeWhile(valueIfContinue: T, noinline listener: suspend (E) -> T) = + E::class.subscribeWhile(valueIfContinue, listener) + +suspend inline fun <reified E : Event> subscribeWhileFalse(noinline listener: suspend (E) -> Boolean) = + E::class.subscribeWhileFalse(listener) + +suspend inline fun <reified E : Event> subscribeWhileTrue(noinline listener: suspend (E) -> Boolean) = + E::class.subscribeWhileTrue(listener) + +suspend inline fun <reified E : Event> subscribeWhileNull(noinline listener: suspend (E) -> Any?) = + E::class.subscribeWhileNull(listener) // endregion // region KClass 的扩展方法 (不推荐) -fun <E : Event> KClass<E>.subscribe(handler: suspend (E) -> ListeningStatus) = this.subscribeInternal(Handler(handler)) +suspend fun <E : Event> KClass<E>.subscribe(handler: suspend (E) -> ListeningStatus) = + this.subscribeInternal(Handler(handler)) -fun <E : Event> KClass<E>.subscribeAlways(listener: suspend (E) -> Unit) = this.subscribeInternal(Handler { listener(it); ListeningStatus.LISTENING }) +suspend fun <E : Event> KClass<E>.subscribeAlways(listener: suspend (E) -> Unit) = + this.subscribeInternal(Handler { listener(it); ListeningStatus.LISTENING }) -fun <E : Event> KClass<E>.subscribeOnce(listener: suspend (E) -> Unit) = this.subscribeInternal(Handler { listener(it); ListeningStatus.STOPPED }) +suspend fun <E : Event> KClass<E>.subscribeOnce(listener: suspend (E) -> Unit) = + this.subscribeInternal(Handler { listener(it); ListeningStatus.STOPPED }) -fun <E : Event, T> KClass<E>.subscribeUntil(valueIfStop: T, listener: suspend (E) -> T) = subscribeInternal(Handler { if (listener(it) === valueIfStop) ListeningStatus.STOPPED else ListeningStatus.LISTENING }) -fun <E : Event> KClass<E>.subscribeUntilFalse(listener: suspend (E) -> Boolean) = subscribeUntil(false, listener) -fun <E : Event> KClass<E>.subscribeUntilTrue(listener: suspend (E) -> Boolean) = subscribeUntil(true, listener) -fun <E : Event> KClass<E>.subscribeUntilNull(listener: suspend (E) -> Any?) = subscribeUntil(null, listener) +suspend fun <E : Event, T> KClass<E>.subscribeUntil(valueIfStop: T, listener: suspend (E) -> T) = + subscribeInternal(Handler { if (listener(it) === valueIfStop) ListeningStatus.STOPPED else ListeningStatus.LISTENING }) + +suspend fun <E : Event> KClass<E>.subscribeUntilFalse(listener: suspend (E) -> Boolean) = + subscribeUntil(false, listener) + +suspend fun <E : Event> KClass<E>.subscribeUntilTrue(listener: suspend (E) -> Boolean) = subscribeUntil(true, listener) +suspend fun <E : Event> KClass<E>.subscribeUntilNull(listener: suspend (E) -> Any?) = subscribeUntil(null, listener) -fun <E : Event, T> KClass<E>.subscribeWhile(valueIfContinue: T, listener: suspend (E) -> T) = subscribeInternal(Handler { if (listener(it) !== valueIfContinue) ListeningStatus.STOPPED else ListeningStatus.LISTENING }) -fun <E : Event> KClass<E>.subscribeWhileFalse(listener: suspend (E) -> Boolean) = subscribeWhile(false, listener) -fun <E : Event> KClass<E>.subscribeWhileTrue(listener: suspend (E) -> Boolean) = subscribeWhile(true, listener) -fun <E : Event> KClass<E>.subscribeWhileNull(listener: suspend (E) -> Any?) = subscribeWhile(null, listener) +suspend fun <E : Event, T> KClass<E>.subscribeWhile(valueIfContinue: T, listener: suspend (E) -> T) = + subscribeInternal(Handler { if (listener(it) !== valueIfContinue) ListeningStatus.STOPPED else ListeningStatus.LISTENING }) + +suspend fun <E : Event> KClass<E>.subscribeWhileFalse(listener: suspend (E) -> Boolean) = + subscribeWhile(false, listener) + +suspend fun <E : Event> KClass<E>.subscribeWhileTrue(listener: suspend (E) -> Boolean) = subscribeWhile(true, listener) +suspend fun <E : Event> KClass<E>.subscribeWhileNull(listener: suspend (E) -> Any?) = subscribeWhile(null, listener) // endregion @@ -68,15 +96,18 @@ fun <E : Event> KClass<E>.subscribeWhileNull(listener: suspend (E) -> Any?) = su * 监听一个事件. 可同时进行多种方式的监听 * @see ListenerBuilder */ -fun <E : Event> KClass<E>.subscribeAll(listeners: ListenerBuilder<E>.() -> Unit) { - ListenerBuilder<E> { this.subscribeInternal(it) }.apply(listeners) +suspend fun <E : Event> KClass<E>.subscribeAll(listeners: suspend ListenerBuilder<E>.() -> Unit) { + with(ListenerBuilder<E> { this.subscribeInternal(it) }) { + listeners() + } } /** * 监听一个事件. 可同时进行多种方式的监听 * @see ListenerBuilder */ -inline fun <reified E : Event> subscribeAll(noinline listeners: ListenerBuilder<E>.() -> Unit) = E::class.subscribeAll(listeners) +suspend inline fun <reified E : Event> subscribeAll(noinline listeners: suspend ListenerBuilder<E>.() -> Unit) = + E::class.subscribeAll(listeners) /** * 监听构建器. 可同时进行多种方式的监听 @@ -96,27 +127,31 @@ inline fun <reified E : Event> subscribeAll(noinline listeners: ListenerBuilder< */ @Suppress("MemberVisibilityCanBePrivate", "unused") inline class ListenerBuilder<out E : Event>( - private val handlerConsumer: (Handler<in E>) -> Unit + private val handlerConsumer: suspend (Handler<in E>) -> Unit ) { - fun handler(listener: suspend (E) -> ListeningStatus) { + suspend fun handler(listener: suspend (E) -> ListeningStatus) { handlerConsumer(Handler(listener)) } - fun always(listener: suspend (E) -> Unit) = handler { listener(it); ListeningStatus.LISTENING } + suspend fun always(listener: suspend (E) -> Unit) = handler { listener(it); ListeningStatus.LISTENING } - fun <T> until(until: T, listener: suspend (E) -> T) = handler { if (listener(it) === until) ListeningStatus.STOPPED else ListeningStatus.LISTENING } - fun untilFalse(listener: suspend (E) -> Boolean) = until(false, listener) - fun untilTrue(listener: suspend (E) -> Boolean) = until(true, listener) - fun untilNull(listener: suspend (E) -> Any?) = until(null, listener) + suspend fun <T> until(until: T, listener: suspend (E) -> T) = + handler { if (listener(it) === until) ListeningStatus.STOPPED else ListeningStatus.LISTENING } + + suspend fun untilFalse(listener: suspend (E) -> Boolean) = until(false, listener) + suspend fun untilTrue(listener: suspend (E) -> Boolean) = until(true, listener) + suspend fun untilNull(listener: suspend (E) -> Any?) = until(null, listener) - fun <T> `while`(until: T, listener: suspend (E) -> T) = handler { if (listener(it) !== until) ListeningStatus.STOPPED else ListeningStatus.LISTENING } - fun whileFalse(listener: suspend (E) -> Boolean) = `while`(false, listener) - fun whileTrue(listener: suspend (E) -> Boolean) = `while`(true, listener) - fun whileNull(listener: suspend (E) -> Any?) = `while`(null, listener) + suspend fun <T> `while`(until: T, listener: suspend (E) -> T) = + handler { if (listener(it) !== until) ListeningStatus.STOPPED else ListeningStatus.LISTENING } + + suspend fun whileFalse(listener: suspend (E) -> Boolean) = `while`(false, listener) + suspend fun whileTrue(listener: suspend (E) -> Boolean) = `while`(true, listener) + suspend fun whileNull(listener: suspend (E) -> Any?) = `while`(null, listener) - fun once(block: suspend (E) -> Unit) = handler { block(it); ListeningStatus.STOPPED } + suspend fun once(block: suspend (E) -> Unit) = handler { block(it); ListeningStatus.STOPPED } } // endregion \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/internal/InternalEventListeners.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/internal/InternalEventListeners.kt index 4c38ee329..da9efd545 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/internal/InternalEventListeners.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/internal/InternalEventListeners.kt @@ -1,18 +1,48 @@ package net.mamoe.mirai.event.internal +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import net.mamoe.mirai.event.Event +import net.mamoe.mirai.event.EventScope import net.mamoe.mirai.event.ListeningStatus import net.mamoe.mirai.utils.inlinedRemoveIf import kotlin.reflect.KClass /** - * 监听和广播实现 + * 监听和广播实现. + * 它会首先检查这个事件是否正在被广播 + * - 如果是, 则将新的监听者放入缓存中. 在当前广播结束后转移到主列表 (通过一个协程完成) + * - 如果不是, 则直接将新的监听者放入主列表 * * @author Him188moe */ -internal fun <E : Event> KClass<E>.subscribeInternal(listener: Listener<E>) = this.listeners.add(listener)//TODO lock or sth else +internal suspend fun <E : Event> KClass<E>.subscribeInternal(listener: Listener<E>): Unit = with(this.listeners()) { + if (mainMutex.tryLock()) {//能锁则代表这个事件目前没有正在广播. + try { + add(listener)//直接修改主监听者列表 + } finally { + mainMutex.unlock() + } + return + } + + //不能锁住, 则这个事件正在广播, 那么要将新的监听者放入缓存 + cacheMutex.withLock { + cache.add(listener) + } + + EventScope.launch { + //启动协程并等待正在进行的广播结束, 然后将缓存转移到主监听者列表 + //启动后的协程马上就会因为锁而被挂起 + mainMutex.withLock { + if (cache.size != 0) { + addAll(cache) + cache.clear() + } + } + } +} /** * 事件监听器 @@ -31,18 +61,37 @@ class Handler<E : Event>(val handler: suspend (E) -> ListeningStatus) : Listener override suspend fun onEvent(event: E): ListeningStatus = handler.invoke(event) } -internal val <E : Event> KClass<E>.listeners: EventListeners<E> get() = EventListenerManger.get(this) +/** + * 这个事件类的监听器 list + */ +internal suspend fun <E : Event> KClass<E>.listeners(): EventListeners<E> = EventListenerManger.get(this) internal class EventListeners<E : Event> : MutableList<Listener<E>> by mutableListOf() { - val lock = Mutex() + /** + * 主监听者列表. + * 广播事件时使用这个锁. + */ + val mainMutex = Mutex() + /** + * 缓存(监听)事件时使用的锁 + */ + val cacheMutex = Mutex() + /** + * 等待加入到主 list 的监听者. 务必使用 [cacheMutex] + */ + val cache: MutableList<Listener<E>> = mutableListOf() } +/** + * 管理每个事件 class 的 [EventListeners]. + * [EventListeners] 是 lazy 的: 它们只会在被需要的时候才创建和存储. + */ internal object EventListenerManger { private val registries: MutableMap<KClass<out Event>, EventListeners<out Event>> = mutableMapOf() + private val registriesMutex = Mutex() @Suppress("UNCHECKED_CAST") - internal fun <E : Event> get(clazz: KClass<E>): EventListeners<E> { - //synchronized(clazz) { + internal suspend fun <E : Event> get(clazz: KClass<E>): EventListeners<E> = registriesMutex.withLock { if (registries.containsKey(clazz)) { return registries[clazz] as EventListeners<E> } else { @@ -51,21 +100,24 @@ internal object EventListenerManger { return it } } - //} } } @Suppress("UNCHECKED_CAST") internal suspend fun <E : Event> E.broadcastInternal(): E { - suspend fun callListeners(listeners: EventListeners<in E>) = listeners.lock.withLock { - //fixme 这个锁会导致在事件处理时再监听这个事件死锁 + //FIXME 若一个 listener 阻塞, 则这个事件全部阻塞. + suspend fun callListeners(listeners: EventListeners<in E>) = listeners.mainMutex.withLock { listeners.inlinedRemoveIf { it.onEvent(this) == ListeningStatus.STOPPED } } - callListeners(this::class.listeners as EventListeners<in E>) + callListeners(this::class.listeners() as EventListeners<in E>) + //FIXME 这可能不支持所有的平台. 可能需要修改. loopAllListeners(this::class) { callListeners(it as EventListeners<in E>) } return this } -internal expect inline fun <E : Event> loopAllListeners(clazz: KClass<E>, consumer: (EventListeners<in E>) -> Unit) \ No newline at end of file +internal expect suspend inline fun <E : Event> loopAllListeners( + clazz: KClass<E>, + consumer: (EventListeners<in E>) -> Unit +) \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/BotNetworkHandler.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/BotNetworkHandler.kt index 44cb454fb..6a0fea7e6 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/BotNetworkHandler.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/BotNetworkHandler.kt @@ -28,27 +28,25 @@ import kotlin.coroutines.ContinuationInterceptor * - [EventPacketHandler] 处理消息相关(群消息/好友消息)([ServerEventPacket]) * - [ActionPacketHandler] 处理动作相关(踢人/加入群/好友列表等) * + * + * NetworkHandler 实现接口 [CoroutineScope] + * 即 [BotNetworkHandler] 自己就是作用域. + * 所有 [BotNetworkHandler] 的协程均启动在此作用域下. + * + * [BotNetworkHandler] 的协程包含: + * - UDP 包接收: [PlatformDatagramChannel.read] + * - 心跳 Job [HeartbeatPacket] + * - SKey 刷新 [ReuestSKeyPcket] + * - 所有数据包处理和发送 + * + * [BotNetworkHandler.close] 时将会 [取消][kotlin.coroutines.CoroutineContext.cancelChildren] 所有此作用域下的协程 + * * A BotNetworkHandler is used to connect with Tencent servers. */ @Suppress("PropertyName") -interface BotNetworkHandler<Socket : DataPacketSocketAdapter> { - /** - * [BotNetworkHandler] 的协程作用域. - * 所有 [BotNetworkHandler] 的协程均启动在此作用域下. - * - * [BotNetworkHandler] 的协程包含: - * - UDP 包接收: [PlatformDatagramChannel.read] - * - 心跳 Job [HeartbeatPacket] - * - SKey 刷新 [RefreshSKeyRequestPacket] - * - 所有数据包处理和发送 - * - * [BotNetworkHandler.close] 时将会 [取消][kotlin.coroutines.CoroutineContext.cancelChildren] 所有此作用域下的协程 - */ - val NetworkScope: CoroutineScope - +interface BotNetworkHandler<Socket : DataPacketSocketAdapter> : CoroutineScope { val socket: Socket - /** * 得到 [PacketHandler]. * `get(EventPacketHandler)` 返回 [EventPacketHandler] @@ -76,8 +74,13 @@ interface BotNetworkHandler<Socket : DataPacketSocketAdapter> { */ suspend fun sendPacket(packet: OutgoingPacket) - fun close(cause: Throwable? = null) { + /** + * 等待直到与服务器断开连接. 若未连接则立即返回 + */ + suspend fun awaitDisconnection() + + suspend fun close(cause: Throwable? = null) { //todo check?? - NetworkScope.coroutineContext[ContinuationInterceptor]!!.cancelChildren(CancellationException("handler closed", cause)) + coroutineContext[ContinuationInterceptor]!!.cancelChildren(CancellationException("handler closed", cause)) } } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/BotSession.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/BotSession.kt index 928aab574..bed32c4ed 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/BotSession.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/BotSession.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import net.mamoe.mirai.Bot +import net.mamoe.mirai.network.protocol.tim.TIMBotNetworkHandler import net.mamoe.mirai.network.protocol.tim.handler.ActionPacketHandler import net.mamoe.mirai.network.protocol.tim.handler.DataPacketSocketAdapter import net.mamoe.mirai.network.protocol.tim.handler.TemporaryPacketHandler @@ -14,6 +15,12 @@ import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket import net.mamoe.mirai.utils.getGTK import kotlin.coroutines.coroutineContext +internal fun TIMBotNetworkHandler.BotSession( + bot: Bot, + sessionKey: ByteArray, + socket: DataPacketSocketAdapter +) = BotSession(bot, sessionKey, socket, this) + /** * 登录会话. 当登录完成后, 客户端会拿到 sessionKey. * 此时建立 session, 然后开始处理事务. @@ -21,10 +28,10 @@ import kotlin.coroutines.coroutineContext * @author Him188moe */ class BotSession( - val bot: Bot, - val sessionKey: ByteArray,//TODO 协议抽象? 可能并不是所有协议均需要 sessionKey - val socket: DataPacketSocketAdapter, - val NetworkScope: CoroutineScope + val bot: Bot, + val sessionKey: ByteArray,//TODO 协议抽象? 可能并不是所有协议均需要 sessionKey + val socket: DataPacketSocketAdapter, + val NetworkScope: CoroutineScope ) { /** @@ -66,7 +73,8 @@ class BotSession( * @param handler 处理期待的包 */ suspend inline fun <reified P : ServerPacket, R> OutgoingPacket.sendAndExpect(noinline handler: suspend (P) -> R): CompletableDeferred<R> { - val deferred: CompletableDeferred<R> = coroutineContext[Job].takeIf { it != null }?.let { CompletableDeferred<R>(it) } ?: CompletableDeferred() + val deferred: CompletableDeferred<R> = + coroutineContext[Job].takeIf { it != null }?.let { CompletableDeferred<R>(it) } ?: CompletableDeferred() bot.network.addHandler(TemporaryPacketHandler(P::class, deferred, this@BotSession).also { it.toSend(this) it.onExpect(handler) @@ -74,7 +82,8 @@ class BotSession( return deferred } - suspend inline fun <reified P : ServerPacket> OutgoingPacket.sendAndExpect(): CompletableDeferred<Unit> = sendAndExpect<P, Unit> {} + suspend inline fun <reified P : ServerPacket> OutgoingPacket.sendAndExpect(): CompletableDeferred<Unit> = + sendAndExpect<P, Unit> {} suspend inline fun OutgoingPacket.send() = socket.sendPacket(this) } diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/TIMBotNetworkHandler.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/TIMBotNetworkHandler.kt index ec43a46c4..cc77d1131 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/TIMBotNetworkHandler.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/TIMBotNetworkHandler.kt @@ -5,7 +5,6 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.io.core.* import net.mamoe.mirai.* -import net.mamoe.mirai.event.EventScope import net.mamoe.mirai.event.ListeningStatus import net.mamoe.mirai.event.broadcast import net.mamoe.mirai.event.events.BeforePacketSendEvent @@ -26,14 +25,18 @@ import net.mamoe.mirai.network.session import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.io.parseServerPacket import net.mamoe.mirai.utils.io.toUHexString +import kotlin.coroutines.CoroutineContext /** * [BotNetworkHandler] 的 TIM PC 协议实现 * * @see BotNetworkHandler */ -internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) : BotNetworkHandler<TIMBotNetworkHandler.BotSocketAdapter>, PacketHandlerList() { - override val NetworkScope: CoroutineScope = CoroutineScope(Dispatchers.Default) +internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) : + BotNetworkHandler<TIMBotNetworkHandler.BotSocketAdapter>, PacketHandlerList() { + + override val coroutineContext: CoroutineContext = + Dispatchers.Default + CoroutineExceptionHandler { _, e -> bot.logger.log(e) } override lateinit var socket: BotSocketAdapter private set @@ -76,7 +79,7 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) : //private | internal private fun onLoggedIn(sessionKey: ByteArray) { require(size == 0) { "Already logged in" } - val session = BotSession(bot, sessionKey, socket, NetworkScope) + val session = BotSession(bot, sessionKey, socket) add(EventPacketHandler(session).asNode(EventPacketHandler)) add(ActionPacketHandler(session).asNode(ActionPacketHandler)) @@ -85,14 +88,20 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) : private lateinit var sessionKey: ByteArray - override fun close(cause: Throwable?) { + override suspend fun awaitDisconnection() { + heartbeatJob?.join() + } + + override suspend fun close(cause: Throwable?) { super.close(cause) this.heartbeatJob?.cancel(CancellationException("handler closed")) + this.heartbeatJob?.join()//等待 cancel 完成 this.heartbeatJob = null if (!this.loginResult.isCompleted && !this.loginResult.isCancelled) { this.loginResult.cancel(CancellationException("socket closed")) + this.loginResult.join() } this.forEach { @@ -104,7 +113,8 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) : override suspend fun sendPacket(packet: OutgoingPacket) = socket.sendPacket(packet) - internal inner class BotSocketAdapter(override val serverIp: String, val configuration: BotNetworkConfiguration) : DataPacketSocketAdapter { + internal inner class BotSocketAdapter(override val serverIp: String, val configuration: BotNetworkConfiguration) : + DataPacketSocketAdapter { override val channel: PlatformDatagramChannel = PlatformDatagramChannel(serverIp, 8000) override val isOpen: Boolean get() = channel.isOpen @@ -130,14 +140,10 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) : continue } - NetworkScope.launch { - try { - //`.use`: Ensure that the packet is consumed totally so that all the buffers are released - ByteReadPacket(buffer, IoBuffer.Pool).use { - distributePacket(it.parseServerPacket(buffer.readRemaining)) - } - } catch (e: Throwable) { - e.log() + launch { + //`.use`: Ensure that the packet is consumed totally so that all the buffers are released + ByteReadPacket(buffer, IoBuffer.Pool).use { + distributePacket(it.parseServerPacket(buffer.readRemaining)) } } } @@ -150,8 +156,8 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) : val expect = expectPacket<ServerTouchResponsePacket>() - NetworkScope.launch { processReceive() } - NetworkScope.launch { + launch { processReceive() } + launch { if (withTimeoutOrNull(configuration.touchTimeout.millisecondsLong) { expect.join() } == null) { loginResult.complete(LoginResult.TIMEOUT) } @@ -161,7 +167,7 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) : return loginResult.await() } - private inline fun <reified P : ServerPacket> expectPacket(): CompletableDeferred<P> { + private suspend inline fun <reified P : ServerPacket> expectPacket(): CompletableDeferred<P> { val receiving = CompletableDeferred<P>() subscribe<ServerPacketReceivedEvent> { if (it.packet is P && it.bot === bot) { @@ -173,7 +179,7 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) : return receiving } - override suspend fun distributePacket(packet: ServerPacket) { + override suspend fun distributePacket(packet: ServerPacket) = coroutineScope { try { packet.decode() } catch (e: Exception) { @@ -197,13 +203,13 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) : if (packet is ServerEventPacket) { //no need to sync acknowledgement packets - NetworkScope.launch { + launch { sendPacket(packet.ResponsePacket(bot.qqAccount, sessionKey)) } } if (ServerPacketReceivedEvent(bot, packet).broadcast().cancelled) { - return + return@coroutineScope } loginHandler.onPacketReceived(packet) @@ -231,11 +237,11 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) : } }*/ - override suspend fun sendPacket(packet: OutgoingPacket) = withContext(NetworkScope.coroutineContext) { + override suspend fun sendPacket(packet: OutgoingPacket): Unit = coroutineScope { check(channel.isOpen) { "channel is not open" } if (BeforePacketSendEvent(bot, packet).broadcast().cancelled) { - return@withContext + return@coroutineScope } packet.packet.use { build -> @@ -246,7 +252,7 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) : } catch (e: SendPacketInternalException) { bot.logger.logError("Caught SendPacketInternalException: ${e.cause?.message}") bot.reinitializeNetworkHandler(configuration, e) - return@withContext + return@coroutineScope } finally { buffer.release(IoBuffer.Pool) } @@ -254,7 +260,9 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) : bot.logGreen("Packet sent: $packet") - EventScope.launch { PacketSentEvent(bot, packet).broadcast() } + PacketSentEvent(bot, packet).broadcast() + + Unit } override val owner: Bot get() = this@TIMBotNetworkHandler.bot @@ -307,7 +315,8 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) : this.loginTime = packet.loginTime this.token0825 = packet.token0825 - socket.sendPacket(OutgoingPasswordSubmissionPacket( + socket.sendPacket( + OutgoingPasswordSubmissionPacket( bot = bot.qqAccount, password = bot.account.password, loginTime = loginTime, @@ -316,7 +325,8 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) : token0825 = token0825, token00BA = null, randomDeviceName = socket.configuration.randomDeviceName - )) + ) + ) } } @@ -329,7 +339,8 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) : this.privateKey = getRandomByteArray(16)//似乎是必须的 this.token00BA = packet.token00BA - socket.sendPacket(OutgoingPasswordSubmissionPacket( + socket.sendPacket( + OutgoingPasswordSubmissionPacket( bot = bot.qqAccount, password = bot.account.password, loginTime = loginTime, @@ -338,7 +349,8 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) : token0825 = token0825, token00BA = packet.token00BA, randomDeviceName = socket.configuration.randomDeviceName - )) + ) + ) } is ServerLoginResponseCaptchaInitPacket -> { @@ -348,7 +360,14 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) : if (packet.unknownBoolean) { this.captchaSectionId = 1 - socket.sendPacket(OutgoingCaptchaTransmissionRequestPacket(bot.qqAccount, this.token0825, this.captchaSectionId++, packet.token00BA)) + socket.sendPacket( + OutgoingCaptchaTransmissionRequestPacket( + bot.qqAccount, + this.token0825, + this.captchaSectionId++, + packet.token00BA + ) + ) } } @@ -372,23 +391,46 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) : socket.sendPacket(OutgoingCaptchaRefreshPacket(bot.qqAccount, token0825)) } else { this.captchaSectionId = 0//意味着已经提交验证码 - socket.sendPacket(OutgoingCaptchaSubmitPacket(bot.qqAccount, token0825, code, packet.verificationToken)) + socket.sendPacket( + OutgoingCaptchaSubmitPacket( + bot.qqAccount, + token0825, + code, + packet.verificationToken + ) + ) } } else { - socket.sendPacket(OutgoingCaptchaTransmissionRequestPacket(bot.qqAccount, token0825, captchaSectionId++, packet.token00BA)) + socket.sendPacket( + OutgoingCaptchaTransmissionRequestPacket( + bot.qqAccount, + token0825, + captchaSectionId++, + packet.token00BA + ) + ) } } is ServerLoginResponseSuccessPacket -> { this.sessionResponseDecryptionKey = packet.sessionResponseDecryptionKey - socket.sendPacket(OutgoingSessionRequestPacket(bot.qqAccount, socket.serverIp, packet.token38, packet.token88, packet.encryptionKey)) + socket.sendPacket( + OutgoingSessionRequestPacket( + bot.qqAccount, + socket.serverIp, + packet.token38, + packet.token88, + packet.encryptionKey + ) + ) } //是ClientPasswordSubmissionPacket之后服务器回复的可能之一 is ServerLoginResponseKeyExchangePacket -> { this.privateKey = packet.privateKeyUpdate - socket.sendPacket(OutgoingPasswordSubmissionPacket( + socket.sendPacket( + OutgoingPasswordSubmissionPacket( bot = bot.qqAccount, password = bot.account.password, loginTime = loginTime, @@ -398,22 +440,26 @@ internal class TIMBotNetworkHandler internal constructor(private val bot: Bot) : token00BA = packet.tokenUnknown ?: token00BA, randomDeviceName = socket.configuration.randomDeviceName, tlv0006 = packet.tlv0006 - )) + ) + ) } is ServerSessionKeyResponsePacket -> { sessionKey = packet.sessionKey bot.logger.logPurple("sessionKey = ${sessionKey.toUHexString()}") - heartbeatJob = NetworkScope.launch { + heartbeatJob = launch { while (socket.isOpen) { delay(configuration.heartbeatPeriod.millisecondsLong) with(session) { class HeartbeatTimeoutException : CancellationException("heartbeat timeout") if (withTimeoutOrNull(configuration.heartbeatTimeout.millisecondsLong) { - HeartbeatPacket(bot.qqAccount, sessionKey).sendAndExpect<HeartbeatPacket.Response>().join() - } == null) { + HeartbeatPacket( + bot.qqAccount, + sessionKey + ).sendAndExpect<HeartbeatPacket.Response>().join() + } == null) { bot.logPurple("Heartbeat timed out") bot.reinitializeNetworkHandler(configuration, HeartbeatTimeoutException()) return@launch diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/UploadFriendImage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/UploadFriendImage.kt index 91e0d999e..4189bd4cc 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/UploadFriendImage.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/UploadFriendImage.kt @@ -2,6 +2,8 @@ package net.mamoe.mirai.network.protocol.tim.packet +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import kotlinx.io.core.* import net.mamoe.mirai.contact.QQ import net.mamoe.mirai.message.ImageId @@ -21,11 +23,10 @@ suspend fun QQ.uploadImage(image: BufferedImage): ImageId = with(bot.network.ses return FriendImageIdRequestPacket(this.qqAccount, sessionKey, this.qqAccount, image).sendAndExpect<FriendImageIdRequestPacket.Response, ImageId> { if (it.uKey != null) require(httpPostFriendImage( + botAccount = bot.qqAccount, uKeyHex = it.uKey!!.toUHexString(""), - botNumber = bot.qqAccount, - imageData = image.data, - fileSize = image.fileSize, - qq = this.qqAccount + imageInput = image.input, + inputSize = image.inputSize )) it.imageId!! }.await() @@ -217,8 +218,10 @@ class FriendImageIdRequestPacket( writeFully(image.md5) writeUByte(0x28u) - writeUVarInt(image.fileSize.toUInt()) + writeUVarInt(image.inputSize.toUInt()) + + GlobalScope.launch { } writeUByte(0x32u) //长度应为1A writeUVarintLVPacket { diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/UploadGroupImage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/UploadGroupImage.kt index dea0f47d5..3b38c6a40 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/UploadGroupImage.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/UploadGroupImage.kt @@ -18,10 +18,10 @@ suspend fun Group.uploadImage( .sendAndExpect<GroupImageIdRequestPacket.Response, Unit> { if (it.uKey != null) { httpPostGroupImage( - bot = bot.qqAccount, + botAccount = bot.qqAccount, groupNumber = groupId, - imageData = image.data, - fileSize = image.fileSize, + imageInput = image.input, + inputSize = image.inputSize, uKeyHex = it.uKey!!.toUHexString("") ) } @@ -154,7 +154,7 @@ class GroupImageIdRequestPacket( writeUByte(0x10u) writeFully(image.md5) - writeTUVarint(0x28u, image.fileSize.toUInt()) + writeTUVarint(0x28u, image.inputSize.toUInt()) writeUVarintLVPacket(tag = 0x32u) { writeTV(0x5B_00u) writeTV(0x40_00u) diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotNetworkConfiguration.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotNetworkConfiguration.kt index bed1e0c5a..de91f555e 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotNetworkConfiguration.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotNetworkConfiguration.kt @@ -3,6 +3,7 @@ package net.mamoe.mirai.utils import com.soywiz.klock.TimeSpan import com.soywiz.klock.seconds import net.mamoe.mirai.network.protocol.tim.packet.login.ServerTouchResponsePacket +import kotlin.jvm.JvmField /** * 网络配置 @@ -32,6 +33,7 @@ class BotNetworkConfiguration { var heartbeatTimeout: TimeSpan = 2.seconds companion object { + @JvmField val Default = BotNetworkConfiguration() } } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BufferedImage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BufferedImage.kt index 6cba3bd19..5bca83c88 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BufferedImage.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BufferedImage.kt @@ -1,16 +1,27 @@ +@file:Suppress("EXPERIMENTAL_API_USAGE") + package net.mamoe.mirai.utils import kotlinx.io.core.ByteReadPacket +import kotlinx.io.core.Input import net.mamoe.mirai.message.ImageId +fun BufferedImage( + width: Int, + height: Int, + md5: ByteArray, + format: String, + data: ByteReadPacket +) = BufferedImage(width, height, md5, format, data, data.remaining) + class BufferedImage( - val width: Int, - val height: Int, - val md5: ByteArray, - val format: String, - val data: ByteReadPacket + val width: Int, + val height: Int, + val md5: ByteArray, + val format: String, + val input: Input, + val inputSize: Long ) { - val fileSize: Long = data.remaining /** * 用于发送消息的 [ImageId] diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/PlatformUtils.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/PlatformUtils.kt index 3aa4dbd94..329dd0a94 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/PlatformUtils.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/PlatformUtils.kt @@ -3,8 +3,13 @@ package net.mamoe.mirai.utils import com.soywiz.klock.DateTime -import kotlinx.io.core.ByteReadPacket -import net.mamoe.mirai.utils.io.printStringFromHex +import io.ktor.client.HttpClient +import io.ktor.client.request.HttpRequestBuilder +import io.ktor.client.request.post +import io.ktor.http.HttpStatusCode +import io.ktor.http.URLProtocol +import io.ktor.http.userAgent +import kotlinx.io.core.Input /** * 时间戳 @@ -39,28 +44,67 @@ expect fun solveIpAddress(hostname: String): String */ expect fun localIpAddress(): String +/** + * Provided by Ktor Http + */ +internal expect val httpClient: HttpClient + /** * 上传好友图片 */ -expect suspend fun httpPostFriendImage( - uKeyHex: String, - fileSize: Long, - botNumber: UInt, - qq: UInt, - imageData: ByteReadPacket -): Boolean +@Suppress("DuplicatedCode") +suspend fun httpPostFriendImage( + botAccount: UInt, + uKeyHex: String, + imageInput: Input, + inputSize: Long +): Boolean = (httpClient.postImage(imageInput, inputSize, uKeyHex) { + url { + parameters["htcmd"] = "0x6ff0070" + parameters["uin"] = botAccount.toLong().toString() + } + +} as HttpStatusCode).value.also { println(it) } == 200 /** * 上传群图片 */ -expect suspend fun httpPostGroupImage( - bot: UInt, - groupNumber: UInt, - uKeyHex: String, - fileSize: Long, - imageData: ByteReadPacket -): Boolean +@Suppress("DuplicatedCode") +suspend fun httpPostGroupImage( + botAccount: UInt, + groupNumber: UInt, + uKeyHex: String, + imageInput: Input, + inputSize: Long +): Boolean = (httpClient.postImage(imageInput, inputSize, uKeyHex) { + url { + parameters["htcmd"] = "0x6ff0071" + parameters["uin"] = botAccount.toLong().toString() + parameters["groupcode"] = groupNumber.toLong().toString() + } +} as HttpStatusCode).value.also { println(it) } == 200 -fun main() { - "46 52 25 46 60 30 59 4F 4A 5A 51".printStringFromHex() -} \ No newline at end of file + +private suspend inline fun <reified T> HttpClient.postImage( + imageInput: Input, + inputSize: Long, + uKeyHex: String, + block: HttpRequestBuilder.() -> Unit = {} +): T = post { + url { + protocol = URLProtocol.HTTP + host = "htdata2.qq.com" + path("cgi-bin/httpconn") + + parameters["ver"] = "5603" + parameters["filezise"] = inputSize.toString() + parameters["range"] = 0.toString() + parameters["ukey"] = uKeyHex + + userAgent("QQClient") + } + block() + configureBody(inputSize, imageInput) +} + +internal expect fun HttpRequestBuilder.configureBody(inputSize: Long, input: Input) \ No newline at end of file diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/contact/ContactJvm.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/contact/ContactJvm.kt index 0a37596e6..7eff59a9f 100644 --- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/contact/ContactJvm.kt +++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/contact/ContactJvm.kt @@ -2,10 +2,7 @@ package net.mamoe.mirai.contact -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import net.mamoe.mirai.Bot -import net.mamoe.mirai.message.Message import net.mamoe.mirai.message.MessageChain import net.mamoe.mirai.network.protocol.tim.handler.EventPacketHandler import net.mamoe.mirai.utils.ContactList @@ -26,36 +23,36 @@ actual sealed class Contact actual constructor(bot: Bot, number: UInt) : Platfor abstract override suspend fun sendXMLMessage(message: String) - - /** - * 阻塞发送一个消息. 仅应在 Java 使用 - */ - fun blockingSendMessage(chain: MessageChain) = runBlocking { sendMessage(chain) } - - /** - * 阻塞发送一个消息. 仅应在 Java 使用 - */ - fun blockingSendMessage(message: Message) = runBlocking { sendMessage(message) } - - /** - * 阻塞发送一个消息. 仅应在 Java 使用 - */ - fun blockingSendMessage(plain: String) = runBlocking { sendMessage(plain) } - - /** - * 异步发送一个消息. 仅应在 Java 使用 - */ - fun asyncSendMessage(chain: MessageChain) = bot.network.NetworkScope.launch { sendMessage(chain) } - - /** - * 异步发送一个消息. 仅应在 Java 使用 - */ - fun asyncSendMessage(message: Message) = bot.network.NetworkScope.launch { sendMessage(message) } - - /** - * 异步发送一个消息. 仅应在 Java 使用 - */ - fun asyncSendMessage(plain: String) = bot.network.NetworkScope.launch { sendMessage(plain) } +// +// /** +// * 阻塞发送一个消息. 仅应在 Java 使用 +// */ +// fun blockingSendMessage(chain: MessageChain) = runBlocking { sendMessage(chain) } +// +// /** +// * 阻塞发送一个消息. 仅应在 Java 使用 +// */ +// fun blockingSendMessage(message: Message) = runBlocking { sendMessage(message) } +// +// /** +// * 阻塞发送一个消息. 仅应在 Java 使用 +// */ +// fun blockingSendMessage(plain: String) = runBlocking { sendMessage(plain) } +// +// /** +// * 异步发送一个消息. 仅应在 Java 使用 +// */ +// fun asyncSendMessage(chain: MessageChain) = bot.network.launch { sendMessage(chain) } +// +// /** +// * 异步发送一个消息. 仅应在 Java 使用 +// */ +// fun asyncSendMessage(message: Message) = bot.network.launch { sendMessage(message) } +// +// /** +// * 异步发送一个消息. 仅应在 Java 使用 +// */ +// fun asyncSendMessage(plain: String) = bot.network.launch { sendMessage(plain) } } /** diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/event/SubscribersJvm.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/event/SubscribersJvm.kt new file mode 100644 index 000000000..e778dac9c --- /dev/null +++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/event/SubscribersJvm.kt @@ -0,0 +1,14 @@ +package net.mamoe.mirai.event + +import kotlinx.coroutines.runBlocking + + +// TODO 添加更多 +/** + * Jvm 调用实现(阻塞) + */ +object Events { + @JvmStatic + fun <E : Event> subscribe(type: Class<E>, handler: suspend (E) -> ListeningStatus) = + runBlocking { type.kotlin.subscribe(handler) } +} diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/event/internal/EventListeneresInternalJvm.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/event/internal/EventListeneresInternalJvm.kt index b5e5ca8ed..676400ce6 100644 --- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/event/internal/EventListeneresInternalJvm.kt +++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/event/internal/EventListeneresInternalJvm.kt @@ -6,10 +6,13 @@ import kotlin.reflect.full.allSuperclasses import kotlin.reflect.full.isSuperclassOf @Suppress("UNCHECKED_CAST") -internal actual inline fun <E : Event> loopAllListeners(clazz: KClass<E>, consumer: (EventListeners<in E>) -> Unit) { +internal actual suspend inline fun <E : Event> loopAllListeners( + clazz: KClass<E>, + consumer: (EventListeners<in E>) -> Unit +) { clazz.allSuperclasses.forEach { if (Event::class.isSuperclassOf(it)) { - consumer((it as KClass<out Event>).listeners as EventListeners<in E>) + consumer((it as KClass<out Event>).listeners() as EventListeners<in E>) } } } \ No newline at end of file diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/BufferedImageJvm.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/BufferedImageJvm.kt index df3c9933b..fb30b763c 100644 --- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/BufferedImageJvm.kt +++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/BufferedImageJvm.kt @@ -2,8 +2,11 @@ package net.mamoe.mirai.utils +import kotlinx.io.core.IoBuffer import kotlinx.io.core.buildPacket -import kotlinx.io.streams.writePacket +import kotlinx.io.streams.asInput +import java.io.File +import java.io.FileInputStream import java.io.InputStream import java.io.OutputStream import java.security.MessageDigest @@ -29,14 +32,33 @@ fun JavaBufferedImage.toMiraiImage(formatName: String = "gif"): BufferedImage { } fun BufferedImage.toJavaImage(): JavaBufferedImage = ImageIO.read(object : InputStream() { - override fun read(): Int = with(this@toJavaImage.data) { - if (remaining != 0L) + override fun read(): Int = with(this@toJavaImage.input) { + if (!endOfInput) readByte().toInt() else -1 } }) -/** - * 将缓存的图片写入流. 注意, 写入后缓存将会被清空. - */ -fun OutputStream.writeImage(image: BufferedImage) = this.writePacket(image.data) \ No newline at end of file +fun File.toMiraiImage(): BufferedImage { + val image = ImageIO.getImageReaders(this.inputStream()).asSequence().first() + + val digest = MessageDigest.getInstance("md5") + digest.reset() + FileInputStream(this).transferTo(object : OutputStream() { + override fun write(b: Int) { + b.toByte().let { + digest.update(it) + } + } + }) + + val dimension = image.defaultReadParam.sourceRenderSize + return BufferedImage( + width = dimension.width, + height = dimension.height, + md5 = digest.digest(), + format = image.formatName, + input = this.inputStream().asInput(IoBuffer.Pool), + inputSize = this.length() + ) +} \ No newline at end of file diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/PlatformUtilsJvm.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/PlatformUtilsJvm.kt index fe05c7be6..a2b067e4b 100644 --- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/PlatformUtilsJvm.kt +++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/PlatformUtilsJvm.kt @@ -4,15 +4,17 @@ package net.mamoe.mirai.utils import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO -import io.ktor.client.request.post -import io.ktor.content.ByteArrayContent +import io.ktor.client.request.HttpRequestBuilder import io.ktor.http.ContentType -import io.ktor.http.HttpStatusCode -import io.ktor.http.URLProtocol +import io.ktor.http.content.OutgoingContent import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.io.ByteWriteChannel +import kotlinx.coroutines.io.writeFully import kotlinx.coroutines.withContext import kotlinx.io.core.ByteReadPacket +import kotlinx.io.core.Input import kotlinx.io.core.readBytes +import kotlinx.io.core.readFully import org.jsoup.Connection import org.jsoup.Jsoup import java.net.InetAddress @@ -22,7 +24,7 @@ import java.util.zip.CRC32 actual val deviceName: String = InetAddress.getLocalHost().hostName /* - * TODO we may use libraries that provide these functions + * TODO: we may use libraries that provide these functions */ actual fun crc32(key: ByteArray): Int = CRC32().let { it.update(key); it.value.toInt() } @@ -33,56 +35,18 @@ actual fun solveIpAddress(hostname: String): String = InetAddress.getByName(host actual fun localIpAddress(): String = InetAddress.getLocalHost().hostAddress -/** - * Provided by Ktor Http - */ -private val httpClient: HttpClient = HttpClient { - engine { CIO } -} - -actual suspend fun httpPostFriendImage( +suspend fun httpPostFriendImageOld( uKeyHex: String, - fileSize: Long, botNumber: UInt, - qq: UInt, - imageData: ByteReadPacket -): Boolean = (httpClient.post { - url { - protocol = URLProtocol.HTTP - host = "htdata2.qq.com" - path("cgi-bin/httpconn") - parameters["htcmd"] = "0x6ff0070" - parameters["ver"] = "5603" - parameters["ukey"] = uKeyHex - parameters["filezise"] = imageData.remaining.toString() - parameters["range"] = 0.toString() - parameters["uin"] = qq.toString() - } - - body = ByteArrayContent(imageData.readBytes(), ContentType.Image.Any) -} as HttpStatusCode).value == 200 - - -//.postImage(imageData) - -/** - * 上传群图片 - */ -actual suspend fun httpPostGroupImage( - bot: UInt, - groupNumber: UInt, - uKeyHex: String, - fileSize: Long, imageData: ByteReadPacket ): Boolean = Jsoup.connect("http://htdata2.qq.com/cgi-bin/httpconn" + - "?htcmd=0x6ff0071" + - "&term=pc" + + "?htcmd=0x6ff0070" + "&ver=5603" + - "&filesize=${imageData.remaining}" + - "&uin=$bot" + - "&groupcode=$groupNumber" + + "&ukey=${uKeyHex}" + + "&filezise=${imageData.remaining}" + "&range=0" + - "&ukey=" + uKeyHex) + "&uin=$botNumber" +) .postImage(imageData) @@ -99,4 +63,24 @@ private suspend fun Connection.postImage(image: ByteReadPacket): Boolean = this private suspend fun Connection.suspendExecute(): Connection.Response = withContext(Dispatchers.IO) { execute() +} + +internal actual val httpClient: HttpClient = HttpClient(CIO) + +internal actual fun HttpRequestBuilder.configureBody( + inputSize: Long, + input: Input +) { + body = object : OutgoingContent.WriteChannelContent() { + override val contentType: ContentType = ContentType.Image.GIF + override val contentLength: Long = inputSize + + override suspend fun writeTo(channel: ByteWriteChannel) { + val buffer = byteArrayOf(1) + while (!input.endOfInput) { + input.readFully(buffer) + channel.writeFully(buffer) + } + } + } } \ No newline at end of file diff --git a/mirai-debug/build.gradle b/mirai-debug/build.gradle index b1a47636b..5b3bdc6d5 100644 --- a/mirai-debug/build.gradle +++ b/mirai-debug/build.gradle @@ -5,11 +5,10 @@ dependencies { implementation project(':mirai-core') compile files('./lib/jpcap.jar') - implementation rootProject.ext.coroutineCommon - implementation rootProject.ext.kotlinJvm - implementation rootProject.ext.kotlinxIOJvm - compile "org.jetbrains.kotlin:kotlin-reflect:1.3.50" - implementation 'org.jsoup:jsoup:1.12.1' + api group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8', version: kotlin_version + api group: 'org.jetbrains.kotlinx', name: 'kotlinx-io', version: kotlinxio_version + + api group: 'org.jetbrains.kotlin', name: 'kotlin-reflect', version: kotlin_version } tasks.withType(JavaCompile) { diff --git a/mirai-debug/src/main/java/DownloadImgTest.kt b/mirai-debug/src/main/java/DownloadImgTest.kt deleted file mode 100644 index e78377cbd..000000000 --- a/mirai-debug/src/main/java/DownloadImgTest.kt +++ /dev/null @@ -1,12 +0,0 @@ -import net.mamoe.mirai.utils.io.toUHexString -import org.jsoup.Connection -import org.jsoup.Jsoup - -fun main() { - println(Jsoup.connect("http://61.183.164.34/gchatpic_new/B814D8D6A55D6DB423469E38D2A17AAA23836D74E7A656A4A8288C6078950A33B4A49E854E59B6D2314EFC47D6475902EDE8CADAFAF7F2A7670CAC05EA8314A1241128102F0A3AAF9C07284B1AE35E52F6D0A265235AFA6B/0?vuin=1040400290&term=255&srvver=26933&rf=n") - .cookie("ST", "00015DAA9C030040F3B82971FCBC718AA35573100B9CDBA2CB0DE38AF8710CA22E2986246345FC96B82BA0C211FDA700C397DF99FCC0989D67FD75F00B2FFB9CE0D032C3DCAC5A77") - .method(Connection.Method.GET) - .ignoreContentType(true) - .execute() - .bodyAsBytes().toUHexString()) -} \ No newline at end of file diff --git a/mirai-demos/mirai-demo-1/build.gradle b/mirai-demos/mirai-demo-1/build.gradle index 820eb9690..e0a26eb75 100644 --- a/mirai-demos/mirai-demo-1/build.gradle +++ b/mirai-demos/mirai-demo-1/build.gradle @@ -3,6 +3,6 @@ apply plugin: "java" dependencies { compile project(":mirai-core") - compile rootProject.ext.coroutine - compile rootProject.ext.kotlinJvm + api group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8', version: kotlin_version + api group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: coroutines_version } diff --git a/mirai-demos/mirai-demo-1/build.gradle.kts b/mirai-demos/mirai-demo-1/build.gradle.kts new file mode 100644 index 000000000..e69de29bb diff --git a/mirai-demos/mirai-demo-1/src/main/java/demo1/Main.kt b/mirai-demos/mirai-demo-1/src/main/java/demo1/Main.kt index f23b44d10..f3fc82eb8 100644 --- a/mirai-demos/mirai-demo-1/src/main/java/demo1/Main.kt +++ b/mirai-demos/mirai-demo-1/src/main/java/demo1/Main.kt @@ -2,10 +2,7 @@ package demo1 -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.coroutines.* import net.mamoe.mirai.Bot import net.mamoe.mirai.BotAccount import net.mamoe.mirai.contact.Group @@ -44,14 +41,14 @@ private fun readTestAccount(): BotAccount? { } @Suppress("UNUSED_VARIABLE") -suspend fun main() { +suspend fun main() = coroutineScope { val bot = Bot(readTestAccount() ?: BotAccount(//填写你的账号 account = 1994701121u, password = "123456" ), PlatformLogger()) bot.login { - randomDeviceName = true + randomDeviceName = false }.let { if (it != LoginResult.SUCCESS) { MiraiLogger.logError("Login failed: " + it.name) @@ -143,8 +140,7 @@ suspend fun main() { demo2() - //由于使用的是协程, main函数执行完后就会结束程序. - delay(Long.MAX_VALUE)//永远等待, 以测试事件 + bot.network.awaitDisconnection()//等到直到断开连接 } @@ -153,7 +149,7 @@ suspend fun main() { * 对机器人说 "记笔记", 机器人记录之后的所有消息. * 对机器人说 "停止", 机器人停止 */ -fun demo2() { +suspend fun demo2() { subscribeAlways<FriendMessageEvent> { event -> if (event.message eq "记笔记") { subscribeUntilFalse<FriendMessageEvent> {