diff --git a/.gitignore b/.gitignore index fb297786296..da52815b901 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,5 @@ bintray.key.txt /build-gpg-sign # Name for IDEA direction sorting build-secret-keys/ + +**/local.* \ No newline at end of file diff --git a/.run/RunRecorderKt.run.xml b/.run/RunRecorderKt.run.xml new file mode 100644 index 00000000000..a35c68ae675 --- /dev/null +++ b/.run/RunRecorderKt.run.xml @@ -0,0 +1,21 @@ +<!-- + ~ 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/dev/LICENSE + --> + +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="RunRecorderKt" type="JetRunConfigurationType" nameIsGenerated="true"> + <option name="MAIN_CLASS_NAME" value="net.mamoe.mirai.internal.bootstrap.RunRecorderKt"/> + <module name="mirai.mirai-core.jvmTest"/> + <option name="VM_PARAMETERS" + value="-Dmirai.debug.network.state.observer.logging=true -Dmirai.debug.network.show.all.components=true -Dkotlinx.coroutines.debug=on -Dmirai.debug.network.show.packet.details=true"/> + <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/test"/> + <method v="2"> + <option name="Make" enabled="true"/> + </method> + </configuration> +</component> \ No newline at end of file diff --git a/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api b/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api index d84f6e447a5..8cbf15dfc17 100644 --- a/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api +++ b/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api @@ -290,7 +290,7 @@ public final class net/mamoe/mirai/contact/ContactKt { } public final class net/mamoe/mirai/contact/ContactList : java/util/Collection, kotlin/jvm/internal/markers/KMappedMarker { - public final field delegate Ljava/util/concurrent/ConcurrentLinkedQueue; + public final field delegate Ljava/util/Collection; public synthetic fun add (Ljava/lang/Object;)Z public fun add (Lnet/mamoe/mirai/contact/Contact;)Z public fun addAll (Ljava/util/Collection;)Z diff --git a/binary-compatibility-validator/api/binary-compatibility-validator.api b/binary-compatibility-validator/api/binary-compatibility-validator.api index 6c14843292e..0ec04ea4b1a 100644 --- a/binary-compatibility-validator/api/binary-compatibility-validator.api +++ b/binary-compatibility-validator/api/binary-compatibility-validator.api @@ -290,7 +290,7 @@ public final class net/mamoe/mirai/contact/ContactKt { } public final class net/mamoe/mirai/contact/ContactList : java/util/Collection, kotlin/jvm/internal/markers/KMappedMarker { - public final field delegate Ljava/util/concurrent/ConcurrentLinkedQueue; + public final field delegate Ljava/util/Collection; public synthetic fun add (Ljava/lang/Object;)Z public fun add (Lnet/mamoe/mirai/contact/Contact;)Z public fun addAll (Ljava/util/Collection;)Z diff --git a/build.gradle.kts b/build.gradle.kts index eca17782463..016a78bd2f5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,8 +15,10 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType buildscript { repositories { -// mavenLocal() - // maven(url = "https://mirrors.huaweicloud.com/repository/maven") + if (System.getProperty("use.maven.local") == "true") { + mavenLocal() + } + mavenCentral() gradlePluginPortal() google() @@ -76,8 +78,10 @@ allprojects { version = Versions.project repositories { - // mavenLocal() // cheching issue cause compiler exception - // maven(url = "https://mirrors.huaweicloud.com/repository/maven") + if (System.getProperty("use.maven.local") == "true") { + mavenLocal() + } + mavenCentral() gradlePluginPortal() google() diff --git a/buildSrc/src/main/kotlin/ProjectConfigure.kt b/buildSrc/src/main/kotlin/ProjectConfigure.kt index 5c10712f8e0..9b57df9f8c4 100644 --- a/buildSrc/src/main/kotlin/ProjectConfigure.kt +++ b/buildSrc/src/main/kotlin/ProjectConfigure.kt @@ -12,9 +12,9 @@ import org.gradle.api.JavaVersion import org.gradle.api.Project import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.tasks.bundling.Jar import org.gradle.api.tasks.compile.JavaCompile import org.gradle.api.tasks.testing.Test -import org.gradle.api.tasks.bundling.Jar import org.gradle.kotlin.dsl.* import org.jetbrains.kotlin.gradle.dsl.* import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType @@ -144,7 +144,9 @@ val experimentalAnnotations = arrayOf( "net.mamoe.mirai.message.data.ExperimentalMessageKey", "net.mamoe.mirai.console.ConsoleFrontEndImplementation", "net.mamoe.mirai.console.util.ConsoleInternalApi", - "net.mamoe.mirai.console.util.ConsoleExperimentalApi" + "net.mamoe.mirai.console.util.ConsoleExperimentalApi", + + "kotlinx.io.core.internal.DangerousInternalIoApi", ) fun Project.configureKotlinExperimentalUsages() { diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 8e51745bfda..4ae3210e947 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -51,7 +51,7 @@ object Versions { // If you the versions below, you need to sync changes to mirai-console/buildSrc/src/main/kotlin/Versions.kt - const val yamlkt = "0.10.0" + const val yamlkt = "0.10.2" const val intellijGradlePlugin = "1.1" const val kotlinIntellijPlugin = "211-1.5.20-release-284-IJ7442.40" // keep to newest as kotlinCompiler const val intellij = "2021.1.3" // don't update easily unless you want your disk space -= 500MB diff --git a/gradle.properties b/gradle.properties index 9acc1ce11f8..5f3ba4fbbf6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,3 +22,5 @@ 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 + +systemProp.use.maven.local=false \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/contact/ContactList.kt b/mirai-core-api/src/commonMain/kotlin/contact/ContactList.kt index 32cb82a99cb..9d93684fae4 100644 --- a/mirai-core-api/src/commonMain/kotlin/contact/ContactList.kt +++ b/mirai-core-api/src/commonMain/kotlin/contact/ContactList.kt @@ -21,11 +21,9 @@ import java.util.concurrent.ConcurrentLinkedQueue * @see ContactList.asSequence */ @Suppress("unused") -public class ContactList<C : Contact> -@MiraiInternalApi public constructor(@JvmField @MiraiInternalApi public val delegate: ConcurrentLinkedQueue<C>) : +public class ContactList<out C : Contact> +@MiraiInternalApi public constructor(@JvmField @MiraiInternalApi public val delegate: MutableCollection<@UnsafeVariance C>) : Collection<C> by delegate { - @MiraiInternalApi - public constructor(collection: Collection<C>) : this(ConcurrentLinkedQueue(collection)) @MiraiInternalApi public constructor() : this(ConcurrentLinkedQueue()) diff --git a/mirai-core-api/src/commonMain/kotlin/event/events/friend.kt b/mirai-core-api/src/commonMain/kotlin/event/events/friend.kt index 5a38dd007f2..d28495c0c00 100644 --- a/mirai-core-api/src/commonMain/kotlin/event/events/friend.kt +++ b/mirai-core-api/src/commonMain/kotlin/event/events/friend.kt @@ -28,34 +28,34 @@ import java.util.concurrent.atomic.AtomicBoolean /** * 好友昵称改变事件. 目前仅支持解析 (来自 PC 端的修改). */ -public data class FriendRemarkChangeEvent internal constructor( +public data class FriendRemarkChangeEvent @MiraiInternalApi public constructor( public override val friend: Friend, public val oldRemark: String, - public val newRemark: String + public val newRemark: String, ) : FriendEvent, Packet, AbstractEvent(), FriendInfoChangeEvent /** * 成功添加了一个新好友的事件 */ -public data class FriendAddEvent @MiraiInternalApi constructor( +public data class FriendAddEvent @MiraiInternalApi public constructor( /** * 新好友. 已经添加到 [Bot.friends] */ - public override val friend: Friend + public override val friend: Friend, ) : FriendEvent, Packet, AbstractEvent(), FriendInfoChangeEvent /** * 好友已被删除或主动删除的事件. */ -public data class FriendDeleteEvent internal constructor( - public override val friend: Friend +public data class FriendDeleteEvent @MiraiInternalApi public constructor( + public override val friend: Friend, ) : FriendEvent, Packet, AbstractEvent(), FriendInfoChangeEvent /** * 一个账号请求添加机器人为好友的事件 */ @Suppress("DEPRECATION") -public data class NewFriendRequestEvent internal constructor( +public data class NewFriendRequestEvent @MiraiInternalApi public constructor( public override val bot: Bot, /** * 事件唯一识别号 @@ -76,7 +76,7 @@ public data class NewFriendRequestEvent internal constructor( /** * 群名片或好友昵称 */ - public val fromNick: String + public val fromNick: String, ) : BotEvent, Packet, AbstractEvent(), FriendInfoChangeEvent { @JvmField internal val responded: AtomicBoolean = AtomicBoolean(false) @@ -97,25 +97,25 @@ public data class NewFriendRequestEvent internal constructor( /** * [Friend] 头像被修改. 在此事件广播前就已经修改完毕. */ -public data class FriendAvatarChangedEvent internal constructor( - public override val friend: Friend +public data class FriendAvatarChangedEvent @MiraiInternalApi public constructor( + public override val friend: Friend, ) : FriendEvent, Packet, AbstractEvent() /** * [Friend] 昵称改变事件, 在此事件广播时好友已经完成改名 * @see BotNickChangedEvent */ -public data class FriendNickChangedEvent internal constructor( +public data class FriendNickChangedEvent @MiraiInternalApi public constructor( public override val friend: Friend, public val from: String, - public val to: String + public val to: String, ) : FriendEvent, Packet, AbstractEvent(), FriendInfoChangeEvent /** * 好友输入状态改变的事件,当开始输入文字、退出聊天窗口或清空输入框时会触发此事件 */ -public data class FriendInputStatusChangedEvent internal constructor( +public data class FriendInputStatusChangedEvent @MiraiInternalApi public constructor( public override val friend: Friend, - public val inputting: Boolean + public val inputting: Boolean, -) : FriendEvent, Packet, AbstractEvent() \ No newline at end of file + ) : FriendEvent, Packet, AbstractEvent() \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/event/events/group.kt b/mirai-core-api/src/commonMain/kotlin/event/events/group.kt index 66870e2f318..b64e3222c21 100644 --- a/mirai-core-api/src/commonMain/kotlin/event/events/group.kt +++ b/mirai-core-api/src/commonMain/kotlin/event/events/group.kt @@ -10,7 +10,7 @@ @file:JvmMultifileClass @file:JvmName("BotEventsKt") @file:Suppress( - "unused", "FunctionName", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "DEPRECATION_ERROR", + "FunctionName", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "DEPRECATION_ERROR", "MemberVisibilityCanBePrivate" ) @@ -426,6 +426,7 @@ public data class MemberJoinRequestEvent @MiraiInternalApi constructor( ) internal companion object { + @Suppress("unused") @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) @JvmStatic @JvmName("copy\$default") // avoid being mangled diff --git a/mirai-core-api/src/commonMain/kotlin/event/events/stranger.kt b/mirai-core-api/src/commonMain/kotlin/event/events/stranger.kt index fda01c51eac..d7e24b36d82 100644 --- a/mirai-core-api/src/commonMain/kotlin/event/events/stranger.kt +++ b/mirai-core-api/src/commonMain/kotlin/event/events/stranger.kt @@ -14,16 +14,17 @@ import net.mamoe.mirai.contact.Friend import net.mamoe.mirai.contact.Stranger import net.mamoe.mirai.event.AbstractEvent import net.mamoe.mirai.internal.network.Packet +import net.mamoe.mirai.utils.MiraiInternalApi /** * 新增陌生人的事件 * */ -public data class StrangerAddEvent internal constructor( +public data class StrangerAddEvent @MiraiInternalApi public constructor( /** * 新的陌生人. 已经添加到 [Bot.strangers] */ - public override val stranger: Stranger + public override val stranger: Stranger, ) : StrangerEvent, Packet, AbstractEvent() @@ -32,19 +33,16 @@ public data class StrangerAddEvent internal constructor( * */ public sealed class StrangerRelationChangeEvent( - public override val stranger: Stranger + public override val stranger: Stranger, ) : StrangerEvent, Packet, AbstractEvent() { /** - * 主动删除陌生人或陌生人被删除的事件 - * - * 除主动删除外,此事件为惰性广播,无法确保实时性 - * 目前被动删除仅会在陌生人二次添加时才会进行广播 + * 主动删除陌生人或陌生人被删除的事件, 不一定能接收到被动删除的事件 */ public class Deleted( /** * 被删除的陌生人 */ - stranger: Stranger + stranger: Stranger, ) : StrangerRelationChangeEvent(stranger) /** @@ -62,7 +60,7 @@ public sealed class StrangerRelationChangeEvent( * * 已经添加到Bot的好友列表中 */ - public val friend: Friend + public val friend: Friend, ) : StrangerRelationChangeEvent(stranger) } \ No newline at end of file diff --git a/mirai-core-utils/build.gradle.kts b/mirai-core-utils/build.gradle.kts index 37caa8d6d2d..e0464ee5541 100644 --- a/mirai-core-utils/build.gradle.kts +++ b/mirai-core-utils/build.gradle.kts @@ -67,6 +67,12 @@ kotlin { } } + val commonTest by getting { + dependencies { + api(yamlkt) + } + } + if (isAndroidSDKAvailable) { val androidMain by getting { // diff --git a/mirai-core-utils/src/commonMain/kotlin/Bytes.kt b/mirai-core-utils/src/commonMain/kotlin/Bytes.kt index 733beb89ff6..a3706d044cd 100644 --- a/mirai-core-utils/src/commonMain/kotlin/Bytes.kt +++ b/mirai-core-utils/src/commonMain/kotlin/Bytes.kt @@ -104,6 +104,33 @@ public fun ByteArray.checkOffsetAndLength(offset: Int, length: Int) { require(offset + length <= this.size) { "offset ($offset) + length ($length) > array.size (${this.size})" } } +@JvmOverloads +@Suppress("DuplicatedCode") // false positive. foreach is not common to UByteArray and ByteArray +public fun Array<Byte>.toUHexString( + separator: String = " ", + offset: Int = 0, + length: Int = this.size - offset +): String { + require(offset >= 0) { "offset shouldn't be negative: $offset" } + require(length >= 0) { "length shouldn't be negative: $length" } + require(offset + length <= this.size) { "offset ($offset) + length ($length) > array.size (${this.size})" } + + if (length == 0) { + return "" + } + val lastIndex = offset + length + return buildString(length * 2) { + this@toUHexString.forEachIndexed { index, it -> + if (index in offset until lastIndex) { + var ret = it.toUByte().toString(16).uppercase() + if (ret.length == 1) ret = "0$ret" + append(ret) + if (index < lastIndex - 1) append(separator) + } + } + } +} + @JvmOverloads @Suppress("DuplicatedCode") // false positive. foreach is not common to UByteArray and ByteArray diff --git a/mirai-core-utils/src/commonMain/kotlin/CollectionDiff.kt b/mirai-core-utils/src/commonMain/kotlin/CollectionDiff.kt new file mode 100644 index 00000000000..2a7e1149f09 --- /dev/null +++ b/mirai-core-utils/src/commonMain/kotlin/CollectionDiff.kt @@ -0,0 +1,24 @@ +/* + * 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/dev/LICENSE + */ + +package net.mamoe.mirai.utils + +public class CollectionDiff<E> { + private var save: Collection<E> = listOf() + + public fun save(collection: Collection<E>) { + save = collection.toList() + } + + public fun subtract(collection: Collection<E>): Collection<E> = collection subtract save + + public fun subtractAndSave(collection: Collection<E>): Collection<E> { + return subtract(collection).also { save(collection) } + } +} \ No newline at end of file diff --git a/mirai-core-utils/src/commonMain/kotlin/Numbers.kt b/mirai-core-utils/src/commonMain/kotlin/Numbers.kt index e0ecaf564ef..7b894cbb553 100644 --- a/mirai-core-utils/src/commonMain/kotlin/Numbers.kt +++ b/mirai-core-utils/src/commonMain/kotlin/Numbers.kt @@ -12,4 +12,5 @@ package net.mamoe.mirai.utils -public fun Int.toLongUnsigned(): Long = this.toLong().and(0xFFFF_FFFF) \ No newline at end of file +public fun Int.toLongUnsigned(): Long = this.toLong().and(0xFFFF_FFFF) +public fun Short.toIntUnsigned(): Int = this.toUShort().toInt() \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/utils/io/JceStruct.kt b/mirai-core-utils/src/commonMain/kotlin/Resources.kt similarity index 60% rename from mirai-core/src/commonMain/kotlin/utils/io/JceStruct.kt rename to mirai-core-utils/src/commonMain/kotlin/Resources.kt index 27b66490073..11452e6d49e 100644 --- a/mirai-core/src/commonMain/kotlin/utils/io/JceStruct.kt +++ b/mirai-core-utils/src/commonMain/kotlin/Resources.kt @@ -7,6 +7,10 @@ * https://github.com/mamoe/mirai/blob/dev/LICENSE */ -package net.mamoe.mirai.internal.utils.io +package net.mamoe.mirai.utils -internal interface JceStruct \ No newline at end of file + +@TestOnly +public fun readResource(url: String): String = + Thread.currentThread().contextClassLoader.getResourceAsStream(url)?.readBytes()?.decodeToString() + ?: error("Could not find resource '$url'") diff --git a/mirai-core-utils/src/commonMain/kotlin/ResultExtensions.kt b/mirai-core-utils/src/commonMain/kotlin/ResultExtensions.kt new file mode 100644 index 00000000000..645ca704686 --- /dev/null +++ b/mirai-core-utils/src/commonMain/kotlin/ResultExtensions.kt @@ -0,0 +1,153 @@ +/* + * 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:JvmName("MiraiUtils") + +package net.mamoe.mirai.utils + +import kotlin.reflect.KClass + + +@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE") +@kotlin.internal.InlineOnly +@kotlin.internal.LowPriorityInOverloadResolution +public inline fun <R, T : R> Result<T>.recoverCatchingSuppressed(transform: (exception: Throwable) -> R): Result<R> { + return when (val exception = exceptionOrNull()) { + null -> this + else -> { + try { + Result.success(transform(exception)) + } catch (e: Throwable) { + e.addSuppressed(exception) + Result.failure(e) + } + } + } +} + +@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE") +@kotlin.internal.InlineOnly +@kotlin.internal.LowPriorityInOverloadResolution +public inline fun <R> retryCatching( + n: Int, + except: KClass<out Throwable>? = null, + block: (count: Int, lastException: Throwable?) -> R, +): Result<R> { + require(n >= 0) { + "param n for retryCatching must not be negative" + } + var exception: Throwable? = null + repeat(n) { + try { + return Result.success(block(it, exception)) + } catch (e: Throwable) { + if (except?.isInstance(e) == true) { + return Result.failure(e) + } + exception?.addSuppressed(e) + exception = e + } + } + return Result.failure(exception!!) +} + +@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE") +@kotlin.internal.InlineOnly +@kotlin.internal.LowPriorityInOverloadResolution +public inline fun <R> retryCatchingExceptions( + n: Int, + except: KClass<out Exception>? = null, + block: (count: Int, lastException: Throwable?) -> R, +): Result<R> { + require(n >= 0) { + "param n for retryCatching must not be negative" + } + var exception: Throwable? = null + repeat(n) { + try { + return Result.success(block(it, exception)) + } catch (e: Exception) { + if (except?.isInstance(e) == true) { + return Result.failure(e) + } + exception?.addSuppressed(e) + exception = e + } + } + return Result.failure(exception!!) +} + + +@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE") +@kotlin.internal.InlineOnly +public inline fun <R> retryCatching( + n: Int, + except: KClass<out Throwable>? = null, + block: () -> R, +): Result<R> { + require(n >= 0) { + "param n for retryCatching must not be negative" + } + var exception: Throwable? = null + repeat(n) { + try { + return Result.success(block()) + } catch (e: Throwable) { + if (except?.isInstance(e) == true) { + return Result.failure(e) + } + exception?.addSuppressed(e) + exception = e + } + } + return Result.failure(exception!!) +} + +@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE") +@kotlin.internal.InlineOnly +public inline fun <R> retryCatchingExceptions( + n: Int, + except: KClass<out Exception>? = null, + block: () -> R, +): Result<R> { + require(n >= 0) { + "param n for retryCatching must not be negative" + } + var exception: Throwable? = null + repeat(n) { + try { + return Result.success(block()) + } catch (e: Exception) { + if (except?.isInstance(e) == true) { + return Result.failure(e) + } + exception?.addSuppressed(e) + exception = e + } + } + return Result.failure(exception!!) +} + +@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE") +@kotlin.internal.InlineOnly +public inline fun <R> runCatchingExceptions(block: () -> R): Result<R> { + return try { + Result.success(block()) + } catch (e: Exception) { + Result.failure(e) + } +} + +public inline fun <R> Result<R>.mapFailure( + block: (Throwable) -> Throwable, +): Result<R> = onFailure { + return Result.failure(block(it)) +} \ No newline at end of file diff --git a/mirai-core-utils/src/commonMain/kotlin/StandardUtils.kt b/mirai-core-utils/src/commonMain/kotlin/StandardUtils.kt index 099308097cd..608b0422988 100644 --- a/mirai-core-utils/src/commonMain/kotlin/StandardUtils.kt +++ b/mirai-core-utils/src/commonMain/kotlin/StandardUtils.kt @@ -13,151 +13,35 @@ package net.mamoe.mirai.utils import java.util.* -import kotlin.reflect.KClass +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract -public inline fun <reified T> Any?.cast(): T = this as T - -public inline fun <reified T> Any?.safeCast(): T? = this as? T - -public inline fun <reified T> Any?.castOrNull(): T? = this as? T - -public inline fun <reified R> Iterable<*>.firstIsInstanceOrNull(): R? { - for (it in this) { - if (it is R) return it - } - return null +public inline fun <reified T> Any?.cast(): T { + contract { returns() implies (this@cast is T) } + return this as T } - -@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE") -@kotlin.internal.InlineOnly -@kotlin.internal.LowPriorityInOverloadResolution -public inline fun <R, T : R> Result<T>.recoverCatchingSuppressed(transform: (exception: Throwable) -> R): Result<R> { - return when (val exception = exceptionOrNull()) { - null -> this - else -> { - try { - Result.success(transform(exception)) - } catch (e: Throwable) { - e.addSuppressed(exception) - Result.failure(e) - } - } - } +public inline fun <reified T> Any?.safeCast(): T? { + contract { returnsNotNull() implies (this@safeCast is T) } + return this as? T } -@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE") -@kotlin.internal.InlineOnly -@kotlin.internal.LowPriorityInOverloadResolution -public inline fun <R> retryCatching( - n: Int, - except: KClass<out Throwable>? = null, - block: (count: Int, lastException: Throwable?) -> R -): Result<R> { - require(n >= 0) { - "param n for retryCatching must not be negative" - } - var exception: Throwable? = null - repeat(n) { - try { - return Result.success(block(it, exception)) - } catch (e: Throwable) { - if (except?.isInstance(e) == true) { - return Result.failure(e) - } - exception?.addSuppressed(e) - exception = e - } - } - return Result.failure(exception!!) +public inline fun <reified T> Any?.castOrNull(): T? { + contract { returnsNotNull() implies (this@castOrNull is T) } + return this as? T } -@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE") -@kotlin.internal.InlineOnly -@kotlin.internal.LowPriorityInOverloadResolution -public inline fun <R> retryCatchingExceptions( - n: Int, - except: KClass<out Exception>? = null, - block: (count: Int, lastException: Throwable?) -> R -): Result<R> { - require(n >= 0) { - "param n for retryCatching must not be negative" - } - var exception: Throwable? = null - repeat(n) { - try { - return Result.success(block(it, exception)) - } catch (e: Exception) { - if (except?.isInstance(e) == true) { - return Result.failure(e) - } - exception?.addSuppressed(e) - exception = e - } - } - return Result.failure(exception!!) -} +@Suppress("NOTHING_TO_INLINE", "UNCHECKED_CAST") +public inline fun <T> Any?.uncheckedCast(): T = this as T -@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE") -@kotlin.internal.InlineOnly -public inline fun <R> retryCatching( - n: Int, - except: KClass<out Throwable>? = null, - block: () -> R -): Result<R> { - require(n >= 0) { - "param n for retryCatching must not be negative" - } - var exception: Throwable? = null - repeat(n) { - try { - return Result.success(block()) - } catch (e: Throwable) { - if (except?.isInstance(e) == true) { - return Result.failure(e) - } - exception?.addSuppressed(e) - exception = e - } - } - return Result.failure(exception!!) -} - -@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE") -@kotlin.internal.InlineOnly -public inline fun <R> retryCatchingExceptions( - n: Int, - except: KClass<out Exception>? = null, - block: () -> R -): Result<R> { - require(n >= 0) { - "param n for retryCatching must not be negative" - } - var exception: Throwable? = null - repeat(n) { - try { - return Result.success(block()) - } catch (e: Exception) { - if (except?.isInstance(e) == true) { - return Result.failure(e) - } - exception?.addSuppressed(e) - exception = e - } +public inline fun <reified R> Iterable<*>.firstIsInstanceOrNull(): R? { + for (it in this) { + if (it is R) return it } - return Result.failure(exception!!) + return null } -@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE") -@kotlin.internal.InlineOnly -public inline fun <R> runCatchingExceptions(block: () -> R): Result<R> { - return try { - Result.success(block()) - } catch (e: Exception) { - Result.failure(e) - } -} public inline fun <E> MutableList<E>.replaceAllKotlin(operator: (E) -> E) { val li: MutableListIterator<E> = this.listIterator() @@ -166,16 +50,6 @@ public inline fun <E> MutableList<E>.replaceAllKotlin(operator: (E) -> E) { } } -public fun systemProp(name: String, default: String): String = - System.getProperty(name, default) ?: default - -public fun systemProp(name: String, default: Boolean): Boolean = - System.getProperty(name, default.toString())?.toBoolean() ?: default - - -public fun systemProp(name: String, default: Long): Long = - System.getProperty(name, default.toString())?.toLongOrNull() ?: default - public fun Throwable.getRootCause(maxDepth: Int = 20): Throwable { var depth = 0 @@ -224,4 +98,18 @@ public fun String.truncated(length: Int, truncated: String = "..."): String { return if (this.length > length) { this.take(10) + truncated } else this -} \ No newline at end of file +} + +/** + * Similar to [run] bot with [Unit] return type. + * + * You should not reference to [T] directly in the [block]. + */ +// can convert to contextual receiver in the future, or there might be a stdlib function which we can delegate to. +public inline fun <T> T.context(block: T.() -> Unit) { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return block() +} + +public fun assertUnreachable(hint: String? = null): Nothing = + error("This clause should not be reached. " + hint.orEmpty()) \ No newline at end of file diff --git a/mirai-core-utils/src/commonMain/kotlin/TypeSafeMap.kt b/mirai-core-utils/src/commonMain/kotlin/TypeSafeMap.kt new file mode 100644 index 00000000000..50d15e7084f --- /dev/null +++ b/mirai-core-utils/src/commonMain/kotlin/TypeSafeMap.kt @@ -0,0 +1,131 @@ +/* + * 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("NOTHING_TO_INLINE") + +package net.mamoe.mirai.utils + +import kotlinx.serialization.Serializable +import java.util.concurrent.ConcurrentHashMap +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +@Serializable +@JvmInline +public value class TypeKey<T>(public val name: String) { + override fun toString(): String = "Key($name)" + + public inline infix fun to(value: T): TypeSafeMap = buildTypeSafeMap { set(this@TypeKey, value) } +} + +/** + * @see buildTypeSafeMap + */ +public sealed interface TypeSafeMap { + public val size: Int + + public operator fun <T> get(key: TypeKey<T>): T + public operator fun <T> contains(key: TypeKey<T>): Boolean = get(key) != null + + public fun toMapBoxed(): Map<TypeKey<*>, Any?> + public fun toMap(): Map<String, Any?> + + public companion object { + public val EMPTY: TypeSafeMap = TypeSafeMapImpl(emptyMap()) + } +} + +public operator fun TypeSafeMap.plus(other: TypeSafeMap): TypeSafeMap { + return when { + other.size == 0 -> this + this.size == 0 -> other + else -> buildTypeSafeMap { + setAll(this@plus) + setAll(other) + } + } +} + +public sealed interface MutableTypeSafeMap : TypeSafeMap { + public operator fun <T> set(key: TypeKey<T>, value: T) + public fun <T> remove(key: TypeKey<T>): T? + public fun setAll(other: TypeSafeMap) +} + + +@PublishedApi +internal open class TypeSafeMapImpl( + @PublishedApi internal open val map: Map<String, Any?> = ConcurrentHashMap() +) : TypeSafeMap { + override val size: Int get() = map.size + + override fun equals(other: Any?): Boolean { + return other is TypeSafeMapImpl && other.map == this.map + } + + override fun hashCode(): Int { + return map.hashCode() + } + + override fun toString(): String { + return "TypeSafeMapImpl(map=$map)" + } + + override operator fun <T> get(key: TypeKey<T>): T = + map[key.name]?.uncheckedCast() ?: throw NoSuchElementException(key.toString()) + + override operator fun <T> contains(key: TypeKey<T>): Boolean = get(key) != null + + override fun toMapBoxed(): Map<TypeKey<*>, Any?> = map.mapKeys { TypeKey<Any?>(it.key) } + override fun toMap(): Map<String, Any?> = map +} + +@PublishedApi +internal class MutableTypeSafeMapImpl( + @PublishedApi override val map: MutableMap<String, Any?> = ConcurrentHashMap() +) : TypeSafeMap, MutableTypeSafeMap, TypeSafeMapImpl(map) { + override fun equals(other: Any?): Boolean { + return other is MutableTypeSafeMapImpl && other.map == this.map + } + + override fun hashCode(): Int { + return map.hashCode() + } + + override fun toString(): String { + return "MutableTypeSafeMapImpl(map=$map)" + } + + override operator fun <T> set(key: TypeKey<T>, value: T) { + map[key.name] = value + } + + override fun setAll(other: TypeSafeMap) { + if (other is TypeSafeMapImpl) { + map.putAll(other.map) + } else { + map.putAll(other.toMap()) + } + } + + override fun <T> remove(key: TypeKey<T>): T? = map.remove(key.name)?.uncheckedCast() +} + +public inline fun MutableTypeSafeMap(): MutableTypeSafeMap = MutableTypeSafeMapImpl() +public inline fun MutableTypeSafeMap(map: Map<String, Any?>): MutableTypeSafeMap = + MutableTypeSafeMapImpl().also { it.map.putAll(map) } + +public inline fun TypeSafeMap(): TypeSafeMap = TypeSafeMap.EMPTY +public inline fun TypeSafeMap(map: Map<String, Any?>): TypeSafeMap = + MutableTypeSafeMapImpl().also { it.map.putAll(map) } + +public inline fun buildTypeSafeMap(block: MutableTypeSafeMap.() -> Unit): MutableTypeSafeMap { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return MutableTypeSafeMapImpl().apply(block) +} diff --git a/mirai-core-utils/src/commonMain/kotlin/systemProp.kt b/mirai-core-utils/src/commonMain/kotlin/systemProp.kt new file mode 100644 index 00000000000..3c9faf3019e --- /dev/null +++ b/mirai-core-utils/src/commonMain/kotlin/systemProp.kt @@ -0,0 +1,39 @@ +/* + * 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:JvmName("MiraiUtils") + +package net.mamoe.mirai.utils + +import java.util.concurrent.ConcurrentHashMap + + +public fun systemProp(name: String, default: String): String = + System.getProperty(name, default) ?: default + +public fun systemProp(name: String, default: Boolean): Boolean = + System.getProperty(name, default.toString())?.toBoolean() ?: default + + +public fun systemProp(name: String, default: Long): Long = + System.getProperty(name, default.toString())?.toLongOrNull() ?: default + + +private val debugProps = ConcurrentHashMap<String, Boolean>() +public fun Any?.toDebugString(prop: String, default: Boolean = false): String { + if (this == null) return "null" + val debug = debugProps.getOrPut(prop) { systemProp(prop, default) } + return if (debug) { + "${this::class.simpleName}($this)" + } else { + "${this::class.simpleName}" + } +} \ No newline at end of file diff --git a/mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/TypeSafeMapTest.kt b/mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/TypeSafeMapTest.kt new file mode 100644 index 00000000000..e10e313946d --- /dev/null +++ b/mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/TypeSafeMapTest.kt @@ -0,0 +1,79 @@ +/* + * 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/dev/LICENSE + */ + +package net.mamoe.mirai.utils + +import net.mamoe.yamlkt.Yaml +import net.mamoe.yamlkt.YamlBuilder +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class TypeSafeMapTest { + + private val myKey = TypeKey<String>("test") + private val myKey2 = TypeKey<CharSequence>("test2") + + @Test + fun `can set get`() { + val map = MutableTypeSafeMap() + map[myKey] = "str" + map[myKey2] = "str2" + assertEquals(2, map.size) + assertEquals("str", map[myKey]) + assertEquals("str2", map[myKey2]) + } + + @Test + fun `key is inlined`() { + val map = MutableTypeSafeMap() + map[TypeKey<String>("test")] = "str" + map[TypeKey<String>("test")] = "str2" + assertEquals(1, map.size) + assertEquals("str2", map[TypeKey("test")]) + } + + @Test + fun `can toMap`() { + val map = MutableTypeSafeMap() + map[myKey] = "str" + map[myKey2] = "str2" + assertEquals(2, map.size) + + val map1 = map.toMapBoxed() + + assertEquals(2, map1.size) + assertEquals("str", map1[myKey]) + assertEquals("str2", map1[myKey2]) + } + + @Test + fun `test serialization`() { + val map = MutableTypeSafeMap() + map[myKey] = "str" + map[myKey2] = "str2" + assertEquals(2, map.size) + + val map1 = map.toMap() + + // Json does not support reflective serialization, so we use Yaml in JSON format + val yaml = Yaml { + classSerialization = YamlBuilder.MapSerialization.FLOW_MAP + mapSerialization = YamlBuilder.MapSerialization.FLOW_MAP + listSerialization = YamlBuilder.ListSerialization.FLOW_SEQUENCE + stringSerialization = YamlBuilder.StringSerialization.DOUBLE_QUOTATION + encodeDefaultValues = true + } + + val string = yaml.encodeToString(map1) + println(string) // { "test2": "str2" ,"test": "str" } + + val result = MutableTypeSafeMap(Yaml.decodeMapFromString(string).cast()) + assertEquals(map, result) + } +} \ No newline at end of file diff --git a/mirai-core/build.gradle.kts b/mirai-core/build.gradle.kts index e3503407b06..e2753e28caa 100644 --- a/mirai-core/build.gradle.kts +++ b/mirai-core/build.gradle.kts @@ -80,6 +80,7 @@ kotlin { commonTest { dependencies { implementation(kotlin("script-runtime")) + api(yamlkt) } } diff --git a/mirai-core/src/commonMain/kotlin/AbstractBot.kt b/mirai-core/src/commonMain/kotlin/AbstractBot.kt index c07bf955827..59f504a4368 100644 --- a/mirai-core/src/commonMain/kotlin/AbstractBot.kt +++ b/mirai-core/src/commonMain/kotlin/AbstractBot.kt @@ -13,13 +13,13 @@ package net.mamoe.mirai.internal import kotlinx.coroutines.* import net.mamoe.mirai.Bot import net.mamoe.mirai.Mirai -import net.mamoe.mirai.contact.* +import net.mamoe.mirai.contact.ContactList import net.mamoe.mirai.event.EventChannel import net.mamoe.mirai.event.GlobalEventChannel import net.mamoe.mirai.event.events.BotEvent +import net.mamoe.mirai.internal.contact.* import net.mamoe.mirai.internal.contact.info.FriendInfoImpl import net.mamoe.mirai.internal.contact.info.StrangerInfoImpl -import net.mamoe.mirai.internal.contact.uin import net.mamoe.mirai.internal.network.component.ComponentStorage import net.mamoe.mirai.internal.network.components.SsoProcessor import net.mamoe.mirai.internal.network.handler.NetworkHandler @@ -90,13 +90,18 @@ internal abstract class AbstractBot constructor( final override val eventChannel: EventChannel<BotEvent> = GlobalEventChannel.filterIsInstance<BotEvent>().filter { it.bot === this@AbstractBot } - final override val otherClients: ContactList<OtherClient> = ContactList() - final override val friends: ContactList<Friend> = ContactList() - final override val groups: ContactList<Group> = ContactList() - final override val strangers: ContactList<Stranger> = ContactList() + final override val otherClients: ContactList<OtherClientImpl> = ContactList() + final override val friends: ContactList<FriendImpl> = ContactList() + final override val groups: ContactList<GroupImpl> = ContactList() + final override val strangers: ContactList<StrangerImpl> = ContactList() - final override val asFriend: Friend by lazy { Mirai.newFriend(this, FriendInfoImpl(uin, nick, "")) } - final override val asStranger: Stranger by lazy { Mirai.newStranger(this, StrangerInfoImpl(bot.id, bot.nick)) } + final override val asFriend: FriendImpl by lazy { + Mirai.newFriend(this, FriendInfoImpl(uin, "", "")).cast() + } // nick is initialized later on login + final override val asStranger: StrangerImpl by lazy { + Mirai.newStranger(this, StrangerInfoImpl(bot.id, bot.nick)).cast() + } + final override var nick: String by asFriend.info::nick override fun close(cause: Throwable?) { if (!this.isActive) return diff --git a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt index f7cf286a201..895f9a81275 100644 --- a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt +++ b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt @@ -14,7 +14,8 @@ import io.ktor.client.engine.okhttp.* import io.ktor.client.features.* import io.ktor.client.request.* import io.ktor.client.request.forms.* -import kotlinx.coroutines.SupervisorJob +import io.ktor.http.* +import io.ktor.utils.io.core.* import kotlinx.coroutines.currentCoroutineContext import kotlinx.io.core.discardExact import kotlinx.io.core.readBytes @@ -30,7 +31,9 @@ import net.mamoe.mirai.event.broadcast import net.mamoe.mirai.event.events.* import net.mamoe.mirai.internal.contact.* import net.mamoe.mirai.internal.contact.info.FriendInfoImpl +import net.mamoe.mirai.internal.contact.info.FriendInfoImpl.Companion.impl import net.mamoe.mirai.internal.contact.info.MemberInfoImpl +import net.mamoe.mirai.internal.contact.info.StrangerInfoImpl.Companion.impl import net.mamoe.mirai.internal.message.* import net.mamoe.mirai.internal.message.DeepMessageRefiner.refineDeep import net.mamoe.mirai.internal.network.components.EventDispatcher @@ -335,20 +338,20 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor { } @LowLevelApi - override fun newFriend(bot: Bot, friendInfo: FriendInfo): Friend { + override fun newFriend(bot: Bot, friendInfo: FriendInfo): FriendImpl { return FriendImpl( bot.asQQAndroidBot(), - bot.coroutineContext + SupervisorJob(bot.supervisorJob), - friendInfo + bot.coroutineContext, + friendInfo.impl(), ) } @LowLevelApi - override fun newStranger(bot: Bot, strangerInfo: StrangerInfo): Stranger { + override fun newStranger(bot: Bot, strangerInfo: StrangerInfo): StrangerImpl { return StrangerImpl( bot.asQQAndroidBot(), - bot.coroutineContext + SupervisorJob(bot.supervisorJob), - strangerInfo + bot.coroutineContext, + strangerInfo.impl(), ) } @@ -573,7 +576,7 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor { val rep = network.run { Mirai.Http.get<String> { url("https://qqweb.qq.com/c/activedata/get_mygroup_data") - parameter("bkn", bkn) + parameter("bkn", client.wLoginSigInfo.bkn) parameter("gc", groupId) if (page != -1) { parameter("page", page) @@ -791,7 +794,7 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor { append("group_code", groupId) append("seconds", seconds) append("anony_nick", anonymousNick) - append("bkn", bot.bkn) + append("bkn", bot.client.wLoginSigInfo.bkn) }) headers { append( diff --git a/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt b/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt index ea84c3632be..efbea31d6f7 100644 --- a/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt +++ b/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt @@ -14,12 +14,9 @@ import kotlinx.atomicfu.atomic import kotlinx.coroutines.isActive import kotlinx.coroutines.runBlocking import net.mamoe.mirai.Bot -import net.mamoe.mirai.Mirai -import net.mamoe.mirai.contact.Group import net.mamoe.mirai.event.events.BotOfflineEvent import net.mamoe.mirai.event.events.BotOnlineEvent import net.mamoe.mirai.event.events.BotReloginEvent -import net.mamoe.mirai.internal.contact.checkIsGroupImpl import net.mamoe.mirai.internal.network.component.ComponentStorage import net.mamoe.mirai.internal.network.component.ComponentStorageDelegate import net.mamoe.mirai.internal.network.component.ConcurrentComponentStorage @@ -40,6 +37,17 @@ import net.mamoe.mirai.internal.network.handler.state.StateObserver import net.mamoe.mirai.internal.network.handler.state.safe import net.mamoe.mirai.internal.network.impl.netty.ForceOfflineException import net.mamoe.mirai.internal.network.impl.netty.NettyNetworkHandlerFactory +import net.mamoe.mirai.internal.network.notice.TraceLoggingNoticeProcessor +import net.mamoe.mirai.internal.network.notice.UnconsumedNoticesAlerter +import net.mamoe.mirai.internal.network.notice.decoders.GroupNotificationDecoder +import net.mamoe.mirai.internal.network.notice.decoders.MsgInfoDecoder +import net.mamoe.mirai.internal.network.notice.group.GroupMessageProcessor +import net.mamoe.mirai.internal.network.notice.group.GroupNotificationProcessor +import net.mamoe.mirai.internal.network.notice.group.GroupOrMemberListNoticeProcessor +import net.mamoe.mirai.internal.network.notice.group.GroupRecallProcessor +import net.mamoe.mirai.internal.network.notice.priv.FriendNoticeProcessor +import net.mamoe.mirai.internal.network.notice.priv.OtherClientNoticeProcessor +import net.mamoe.mirai.internal.network.notice.priv.PrivateMessageProcessor import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc import net.mamoe.mirai.internal.utils.subLogger import net.mamoe.mirai.utils.BotConfiguration @@ -153,6 +161,25 @@ internal open class QQAndroidBot constructor( // There's no need to interrupt a broadcasting event when network handler closed. set(EventDispatcher, EventDispatcherImpl(bot.coroutineContext, logger.subLogger("EventDispatcher"))) + val pipelineLogger = networkLogger.subLogger("NoticeProcessor") // shorten name + set( + NoticeProcessorPipeline, + NoticeProcessorPipelineImpl.create( + MsgInfoDecoder(pipelineLogger.subLogger("MsgInfoDecoder")), + GroupNotificationDecoder(), + + FriendNoticeProcessor(pipelineLogger.subLogger("FriendNoticeProcessor")), + GroupOrMemberListNoticeProcessor(pipelineLogger.subLogger("GroupOrMemberListNoticeProcessor")), + GroupMessageProcessor(pipelineLogger.subLogger("GroupMessageProcessor")), + GroupNotificationProcessor(pipelineLogger.subLogger("GroupNotificationProcessor")), + PrivateMessageProcessor(), + OtherClientNoticeProcessor(), + GroupRecallProcessor(), + UnconsumedNoticesAlerter(pipelineLogger.subLogger("UnconsumedNoticesAlerter")), + TraceLoggingNoticeProcessor(pipelineLogger.subLogger("TraceLoggingNoticeProcessor")) + ) + ) + set(SsoProcessorContext, SsoProcessorContextImpl(bot)) set(SsoProcessor, SsoProcessorImpl(get(SsoProcessorContext))) set(HeartbeatProcessor, HeartbeatProcessorImpl()) @@ -166,34 +193,35 @@ internal open class QQAndroidBot constructor( set(ContactUpdater, ContactUpdaterImpl(bot, components, networkLogger.subLogger("ContactUpdater"))) set( BdhSessionSyncer, - BdhSessionSyncerImpl(configuration, components, networkLogger.subLogger("BotSessionSyncer")) + BdhSessionSyncerImpl(configuration, components, networkLogger.subLogger("BotSessionSyncer")), ) set( MessageSvcSyncer, - MessageSvcSyncerImpl(bot, bot.coroutineContext, networkLogger.subLogger("MessageSvcSyncer")) + MessageSvcSyncerImpl(bot, bot.coroutineContext, networkLogger.subLogger("MessageSvcSyncer")), ) set( EcdhInitialPublicKeyUpdater, - EcdhInitialPublicKeyUpdaterImpl(bot, networkLogger.subLogger("ECDHInitialPublicKeyUpdater")) + EcdhInitialPublicKeyUpdaterImpl(bot, networkLogger.subLogger("ECDHInitialPublicKeyUpdater")), ) set(ServerList, ServerListImpl(networkLogger.subLogger("ServerList"))) set(PacketLoggingStrategy, PacketLoggingStrategyImpl(bot)) set( - PacketHandler, PacketHandlerChain( - LoggingPacketHandlerAdapter(get(PacketLoggingStrategy), networkLogger), + PacketHandler, + PacketHandlerChain( EventBroadcasterPacketHandler(components), - CallPacketFactoryPacketHandler(bot) - ) + CallPacketFactoryPacketHandler(bot), + LoggingPacketHandlerAdapter(get(PacketLoggingStrategy), networkLogger), + ), ) set(PacketCodec, PacketCodecImpl()) set( OtherClientUpdater, - OtherClientUpdaterImpl(bot, components, networkLogger.subLogger("OtherClientUpdater")) + OtherClientUpdaterImpl(bot, components, networkLogger.subLogger("OtherClientUpdater")), ) set(ConfigPushSyncer, ConfigPushSyncerImpl()) set( AccountSecretsManager, - configuration.createAccountsSecretsManager(bot.logger.subLogger("AccountSecretsManager")) + configuration.createAccountsSecretsManager(bot.logger.subLogger("AccountSecretsManager")), ) } @@ -203,6 +231,7 @@ internal open class QQAndroidBot constructor( open fun createNetworkLevelComponents(): ComponentStorage { return ConcurrentComponentStorage { set(BotClientHolder, BotClientHolderImpl(bot, networkLogger.subLogger("BotClientHolder"))) + set(SyncController, SyncControllerImpl()) }.withFallback(defaultBotLevelComponents) } @@ -215,37 +244,37 @@ internal open class QQAndroidBot constructor( val context = NetworkHandlerContextImpl( bot, networkLogger, - createNetworkLevelComponents() + createNetworkLevelComponents(), ) NettyNetworkHandlerFactory.create( context, - context[ServerList].pollAny().toSocketAddress() + context[ServerList].pollAny().toSocketAddress(), ) - } + }, ) // We can move the factory to configuration but this is not necessary for now. } +} - /** - * 获取 获取群公告 所需的 bkn 参数 - * */ // TODO: 2021/4/26 extract it after #1141 merged - val bkn: Int - get() = client.wLoginSigInfo.sKey.data - .fold(5381) { acc: Int, b: Byte -> acc + acc.shl(5) + b.toInt() } - .and(Int.MAX_VALUE) +internal fun QQAndroidBot.getGroupByUinOrFail(uin: Long) = + getGroupByUin(uin) ?: throw NoSuchElementException("group.uin=$uin") - /////////////////////////////////////////////////////////////////////////// - // contacts - /////////////////////////////////////////////////////////////////////////// +internal fun QQAndroidBot.getGroupByUin(uin: Long) = groups.firstOrNull { it.uin == uin } - override lateinit var nick: String +/** + * uin first + */ +internal fun QQAndroidBot.getGroupByUinOrCode(uinOrCode: Long) = + groups.firstOrNull { it.uin == uinOrCode } ?: groups.firstOrNull { it.id == uinOrCode } - // internally visible only - fun getGroupByUin(uin: Long): Group { - return getGroupByUinOrNull(uin) - ?: throw NoSuchElementException("Group ${Mirai.calculateGroupCodeByGroupUin(uin)} not found") - } +/** + * uin first + */ +internal fun QQAndroidBot.getGroupByUinOrCodeOrFail(uinOrCode: Long) = + getGroupByUinOrCode(uinOrCode) ?: throw NoSuchElementException("group.code or uin=$uinOrCode") - fun getGroupByUinOrNull(uin: Long): Group? { - return groups.firstOrNull { it.checkIsGroupImpl(); it.uin == uin } - } -} + +/** + * code first + */ +internal fun QQAndroidBot.getGroupByCodeOrUin(uinOrCode: Long) = + groups.firstOrNull { it.id == uinOrCode } ?: groups.firstOrNull { it.uin == uinOrCode } diff --git a/mirai-core/src/commonMain/kotlin/contact/AbstractContact.kt b/mirai-core/src/commonMain/kotlin/contact/AbstractContact.kt index 76eef94d606..2440a0a9efb 100644 --- a/mirai-core/src/commonMain/kotlin/contact/AbstractContact.kt +++ b/mirai-core/src/commonMain/kotlin/contact/AbstractContact.kt @@ -9,15 +9,14 @@ package net.mamoe.mirai.internal.contact -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.internal.QQAndroidBot +import net.mamoe.mirai.utils.childScopeContext import kotlin.coroutines.CoroutineContext internal abstract class AbstractContact( final override val bot: QQAndroidBot, - coroutineContext: CoroutineContext, + parentCoroutineContext: CoroutineContext, ) : Contact { - final override val coroutineContext: CoroutineContext = coroutineContext + SupervisorJob(coroutineContext[Job]) + final override val coroutineContext: CoroutineContext = parentCoroutineContext.childScopeContext() } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/contact/AbstractMember.kt b/mirai-core/src/commonMain/kotlin/contact/AbstractMember.kt index 17bf99da2e1..73f55e4cedd 100644 --- a/mirai-core/src/commonMain/kotlin/contact/AbstractMember.kt +++ b/mirai-core/src/commonMain/kotlin/contact/AbstractMember.kt @@ -16,11 +16,11 @@ import net.mamoe.mirai.internal.contact.info.MemberInfoImpl import net.mamoe.mirai.utils.cast import kotlin.coroutines.CoroutineContext -internal abstract class AbstractMember( +internal sealed class AbstractMember( final override val group: GroupImpl, - coroutineContext: CoroutineContext, + parentCoroutineContext: CoroutineContext, memberInfo: MemberInfo, -) : AbstractUser(group.bot, coroutineContext, memberInfo), Member { +) : AbstractUser(group.bot, parentCoroutineContext, memberInfo), Member { final override val info: MemberInfoImpl = memberInfo.cast() override val nameCard: String get() = info.nameCard diff --git a/mirai-core/src/commonMain/kotlin/contact/AbstractUser.kt b/mirai-core/src/commonMain/kotlin/contact/AbstractUser.kt index bc922271184..a47648caef1 100644 --- a/mirai-core/src/commonMain/kotlin/contact/AbstractUser.kt +++ b/mirai-core/src/commonMain/kotlin/contact/AbstractUser.kt @@ -10,6 +10,9 @@ package net.mamoe.mirai.internal.contact import net.mamoe.mirai.Mirai +import net.mamoe.mirai.contact.Friend +import net.mamoe.mirai.contact.Member +import net.mamoe.mirai.contact.Stranger import net.mamoe.mirai.contact.User import net.mamoe.mirai.data.UserInfo import net.mamoe.mirai.event.broadcast @@ -28,26 +31,45 @@ import net.mamoe.mirai.internal.network.protocol.data.proto.Cmd0x352 import net.mamoe.mirai.internal.network.protocol.packet.chat.image.ImgStore import net.mamoe.mirai.internal.network.protocol.packet.chat.image.LongConn import net.mamoe.mirai.internal.network.protocol.packet.sendAndExpect +import net.mamoe.mirai.internal.utils.AtomicIntSeq +import net.mamoe.mirai.internal.utils.C2CPkgMsgParsingCache import net.mamoe.mirai.internal.utils._miraiContentToString import net.mamoe.mirai.message.MessageReceipt -import net.mamoe.mirai.message.data.Image -import net.mamoe.mirai.message.data.Message -import net.mamoe.mirai.message.data.MessageChain -import net.mamoe.mirai.message.data.isContentEmpty +import net.mamoe.mirai.message.data.* import net.mamoe.mirai.utils.* +import kotlin.contracts.contract import kotlin.coroutines.CoroutineContext internal val User.info: UserInfo? get() = this.castOrNull<AbstractUser>()?.info -internal abstract class AbstractUser( +@Suppress("NOTHING_TO_INLINE") +internal inline fun User.impl(): AbstractUser { + contract { returns() implies (this@impl is AbstractUser) } + check(this is AbstractUser) + return this +} + +internal val User.correspondingMessageSourceKind + get() = when (this) { + is Friend -> MessageSourceKind.FRIEND + is Member -> MessageSourceKind.TEMP + is Stranger -> MessageSourceKind.STRANGER + else -> error("Unknown user: ${this::class.qualifiedName}") + } + +internal sealed class AbstractUser( bot: QQAndroidBot, - coroutineContext: CoroutineContext, + parentCoroutineContext: CoroutineContext, userInfo: UserInfo, -) : User, AbstractContact(bot, coroutineContext) { +) : User, AbstractContact(bot, parentCoroutineContext) { + final override val id: Long = userInfo.uin final override var nick: String = userInfo.nick final override val remark: String = userInfo.remark + val messageSeq = AtomicIntSeq.forMessageSeq() + val fragmentedMessageMerger = C2CPkgMsgParsingCache() + open val info: UserInfo = userInfo @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") @@ -57,7 +79,8 @@ internal abstract class AbstractUser( } val resp = bot.network.run { LongConn.OffPicUp( - bot.client, Cmd0x352.TryUpImgReq( + bot.client, + Cmd0x352.TryUpImgReq( buType = 1, srcUin = bot.id, dstUin = this@AbstractUser.id, @@ -65,8 +88,8 @@ internal abstract class AbstractUser( fileSize = resource.size, fileName = resource.md5.toUHexString("") + "." + resource.formatName, imgOriginal = true, - buildVer = bot.client.buildVer - ) + buildVer = bot.client.buildVer, + ), ).sendAndExpect<LongConn.OffPicUp.Response>() } diff --git a/mirai-core/src/commonMain/kotlin/contact/AnonymousMemberImpl.kt b/mirai-core/src/commonMain/kotlin/contact/AnonymousMemberImpl.kt index bb100fdb73c..abef0d842f1 100644 --- a/mirai-core/src/commonMain/kotlin/contact/AnonymousMemberImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/AnonymousMemberImpl.kt @@ -19,10 +19,15 @@ import kotlin.coroutines.CoroutineContext internal class AnonymousMemberImpl( group: GroupImpl, - coroutineContext: CoroutineContext, + parentCoroutineContext: CoroutineContext, memberInfo: MemberInfo, - override val anonymousId: String, -) : AnonymousMember, AbstractMember(group, coroutineContext, memberInfo) { +) : AnonymousMember, AbstractMember(group, parentCoroutineContext, memberInfo) { + init { + requireNotNull(memberInfo.anonymousId) { "anonymousId must not be null" } + } + + override val anonymousId: String get() = info.anonymousId!! + override suspend fun mute(durationSeconds: Int) { checkBotPermissionHigherThanThis("mute") getMiraiImpl().muteAnonymousMember(bot, anonymousId, nameCard, group.uin, durationSeconds) diff --git a/mirai-core/src/commonMain/kotlin/contact/FriendImpl.kt b/mirai-core/src/commonMain/kotlin/contact/FriendImpl.kt index d0d80a0bafc..d06fe3d2952 100644 --- a/mirai-core/src/commonMain/kotlin/contact/FriendImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/FriendImpl.kt @@ -14,12 +14,9 @@ package net.mamoe.mirai.internal.contact -import kotlinx.atomicfu.AtomicInt -import kotlinx.atomicfu.atomic import net.mamoe.mirai.LowLevelApi import net.mamoe.mirai.Mirai import net.mamoe.mirai.contact.Friend -import net.mamoe.mirai.data.FriendInfo import net.mamoe.mirai.event.events.FriendMessagePostSendEvent import net.mamoe.mirai.event.events.FriendMessagePreSendEvent import net.mamoe.mirai.internal.QQAndroidBot @@ -31,7 +28,6 @@ import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.audioCodec import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList -import net.mamoe.mirai.internal.utils.C2CPkgMsgParsingCache import net.mamoe.mirai.internal.utils.io.serialization.loadAs import net.mamoe.mirai.internal.utils.io.serialization.toByteArray import net.mamoe.mirai.message.MessageReceipt @@ -53,9 +49,9 @@ internal fun net.mamoe.mirai.internal.network.protocol.data.jce.FriendInfo.toMir ) @OptIn(ExperimentalContracts::class) -internal inline fun Friend.checkIsFriendImpl(): FriendImpl { +internal inline fun Friend.impl(): FriendImpl { contract { - returns() implies (this@checkIsFriendImpl is FriendImpl) + returns() implies (this@impl is FriendImpl) } check(this is FriendImpl) { "A Friend instance is not instance of FriendImpl. Your instance: ${this::class.qualifiedName}" } return this @@ -63,13 +59,9 @@ internal inline fun Friend.checkIsFriendImpl(): FriendImpl { internal class FriendImpl( bot: QQAndroidBot, - coroutineContext: CoroutineContext, - internal val friendInfo: FriendInfo, -) : Friend, AbstractUser(bot, coroutineContext, friendInfo) { - @Suppress("unused") // bug - val lastMessageSequence: AtomicInt = atomic(-1) - val friendPkgMsgParsingCache = C2CPkgMsgParsingCache() - + parentCoroutineContext: CoroutineContext, + override val info: FriendInfoImpl, +) : Friend, AbstractUser(bot, parentCoroutineContext, info) { override suspend fun delete() { check(bot.friends[this.id] != null) { "Friend ${this.id} had already been deleted" diff --git a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt index 63f773f9c1b..50dc785fe96 100644 --- a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt @@ -65,38 +65,49 @@ internal fun Group.checkIsGroupImpl(): GroupImpl { return this } +internal fun GroupImpl( + bot: QQAndroidBot, + parentCoroutineContext: CoroutineContext, + id: Long, + groupInfo: GroupInfo, + members: Sequence<MemberInfo>, +): GroupImpl { + return GroupImpl(bot, parentCoroutineContext, id, groupInfo, ContactList(ConcurrentLinkedQueue())).apply Group@{ + members.forEach { info -> + if (info.uin == bot.id) { + botAsMember = newNormalMember(info) + if (info.permission == MemberPermission.OWNER) { + owner = botAsMember + } + } else newNormalMember(info).let { member -> + if (member.permission == MemberPermission.OWNER) { + owner = member + } + this@Group.members.delegate.add(member) + } + } + } +} + @Suppress("PropertyName") -internal class GroupImpl( +internal class GroupImpl constructor( bot: QQAndroidBot, - coroutineContext: CoroutineContext, + parentCoroutineContext: CoroutineContext, override val id: Long, groupInfo: GroupInfo, - members: Sequence<MemberInfo>, -) : Group, AbstractContact(bot, coroutineContext) { + override val members: ContactList<NormalMemberImpl>, +) : Group, AbstractContact(bot, parentCoroutineContext) { companion object val uin: Long = groupInfo.uin override val settings: GroupSettingsImpl = GroupSettingsImpl(this, groupInfo) override var name: String by settings::name - override lateinit var owner: NormalMember - override lateinit var botAsMember: NormalMember + override lateinit var owner: NormalMemberImpl + override lateinit var botAsMember: NormalMemberImpl override val filesRoot: RemoteFile by lazy { RemoteFileImpl(this, "/") } - override val members: ContactList<NormalMember> = ContactList(members.mapNotNullTo(ConcurrentLinkedQueue()) { - if (it.uin == bot.id) { - botAsMember = newMember(it).cast() - if (it.permission == MemberPermission.OWNER) { - owner = botAsMember - } - null - } else newMember(it).cast<NormalMember>().also { member -> - if (member.permission == MemberPermission.OWNER) { - owner = member - } - } - }) override val announcements: Announcements by lazy { AnnouncementsImpl( @@ -128,10 +139,8 @@ internal class GroupImpl( return true } - override operator fun get(id: Long): NormalMember? { - if (id == bot.id) { - return botAsMember - } + override operator fun get(id: Long): NormalMemberImpl? { + if (id == bot.id) return botAsMember return members.firstOrNull { it.id == id } } @@ -288,12 +297,13 @@ internal class GroupImpl( override fun toString(): String = "Group($id)" } +@Deprecated("use addNewNormalMember or newAnonymousMember") internal fun Group.newMember(memberInfo: MemberInfo): Member { this.checkIsGroupImpl() - memberInfo.anonymousId?.let { anId -> + memberInfo.anonymousId?.let { return AnonymousMemberImpl( this, this.coroutineContext, - memberInfo, anId + memberInfo ) } return NormalMemberImpl( @@ -303,16 +313,35 @@ internal fun Group.newMember(memberInfo: MemberInfo): Member { ) } -internal fun GroupImpl.newAnonymous(name: String, id: String): AnonymousMemberImpl = newMember( - MemberInfoImpl( - uin = 80000000L, - nick = name, - permission = MemberPermission.MEMBER, - remark = "匿名", - nameCard = name, - specialTitle = "匿名", - muteTimestamp = 0, - anonymousId = id, +internal fun Group.addNewNormalMember(memberInfo: MemberInfo): NormalMemberImpl? { + if (members.contains(memberInfo.uin)) return null + return newNormalMember(memberInfo).also { + members.delegate.add(it) + } +} + +internal fun Group.newNormalMember(memberInfo: MemberInfo): NormalMemberImpl { + this.checkIsGroupImpl() + return NormalMemberImpl( + this, + this.coroutineContext, + memberInfo + ) +} + +internal fun GroupImpl.newAnonymous(name: String, id: String): AnonymousMemberImpl { + return AnonymousMemberImpl( + this, this.coroutineContext, + MemberInfoImpl( + uin = 80000000L, + nick = name, + permission = MemberPermission.MEMBER, + remark = "匿名", + nameCard = name, + specialTitle = "匿名", + muteTimestamp = 0, + anonymousId = id, + ) ) -) as AnonymousMemberImpl +} diff --git a/mirai-core/src/commonMain/kotlin/contact/NormalMemberImpl.kt b/mirai-core/src/commonMain/kotlin/contact/NormalMemberImpl.kt index bfab6397fd5..1eedb8e2590 100644 --- a/mirai-core/src/commonMain/kotlin/contact/NormalMemberImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/NormalMemberImpl.kt @@ -11,8 +11,6 @@ package net.mamoe.mirai.internal.contact -import kotlinx.atomicfu.AtomicInt -import kotlinx.atomicfu.atomic import kotlinx.coroutines.CancellationException import kotlinx.coroutines.cancel import kotlinx.coroutines.launch @@ -36,12 +34,9 @@ import kotlin.coroutines.CoroutineContext @Suppress("MemberVisibilityCanBePrivate") internal class NormalMemberImpl constructor( group: GroupImpl, - coroutineContext: CoroutineContext, + parentCoroutineContext: CoroutineContext, memberInfo: MemberInfo, -) : NormalMember, AbstractMember(group, coroutineContext, memberInfo) { - - @Suppress("unused") // false positive - val lastMessageSequence: AtomicInt = atomic(-1) +) : NormalMember, AbstractMember(group, parentCoroutineContext, memberInfo) { override val joinTimestamp: Int get() = info.joinTimestamp override val lastSpeakTimestamp: Int get() = info.lastSpeakTimestamp @@ -57,7 +52,7 @@ internal class NormalMemberImpl constructor( ?: handler.sendMessageImpl<NormalMember>( message = message, preSendEventConstructor = ::GroupTempMessagePreSendEvent, - postSendEventConstructor = ::GroupTempMessagePostSendEvent.cast() + postSendEventConstructor = ::GroupTempMessagePostSendEvent.cast(), ) } @@ -102,7 +97,7 @@ internal class NormalMemberImpl constructor( TroopManagement.EditGroupNametag( bot.client, this@NormalMemberImpl, - newValue + newValue, ).sendWithoutExpect() } MemberCardChangeEvent(oldValue, newValue, this@NormalMemberImpl).broadcast() @@ -122,7 +117,7 @@ internal class NormalMemberImpl constructor( TroopManagement.EditSpecialTitle( bot.client, this@NormalMemberImpl, - newValue + newValue, ).sendWithoutExpect() } MemberSpecialTitleChangeEvent(oldValue, newValue, this@NormalMemberImpl, null).broadcast() @@ -143,7 +138,7 @@ internal class NormalMemberImpl constructor( client = bot.client, groupCode = group.id, memberUin = this@NormalMemberImpl.id, - timeInSecond = durationSeconds + timeInSecond = durationSeconds, ).sendAndExpect<TroopManagement.Mute.Response>() } @@ -159,7 +154,7 @@ internal class NormalMemberImpl constructor( client = bot.client, groupCode = group.id, memberUin = this@NormalMemberImpl.id, - timeInSecond = 0 + timeInSecond = 0, ).sendAndExpect<TroopManagement.Mute.Response>() } @@ -210,7 +205,7 @@ internal class NormalMemberImpl constructor( val resp: TroopManagement.ModifyAdmin.Response = TroopManagement.ModifyAdmin( client = bot.client, member = this@NormalMemberImpl, - operation = operation + operation = operation, ).sendAndExpect() check(resp.success) { @@ -227,7 +222,7 @@ internal class NormalMemberImpl constructor( internal fun Member.checkBotPermissionHighest(operationName: String) { check(group.botPermission == MemberPermission.OWNER) { throw PermissionDeniedException( - "`$operationName` operation requires the OWNER permission, while bot has ${group.botPermission}" + "`$operationName` operation requires the OWNER permission, while bot has ${group.botPermission}", ) } } @@ -236,7 +231,7 @@ internal fun Member.checkBotPermissionHigherThanThis(operationName: String) { check(group.botPermission > this.permission) { throw PermissionDeniedException( "`$operationName` operation requires a higher permission, while " + - "${group.botPermission} < ${this.permission}" + "${group.botPermission} < ${this.permission}", ) } } diff --git a/mirai-core/src/commonMain/kotlin/contact/SendMessageHandler.kt b/mirai-core/src/commonMain/kotlin/contact/SendMessageHandler.kt index 322964990bd..22bc3171d64 100644 --- a/mirai-core/src/commonMain/kotlin/contact/SendMessageHandler.kt +++ b/mirai-core/src/commonMain/kotlin/contact/SendMessageHandler.kt @@ -14,15 +14,14 @@ import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import net.mamoe.mirai.contact.* import net.mamoe.mirai.event.nextEventOrNull -import net.mamoe.mirai.internal.MiraiImpl import net.mamoe.mirai.internal.asQQAndroidBot import net.mamoe.mirai.internal.getMiraiImpl import net.mamoe.mirai.internal.message.* -import net.mamoe.mirai.internal.message.LightMessageRefiner.refineLight import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.internal.network.QQAndroidClient import net.mamoe.mirai.internal.network.components.MessageSvcSyncer import net.mamoe.mirai.internal.network.handler.logger +import net.mamoe.mirai.internal.network.notice.group.GroupMessageProcessor.SendGroupMessageReceipt import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket import net.mamoe.mirai.internal.network.protocol.packet.chat.FileManagement @@ -391,9 +390,8 @@ internal open class GroupSendMessageHandler( fromAppId: Int, ): OnlineMessageSource.Outgoing { - val receipt: OnlinePushPbPushGroupMsg.SendGroupMessageReceipt = - nextEventOrNull(3000) { it.fromAppId == fromAppId } - ?: OnlinePushPbPushGroupMsg.SendGroupMessageReceipt.EMPTY + val receipt: SendGroupMessageReceipt = + nextEventOrNull(3000) { it.fromAppId == fromAppId } ?: SendGroupMessageReceipt.EMPTY return OnlineMessageSourceToGroupImpl( contact, @@ -442,7 +440,7 @@ internal open class GroupSendMessageHandler( groupCode = id, md5 = image.md5, size = if (image is OnlineFriendImageImpl) image.delegate.fileLen else 0 - ).sendAndExpect<ImgStore.GroupPicUp.Response>() + ).sendAndExpect() return OfflineGroupImage(image.imageId).also { img -> when (response) { is ImgStore.GroupPicUp.Response.FileExists -> { diff --git a/mirai-core/src/commonMain/kotlin/contact/StrangerImpl.kt b/mirai-core/src/commonMain/kotlin/contact/StrangerImpl.kt index 6600d8b5640..eff70f5751a 100644 --- a/mirai-core/src/commonMain/kotlin/contact/StrangerImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/StrangerImpl.kt @@ -17,8 +17,6 @@ package net.mamoe.mirai.internal.contact -import kotlinx.atomicfu.AtomicInt -import kotlinx.atomicfu.atomic import net.mamoe.mirai.LowLevelApi import net.mamoe.mirai.contact.Stranger import net.mamoe.mirai.contact.User @@ -39,34 +37,30 @@ import kotlin.coroutines.CoroutineContext @OptIn(ExperimentalContracts::class) -internal inline fun Stranger.checkIsImpl(): StrangerImpl { - contract { - returns() implies (this@checkIsImpl is StrangerImpl) - } +internal inline fun Stranger.impl(): StrangerImpl { + contract { returns() implies (this@impl is StrangerImpl) } check(this is StrangerImpl) { "A Stranger instance is not instance of StrangerImpl. Your instance: ${this::class.qualifiedName}" } return this } internal class StrangerImpl( bot: QQAndroidBot, - coroutineContext: CoroutineContext, - internal val strangerInfo: StrangerInfo, -) : Stranger, AbstractUser(bot, coroutineContext, strangerInfo) { - @Suppress("unused") // bug - val lastMessageSequence: AtomicInt = atomic(-1) + parentCoroutineContext: CoroutineContext, + override val info: StrangerInfo, +) : Stranger, AbstractUser(bot, parentCoroutineContext, info) { override suspend fun delete() { check(bot.strangers[this.id] != null) { "Stranger ${this.id} had already been deleted" } bot.network.run { StrangerList.DelStranger(bot.client, this@StrangerImpl) - .sendAndExpect<StrangerList.DelStranger.Response>().also { + .sendAndExpect().also { check(it.isSuccess) { "delete Stranger failed: ${it.result}" } } } } - private val handler by lazy { StrangerSendMessageHandler(this) } + private val handler: StrangerSendMessageHandler by lazy { StrangerSendMessageHandler(this) } @Suppress("DuplicatedCode") override suspend fun sendMessage(message: Message): MessageReceipt<Stranger> { diff --git a/mirai-core/src/commonMain/kotlin/contact/announcement/AnnouncementsImpl.kt b/mirai-core/src/commonMain/kotlin/contact/announcement/AnnouncementsImpl.kt index 323f9817a2a..2415705c53e 100644 --- a/mirai-core/src/commonMain/kotlin/contact/announcement/AnnouncementsImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/announcement/AnnouncementsImpl.kt @@ -169,7 +169,7 @@ internal object AnnouncementProtocol { val resp = Mirai.Http.post<String> { url("https://web.qun.qq.com/cgi-bin/announce/upload_img") body = MultiPartFormDataContent(formData { - append("\"bkn\"", bkn) + append("\"bkn\"", client.wLoginSigInfo.bkn) append("\"source\"", "troopNotice") append("m", "0") append( @@ -212,7 +212,7 @@ internal object AnnouncementProtocol { ) body = MultiPartFormDataContent(formData { append("qid", groupId) - this.append("bkn", bkn) + append("bkn", client.wLoginSigInfo.bkn) append("text", announcement.msg.text) append("pinned", announcement.pinned) image?.let { @@ -243,7 +243,7 @@ internal object AnnouncementProtocol { url("https://web.qun.qq.com/cgi-bin/announce/list_announce") body = MultiPartFormDataContent(formData { append("qid", groupId) - append("bkn", bkn) + append("bkn", client.wLoginSigInfo.bkn) append("ft", 23) //好像是一个用来识别应用的参数 append("s", if (page == 1) 0 else -(page * amount + 1)) // 第一页这里的参数应该是-1 append("n", amount) @@ -288,7 +288,7 @@ internal object AnnouncementProtocol { fid: String ) = MultiPartFormDataContent(formData { append("qid", groupId) - append("bkn", bkn) + append("bkn", client.wLoginSigInfo.bkn) append("fid", fid) append("format", "json") }) diff --git a/mirai-core/src/commonMain/kotlin/contact/info/FriendInfoImpl.kt b/mirai-core/src/commonMain/kotlin/contact/info/FriendInfoImpl.kt index c46c8b135e0..bb170279836 100644 --- a/mirai-core/src/commonMain/kotlin/contact/info/FriendInfoImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/info/FriendInfoImpl.kt @@ -18,4 +18,8 @@ internal data class FriendInfoImpl( override val uin: Long, override var nick: String, override var remark: String, -) : FriendInfo \ No newline at end of file +) : FriendInfo { + companion object { + fun FriendInfo.impl() = if (this is FriendInfoImpl) this else FriendInfoImpl(uin, nick, remark) + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/contact/info/MemberInfoImpl.kt b/mirai-core/src/commonMain/kotlin/contact/info/MemberInfoImpl.kt index 99162a9687f..b31f351eb9c 100644 --- a/mirai-core/src/commonMain/kotlin/contact/info/MemberInfoImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/info/MemberInfoImpl.kt @@ -21,11 +21,11 @@ internal data class MemberInfoImpl( override val uin: Long, override var nick: String, override var permission: MemberPermission, - override var remark: String, - override val nameCard: String, - override val specialTitle: String, - override val muteTimestamp: Int, - override val anonymousId: String?, + override var remark: String = "", + override val nameCard: String = "", + override val specialTitle: String = "", + override val muteTimestamp: Int = 0, + override val anonymousId: String? = null, override val joinTimestamp: Int = currentTimeSeconds().toInt(), override var lastSpeakTimestamp: Int = 0, override val isOfficialBot: Boolean = false, diff --git a/mirai-core/src/commonMain/kotlin/contact/info/StrangerInfoImpl.kt b/mirai-core/src/commonMain/kotlin/contact/info/StrangerInfoImpl.kt index 6d55d00e1e9..6ec1a460417 100644 --- a/mirai-core/src/commonMain/kotlin/contact/info/StrangerInfoImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/info/StrangerInfoImpl.kt @@ -20,4 +20,8 @@ internal class StrangerInfoImpl( override val nick: String, override val fromGroup: Long = 0, override val remark: String = "", -) : StrangerInfo \ No newline at end of file +) : StrangerInfo { + companion object { + fun StrangerInfo.impl() = if (this is StrangerInfoImpl) this else StrangerInfoImpl(uin, nick, fromGroup, remark) + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/contextualBugReportException.kt b/mirai-core/src/commonMain/kotlin/message/contextualBugReportException.kt index 910a87586e4..596ce0c5bbd 100644 --- a/mirai-core/src/commonMain/kotlin/message/contextualBugReportException.kt +++ b/mirai-core/src/commonMain/kotlin/message/contextualBugReportException.kt @@ -9,26 +9,26 @@ package net.mamoe.mirai.internal.message -import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract +internal data class ContextualBugReportException( + override val message: String, + override val cause: Throwable?, +) : IllegalStateException() internal fun contextualBugReportException( context: String, forDebug: String, e: Throwable? = null, additional: String = "", -): IllegalStateException { - return IllegalStateException( +): ContextualBugReportException { + return ContextualBugReportException( "在 $context 时遇到了意料之中的问题. 请完整复制此日志提交给 mirai: https://github.com/mamoe/mirai/issues/new $additional 调试信息: $forDebug", - e + e, ) } -@OptIn(ExperimentalContracts::class) -@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE") -@kotlin.internal.InlineOnly internal inline fun <R> runWithBugReport(context: String, forDebug: () -> String, block: () -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) diff --git a/mirai-core/src/commonMain/kotlin/message/incomingSourceImpl.kt b/mirai-core/src/commonMain/kotlin/message/incomingSourceImpl.kt index 9d452affc6d..9b557a8f631 100644 --- a/mirai-core/src/commonMain/kotlin/message/incomingSourceImpl.kt +++ b/mirai-core/src/commonMain/kotlin/message/incomingSourceImpl.kt @@ -17,8 +17,10 @@ import net.mamoe.mirai.Bot import net.mamoe.mirai.contact.Friend import net.mamoe.mirai.contact.Member import net.mamoe.mirai.contact.Stranger +import net.mamoe.mirai.internal.asQQAndroidBot import net.mamoe.mirai.internal.contact.checkIsGroupImpl import net.mamoe.mirai.internal.contact.newAnonymous +import net.mamoe.mirai.internal.getGroupByUinOrCodeOrFail import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm import net.mamoe.mirai.internal.network.protocol.data.proto.SourceMsg @@ -136,7 +138,12 @@ internal class OnlineMessageSourceFromTempImpl( msg.toMessageChainNoSource(bot, groupIdOrZero = 0, MessageSourceKind.TEMP) } override val sender: Member = with(msg.first().msgHead) { - bot.getGroupOrFail(c2cTmpMsgHead!!.groupUin).getOrFail(fromUin) + // it must be uin, see #1410 + // corresponding test: net.mamoe.mirai.internal.notice.processors.MessageTest.group temp message test 2 + + // search for group code also is for tests. code may be passed as uin in tests. + // clashing is unlikely possible in real time, so it would not be a problem. + bot.asQQAndroidBot().getGroupByUinOrCodeOrFail(c2cTmpMsgHead!!.groupUin).getOrFail(fromUin) } private val jceData: ImMsgBody.SourceMsg by lazy { msg.toJceDataPrivate(internalIds) } diff --git a/mirai-core/src/commonMain/kotlin/message/outgoingSourceImpl.kt b/mirai-core/src/commonMain/kotlin/message/outgoingSourceImpl.kt index d08a76ced93..97048edb7f8 100644 --- a/mirai-core/src/commonMain/kotlin/message/outgoingSourceImpl.kt +++ b/mirai-core/src/commonMain/kotlin/message/outgoingSourceImpl.kt @@ -20,10 +20,10 @@ import net.mamoe.mirai.Bot import net.mamoe.mirai.Mirai import net.mamoe.mirai.contact.* import net.mamoe.mirai.event.asyncFromEventOrNull +import net.mamoe.mirai.internal.network.notice.group.GroupMessageProcessor.SendGroupMessageReceipt import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm import net.mamoe.mirai.internal.network.protocol.data.proto.SourceMsg -import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.OnlinePushPbPushGroupMsg.SendGroupMessageReceipt import net.mamoe.mirai.internal.utils.io.serialization.toByteArray import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageSource diff --git a/mirai-core/src/commonMain/kotlin/network/Packet.kt b/mirai-core/src/commonMain/kotlin/network/Packet.kt index ce6bce38497..fa605f69884 100644 --- a/mirai-core/src/commonMain/kotlin/network/Packet.kt +++ b/mirai-core/src/commonMain/kotlin/network/Packet.kt @@ -11,6 +11,7 @@ package net.mamoe.mirai.internal.network import net.mamoe.mirai.internal.AbstractBot import net.mamoe.mirai.internal.network.handler.logger +import net.mamoe.mirai.internal.utils.io.ProtocolStruct import net.mamoe.mirai.utils.MiraiLogger /* @@ -32,21 +33,59 @@ interface Packet { /** * PacketFactory 可以一次解析多个包出来. 它们将会被分别广播. */ -internal interface MultiPacket<out P : Packet> : Packet, Iterable<P> +internal interface MultiPacket : Packet { + /** + * `true` if this packet has some useful meaning, otherwise it will be considered just as a wrapper of its children. + */ + val isMeaningful: Boolean + + /** + * if item is [MultiPacket], its children will be ignored. + */ + fun children(): Iterator<Packet> +} + +internal fun Collection<Packet>.toPacket(): Packet { + return when (this.size) { + 1 -> this.single() + else -> MultiPacket(this) + } +} -internal open class MultiPacketByIterable<out P : Packet>(internal val delegate: Iterable<P>) : MultiPacket<P>, - Iterable<P> by delegate { - override fun toString(): String = "MultiPacketByIterable" +internal fun MultiPacket(delegate: Collection<Packet>): MultiPacket { + return MultiPacketImpl(delegate) } -internal open class MultiPacketBySequence<out P : Packet>(internal val delegate: Sequence<P>) : - MultiPacket<P> { - override operator fun iterator(): Iterator<P> = delegate.iterator() +internal fun MultiPacket(delegate: Packet): MultiPacket = + if (delegate is MultiPacket) delegate else MultiPacket(listOf(delegate)) + + +private class MultiPacketImpl( + val delegate: Collection<Packet>, +) : MultiPacket { + override val isMeaningful: Boolean get() = false - override fun toString(): String = "MultiPacketBySequence" + override fun children(): Iterator<Packet> { + return sequence { + for (packet in delegate) { + yield(packet) + if (packet is MultiPacket) { + yieldAll(packet.children()) + } + } + }.iterator() + } + + override fun toString(): String = delegate.joinToString( + separator = "\n", + prefix = "MultiPacket [\n", + postfix = "]", + ) } + internal class ParseErrorPacket( + val data: ProtocolStruct, val error: Throwable, val direction: Direction = Direction.TO_BOT_LOGGER, ) : Packet, Packet.NoLog { diff --git a/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt b/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt index 574eea322ed..47c3a7b5241 100644 --- a/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt +++ b/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt @@ -11,7 +11,6 @@ package net.mamoe.mirai.internal.network -import kotlinx.atomicfu.AtomicBoolean import kotlinx.atomicfu.AtomicInt import kotlinx.atomicfu.atomic import kotlinx.io.core.String @@ -21,9 +20,9 @@ import net.mamoe.mirai.internal.BotAccount import net.mamoe.mirai.internal.QQAndroidBot import net.mamoe.mirai.internal.network.components.AccountSecrets import net.mamoe.mirai.internal.network.components.SsoSession -import net.mamoe.mirai.internal.network.protocol.SyncingCacheList import net.mamoe.mirai.internal.network.protocol.data.jce.FileStoragePushFSSvcList import net.mamoe.mirai.internal.network.protocol.packet.Tlv +import net.mamoe.mirai.internal.utils.AtomicIntSeq import net.mamoe.mirai.internal.utils.MiraiProtocolInternal import net.mamoe.mirai.internal.utils.NetworkType import net.mamoe.mirai.utils.* @@ -124,13 +123,10 @@ internal open class QQAndroidClient( internal var strangerSeq: Int = 0 - // TODO: 2021/4/14 investigate whether they can be minimized - private val friendSeq: AtomicInt = atomic(getRandomUnsignedInt()) - internal fun getFriendSeq(): Int = friendSeq.value - - internal fun nextFriendSeq(): Int = friendSeq.incrementAndGet() - - internal fun setFriendSeq(compare: Int, id: Int): Boolean = friendSeq.compareAndSet(compare, id % 65535) + /** + * for send + */ + val sendFriendMessageSeq = AtomicIntSeq.forPrivateSync() internal val groupConfig: GroupConfig = GroupConfig() @@ -144,60 +140,6 @@ internal open class QQAndroidClient( } } - class MessageSvcSyncData { - val firstNotify: AtomicBoolean = atomic(true) - var latestMsgNewGroupTime: Long = currentTimeSeconds() - var latestMsgNewFriendTime: Long = currentTimeSeconds() - - @Volatile - var syncCookie: ByteArray? = null - var pubAccountCookie = EMPTY_BYTE_ARRAY - var msgCtrlBuf: ByteArray = EMPTY_BYTE_ARRAY - - - internal data class PbGetMessageSyncId( - val uid: Long, - val sequence: Int, - val time: Int - ) - - val pbGetMessageCacheList = SyncingCacheList<PbGetMessageSyncId>() - - internal data class SystemMsgNewSyncId( - val sequence: Long, - val time: Long - ) - - val systemMsgNewGroupCacheList = SyncingCacheList<SystemMsgNewSyncId>(10) - val systemMsgNewFriendCacheList = SyncingCacheList<SystemMsgNewSyncId>(10) - - - internal data class PbPushTransMsgSyncId( - val uid: Long, - val sequence: Int, - val time: Int - ) - - val pbPushTransMsgCacheList = SyncingCacheList<PbPushTransMsgSyncId>(10) - - internal data class OnlinePushReqPushSyncId( - val uid: Long, - val sequence: Short, - val time: Long - ) - - val onlinePushReqPushCacheList = SyncingCacheList<OnlinePushReqPushSyncId>(50) - - internal data class PendingGroupMessageReceiptSyncId( - val messageRandom: Int, - ) - - val pendingGroupMessageReceiptCacheList = SyncingCacheList<PendingGroupMessageReceiptSyncId>(50) - } - - - val syncingController = MessageSvcSyncData() - var t150: Tlv? = null var rollbackSig: ByteArray? = null var ipFromT149: ByteArray? = null diff --git a/mirai-core/src/commonMain/kotlin/network/components/ContactUpdater.kt b/mirai-core/src/commonMain/kotlin/network/components/ContactUpdater.kt index 093d90f2959..0efb02ace59 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/ContactUpdater.kt +++ b/mirai-core/src/commonMain/kotlin/network/components/ContactUpdater.kt @@ -19,10 +19,8 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withPermit import net.mamoe.mirai.Mirai import net.mamoe.mirai.contact.Contact -import net.mamoe.mirai.data.FriendInfo import net.mamoe.mirai.data.MemberInfo import net.mamoe.mirai.internal.QQAndroidBot -import net.mamoe.mirai.internal.contact.FriendImpl import net.mamoe.mirai.internal.contact.GroupImpl import net.mamoe.mirai.internal.contact.StrangerImpl import net.mamoe.mirai.internal.contact.info.FriendInfoImpl @@ -34,6 +32,7 @@ import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.internal.network.component.ComponentKey import net.mamoe.mirai.internal.network.component.ComponentStorage import net.mamoe.mirai.internal.network.isValid +import net.mamoe.mirai.internal.network.notice.NewContactSupport import net.mamoe.mirai.internal.network.protocol.data.jce.StTroopNum import net.mamoe.mirai.internal.network.protocol.data.jce.SvcRespRegister import net.mamoe.mirai.internal.network.protocol.data.jce.isValid @@ -76,7 +75,7 @@ internal class ContactUpdaterImpl( val bot: QQAndroidBot, // not good val components: ComponentStorage, private val logger: MiraiLogger, -) : ContactUpdater { +) : ContactUpdater, NewContactSupport { override val otherClientsLock: Mutex = Mutex() override val groupListModifyLock: Mutex = Mutex() private val cacheService get() = components[ContactCacheService] @@ -176,16 +175,13 @@ internal class ContactUpdaterImpl( } for (friendInfoImpl in list) { - addFriendToBot(friendInfoImpl) + bot.addNewFriendAndRemoveStranger(friendInfoImpl) } initFriendOk = true } - private fun addFriendToBot(it: FriendInfo) = - bot.friends.delegate.add(FriendImpl(bot, bot.coroutineContext, it)) - private suspend fun addGroupToBot(stTroopNum: StTroopNum) = stTroopNum.run { suspend fun refreshGroupMemberList(): Sequence<MemberInfo> { return Mirai.getRawGroupMemberList( @@ -214,11 +210,11 @@ internal class ContactUpdaterImpl( bot.groups.delegate.add( GroupImpl( bot = bot, - coroutineContext = bot.coroutineContext, + parentCoroutineContext = bot.coroutineContext, id = groupCode, groupInfo = GroupInfoImpl(stTroopNum), - members = members - ) + members = members, + ), ) } diff --git a/mirai-core/src/commonMain/kotlin/network/components/NoticeProcessorPipeline.kt b/mirai-core/src/commonMain/kotlin/network/components/NoticeProcessorPipeline.kt new file mode 100644 index 00000000000..5da5ab64cfb --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/components/NoticeProcessorPipeline.kt @@ -0,0 +1,338 @@ +/* + * 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.network.components + +import net.mamoe.mirai.internal.QQAndroidBot +import net.mamoe.mirai.internal.message.contextualBugReportException +import net.mamoe.mirai.internal.network.Packet +import net.mamoe.mirai.internal.network.ParseErrorPacket +import net.mamoe.mirai.internal.network.component.ComponentKey +import net.mamoe.mirai.internal.network.component.ComponentStorage +import net.mamoe.mirai.internal.network.notice.BotAware +import net.mamoe.mirai.internal.network.notice.NewContactSupport +import net.mamoe.mirai.internal.network.notice.decoders.DecodedNotifyMsgBody +import net.mamoe.mirai.internal.network.notice.decoders.MsgType0x2DC +import net.mamoe.mirai.internal.network.protocol.data.jce.MsgInfo +import net.mamoe.mirai.internal.network.protocol.data.jce.MsgType0x210 +import net.mamoe.mirai.internal.network.protocol.data.jce.RequestPushStatus +import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm +import net.mamoe.mirai.internal.network.protocol.data.proto.MsgOnlinePush +import net.mamoe.mirai.internal.network.protocol.data.proto.OnlinePushTrans.PbMsgInfo +import net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg +import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbGetMsg +import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.OnlinePushPbPushTransMsg +import net.mamoe.mirai.internal.network.toPacket +import net.mamoe.mirai.internal.utils.io.ProtocolStruct +import net.mamoe.mirai.utils.* +import java.io.Closeable +import java.util.* +import java.util.concurrent.ConcurrentLinkedQueue +import kotlin.reflect.KClass + +internal typealias ProcessResult = Collection<Packet> + +/** + * Centralized processor pipeline for [MessageSvcPbGetMsg] and [OnlinePushPbPushTransMsg] + */ +internal interface NoticeProcessorPipeline { + val processors: Collection<NoticeProcessor> + + fun interface DisposableRegistry : Closeable { + fun dispose() + + override fun close() { + dispose() + } + } + + fun registerProcessor(processor: NoticeProcessor): DisposableRegistry + + /** + * Process [data] into [Packet]s. Exceptions are wrapped into [ParseErrorPacket] + */ + suspend fun process( + bot: QQAndroidBot, + data: ProtocolStruct, + attributes: TypeSafeMap = TypeSafeMap.EMPTY + ): ProcessResult + + companion object : ComponentKey<NoticeProcessorPipeline> { + val ComponentStorage.noticeProcessorPipeline get() = get(NoticeProcessorPipeline) + + @JvmStatic + suspend inline fun QQAndroidBot.processPacketThroughPipeline( + data: ProtocolStruct, + attributes: TypeSafeMap = TypeSafeMap.EMPTY, + ): Packet { + return components.noticeProcessorPipeline.process(this, data, attributes).toPacket() + } + } +} + +@JvmInline +internal value class MutableProcessResult( + val data: MutableCollection<Packet> +) + +internal interface NoticePipelineContext : BotAware, NewContactSupport { + override val bot: QQAndroidBot + + val attributes: TypeSafeMap + + + val isConsumed: Boolean + + /** + * Marks the input as consumed so that there will not be warnings like 'Unknown type xxx'. This will not stop the pipeline. + * + * If this is executed, make sure you provided all information important for debugging. + * + * You need to invoke [markAsConsumed] if your implementation includes some `else` branch which covers all situations, + * and throws a [contextualBugReportException] or logs something. + */ + @ConsumptionMarker + fun NoticeProcessor.markAsConsumed(marker: Any = this) + + /** + * Marks the input as not consumed, if it was marked by this [NoticeProcessor]. + */ + @ConsumptionMarker + fun NoticeProcessor.markNotConsumed(marker: Any = this) + + @DslMarker + annotation class ConsumptionMarker // to give an explicit color. + + + val collected: MutableProcessResult + + // DSL to simplify some expressions + operator fun MutableProcessResult.plusAssign(packet: Packet?) { + if (packet != null) collect(packet) + } + + + /** + * Collect a result. + */ + fun collect(packet: Packet) + + /** + * Collect results. + */ + fun collect(packets: Iterable<Packet>) + + /** + * Fire the [data] into the processor pipeline, and collect the results to current [collected]. + * + * @param attributes extra attributes + * @return result collected from processors. This would also have been collected to this context (where you call [processAlso]). + */ + suspend fun processAlso(data: ProtocolStruct, attributes: TypeSafeMap = TypeSafeMap.EMPTY): ProcessResult + + companion object { + val KEY_FROM_SYNC = TypeKey<Boolean>("fromSync") + val KEY_MSG_INFO = TypeKey<MsgInfo>("msgInfo") + + val NoticePipelineContext.fromSync get() = attributes[KEY_FROM_SYNC] + + /** + * 来自 [MsgInfo] 的数据, 即 [MsgType0x210], [MsgType0x2DC] 的处理过程之中可以使用 + */ + val NoticePipelineContext.msgInfo get() = attributes[KEY_MSG_INFO] + } +} + +internal abstract class AbstractNoticePipelineContext( + override val bot: QQAndroidBot, override val attributes: TypeSafeMap, +) : NoticePipelineContext { + private val consumers: Stack<Any> = Stack() + + override val isConsumed: Boolean get() = consumers.isNotEmpty() + override fun NoticeProcessor.markAsConsumed(marker: Any) { + traceLogging.info { "markAsConsumed: marker=$marker" } + consumers.push(marker) + } + + override fun NoticeProcessor.markNotConsumed(marker: Any) { + if (consumers.peek() === marker) { + consumers.pop() + traceLogging.info { "markNotConsumed: Y, marker=$marker" } + } else { + traceLogging.info { "markNotConsumed: N, marker=$marker" } + } + } + + override val collected = MutableProcessResult(ConcurrentLinkedQueue()) + + override fun collect(packet: Packet) { + collected.data.add(packet) + traceLogging.info { "collect: $packet" } + } + + override fun collect(packets: Iterable<Packet>) { + this.collected.data.addAll(packets) + traceLogging.info { + val list = packets.toList() + "collect: [${list.size}] ${list.joinToString()}" + } + } + + abstract override suspend fun processAlso(data: ProtocolStruct, attributes: TypeSafeMap): ProcessResult +} + + +internal inline val NoticePipelineContext.context get() = this + +private val traceLogging: MiraiLogger by lazy { + MiraiLogger.Factory.create(NoticeProcessorPipelineImpl::class, "NoticeProcessorPipeline") + .withSwitch(systemProp("mirai.network.notice.pipeline.log.full", false)) +} + +internal open class NoticeProcessorPipelineImpl protected constructor() : NoticeProcessorPipeline { + /** + * Must be ordered + */ + override val processors = ConcurrentLinkedQueue<NoticeProcessor>() + + override fun registerProcessor(processor: NoticeProcessor): NoticeProcessorPipeline.DisposableRegistry { + processors.add(processor) + return NoticeProcessorPipeline.DisposableRegistry { + processors.remove(processor) + } + } + + + open inner class ContextImpl( + bot: QQAndroidBot, attributes: TypeSafeMap, + ) : AbstractNoticePipelineContext(bot, attributes) { + override suspend fun processAlso(data: ProtocolStruct, attributes: TypeSafeMap): ProcessResult { + traceLogging.info { "processAlso: data=$data" } + return process(bot, data, this.attributes + attributes).also { + this.collected.data += it + traceLogging.info { "processAlso: result=$it" } + } + } + } + + + override suspend fun process(bot: QQAndroidBot, data: ProtocolStruct, attributes: TypeSafeMap): ProcessResult { + traceLogging.info { "process: data=$data" } + val context = createContext(bot, attributes) + + val diff = if (traceLogging.isEnabled) CollectionDiff<Packet>() else null + diff?.save(context.collected.data) + + for (processor in processors) { + + val result = kotlin.runCatching { + processor.process(context, data) + }.onFailure { e -> + context.collect( + ParseErrorPacket( + data, + IllegalStateException( + "Exception in $processor while processing packet ${packetToString(data)}.", + e, + ), + ), + ) + } + + diff?.run { + val diffPackets = subtractAndSave(context.collected.data) + + traceLogging.info { + "Finished ${ + processor.toString().replace("net.mamoe.mirai.internal.network.notice.", "") + }, success=${result.isSuccess}, consumed=${context.isConsumed}, diff=$diffPackets" + } + } + } + return context.collected.data + } + + protected open fun createContext( + bot: QQAndroidBot, + attributes: TypeSafeMap + ): NoticePipelineContext = ContextImpl(bot, attributes) + + protected open fun packetToString(data: Any?): String = + data.toDebugString("mirai.network.notice.pipeline.log.full") + + + companion object { + fun create(vararg processors: NoticeProcessor): NoticeProcessorPipelineImpl = + NoticeProcessorPipelineImpl().apply { + for (processor in processors) { + registerProcessor(processor) + } + } + } +} + +/////////////////////////////////////////////////////////////////////////// +// NoticeProcessor +/////////////////////////////////////////////////////////////////////////// + +/** + * A processor handling some specific type of message. + */ +internal interface NoticeProcessor { + suspend fun process(context: NoticePipelineContext, data: Any?) +} + +internal abstract class AnyNoticeProcessor : SimpleNoticeProcessor<ProtocolStruct>(type()) + +internal abstract class SimpleNoticeProcessor<in T : ProtocolStruct>( + private val type: KClass<T>, +) : NoticeProcessor { + + final override suspend fun process(context: NoticePipelineContext, data: Any?) { + if (type.isInstance(data)) { + context.processImpl(data.uncheckedCast()) + } + } + + protected abstract suspend fun NoticePipelineContext.processImpl(data: T) + + companion object { + @JvmStatic + protected inline fun <reified T : Any> type(): KClass<T> = T::class + } +} + +internal abstract class MsgCommonMsgProcessor : SimpleNoticeProcessor<MsgComm.Msg>(type()) { + abstract override suspend fun NoticePipelineContext.processImpl(data: MsgComm.Msg) +} + +internal abstract class MixedNoticeProcessor : AnyNoticeProcessor() { + final override suspend fun NoticePipelineContext.processImpl(data: ProtocolStruct) { + when (data) { + is PbMsgInfo -> processImpl(data) + is MsgOnlinePush.PbPushMsg -> processImpl(data) + is MsgComm.Msg -> processImpl(data) + is MsgType0x210 -> processImpl(data) + is MsgType0x2DC -> processImpl(data) + is Structmsg.StructMsg -> processImpl(data) + is RequestPushStatus -> processImpl(data) + is DecodedNotifyMsgBody -> processImpl(data) + } + } + + protected open suspend fun NoticePipelineContext.processImpl(data: MsgType0x210) {} // 528 + protected open suspend fun NoticePipelineContext.processImpl(data: MsgType0x2DC) {} // 732 + protected open suspend fun NoticePipelineContext.processImpl(data: PbMsgInfo) {} + protected open suspend fun NoticePipelineContext.processImpl(data: MsgOnlinePush.PbPushMsg) {} + protected open suspend fun NoticePipelineContext.processImpl(data: MsgComm.Msg) {} + protected open suspend fun NoticePipelineContext.processImpl(data: Structmsg.StructMsg) {} + protected open suspend fun NoticePipelineContext.processImpl(data: RequestPushStatus) {} + + protected open suspend fun NoticePipelineContext.processImpl(data: DecodedNotifyMsgBody) {} +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/components/PacketHandler.kt b/mirai-core/src/commonMain/kotlin/network/components/PacketHandler.kt index bd6b7a1a62e..e87dd6fa446 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/PacketHandler.kt +++ b/mirai-core/src/commonMain/kotlin/network/components/PacketHandler.kt @@ -75,8 +75,8 @@ internal class EventBroadcasterPacketHandler( private fun impl(packet: Packet?) { if (packet == null) return - if (packet is MultiPacket<*>) { - for (p in packet) { + if (packet is MultiPacket) { + for (p in packet.children()) { impl(p) } } diff --git a/mirai-core/src/commonMain/kotlin/network/components/PacketLoggingStrategy.kt b/mirai-core/src/commonMain/kotlin/network/components/PacketLoggingStrategy.kt index 5444949db8d..0bfc637ad93 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/PacketLoggingStrategy.kt +++ b/mirai-core/src/commonMain/kotlin/network/components/PacketLoggingStrategy.kt @@ -53,23 +53,24 @@ internal class PacketLoggingStrategyImpl( onRight = { packet -> packet ?: return if (!bot.logger.isEnabled && !logger.isEnabled) return - if (packet is ParseErrorPacket) { - packet.direction.getLogger(bot).error("Exception in parsing packet.", packet.error) - } - if (packet is MultiPacket<*>) { - for (d in packet) { + if (packet is MultiPacket) { + if (packet.isMeaningful) logReceivedImpl(packet, incomingPacket, logger) + for (d in packet.children()) { logReceivedImpl(d, incomingPacket, logger) } } logReceivedImpl(packet, incomingPacket, logger) - } + }, ) } private fun logReceivedImpl(packet: Packet, incomingPacket: IncomingPacket, logger: MiraiLogger) { when (packet) { + is ParseErrorPacket -> { + packet.direction.getLogger(bot).error("Exception on parsing packet.", packet.error) + } is MessageEvent -> packet.logMessageReceived() is Packet.NoLog -> { // nothing to do diff --git a/mirai-core/src/commonMain/kotlin/network/components/SyncController.kt b/mirai-core/src/commonMain/kotlin/network/components/SyncController.kt new file mode 100644 index 00000000000..25581d2a5c3 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/components/SyncController.kt @@ -0,0 +1,143 @@ +/* + * 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.network.components + +import kotlinx.atomicfu.AtomicBoolean +import kotlinx.atomicfu.atomic +import net.mamoe.mirai.internal.AbstractBot +import net.mamoe.mirai.internal.network.QQAndroidClient +import net.mamoe.mirai.internal.network.component.ComponentKey +import net.mamoe.mirai.internal.network.protocol.SyncingCacheList +import net.mamoe.mirai.internal.network.protocol.data.jce.MsgInfo +import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm +import net.mamoe.mirai.internal.network.protocol.data.proto.OnlinePushTrans +import net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY +import net.mamoe.mirai.utils.currentTimeSeconds + +internal interface SyncController { + val firstNotify: AtomicBoolean + var latestMsgNewGroupTime: Long + var latestMsgNewFriendTime: Long + + var syncCookie: ByteArray? + var pubAccountCookie: ByteArray + var msgCtrlBuf: ByteArray + + fun syncOnlinePush(uid: Long, sequence: Short, time: Long): Boolean + fun syncNewFriend(sequence: Long, time: Long): Boolean + fun syncNewGroup(sequence: Long, time: Long): Boolean + fun syncGetMessage(uid: Long, sequence: Int, time: Int): Boolean + fun syncPushTrans(uid: Long, sequence: Int, time: Int): Boolean + + fun syncGroupMessageReceipt(messageRandom: Int): Boolean + fun containsGroupMessageReceipt(messageRandom: Int): Boolean + + companion object : ComponentKey<SyncController> { + val AbstractBot.syncController get() = this.components[SyncController] + val QQAndroidClient.syncController get() = bot.syncController + var QQAndroidClient.syncCookie + get() = bot.syncController.syncCookie + set(value) { + bot.syncController.syncCookie = value + } + } +} + +internal fun SyncController.syncPushTrans(content: OnlinePushTrans.PbMsgInfo): Boolean = + syncPushTrans(content.msgUid, content.msgSeq, content.msgTime) + +internal fun SyncController.syncGetMessage( + msgHead: MsgComm.MsgHead, +) = msgHead.run { + syncGetMessage(msgUid, msgSeq, msgTime) +} + +internal fun SyncController.syncOnlinePush( + msgInfo: MsgInfo, +) = syncOnlinePush( + uid = msgInfo.lMsgUid ?: 0, + sequence = msgInfo.shMsgSeq, + time = msgInfo.uMsgTime, +) + +internal class SyncControllerImpl : SyncController { + override val firstNotify: AtomicBoolean = atomic(true) + + @Volatile + override var latestMsgNewGroupTime: Long = currentTimeSeconds() + + @Volatile + override var latestMsgNewFriendTime: Long = currentTimeSeconds() + + @Volatile + override var syncCookie: ByteArray? = null + + @Volatile + override var pubAccountCookie = EMPTY_BYTE_ARRAY + + @Volatile + override var msgCtrlBuf: ByteArray = EMPTY_BYTE_ARRAY + + private val pbGetMessageCacheList = SyncingCacheList<PbGetMessageSyncId>() + private val systemMsgNewGroupCacheList = SyncingCacheList<SystemMsgNewSyncId>(10) + private val systemMsgNewFriendCacheList = SyncingCacheList<SystemMsgNewSyncId>(10) + private val pbPushTransMsgCacheList = SyncingCacheList<PbPushTransMsgSyncId>(10) + private val onlinePushReqPushCacheList = SyncingCacheList<OnlinePushReqPushSyncId>(50) + private val pendingGroupMessageReceiptCacheList = SyncingCacheList<PendingGroupMessageReceiptSyncId>(50) + + override fun syncOnlinePush(uid: Long, sequence: Short, time: Long): Boolean = + onlinePushReqPushCacheList.addCache(OnlinePushReqPushSyncId(uid, sequence, time)) + + override fun syncNewFriend(sequence: Long, time: Long): Boolean = + systemMsgNewFriendCacheList.addCache(SystemMsgNewSyncId(sequence, time)) + + override fun syncNewGroup(sequence: Long, time: Long): Boolean = + systemMsgNewGroupCacheList.addCache(SystemMsgNewSyncId(sequence, time)) + + override fun syncGetMessage(uid: Long, sequence: Int, time: Int): Boolean = + pbGetMessageCacheList.addCache(PbGetMessageSyncId(uid, sequence, time)) + + override fun syncPushTrans(uid: Long, sequence: Int, time: Int): Boolean = + pbPushTransMsgCacheList.addCache(PbPushTransMsgSyncId(uid, sequence, time)) + + override fun syncGroupMessageReceipt(messageRandom: Int): Boolean = + pendingGroupMessageReceiptCacheList.addCache(PendingGroupMessageReceiptSyncId(messageRandom)) + + override fun containsGroupMessageReceipt(messageRandom: Int): Boolean = + pendingGroupMessageReceiptCacheList.contains { it.messageRandom == messageRandom } + + data class PbGetMessageSyncId( + val uid: Long, + val sequence: Int, + val time: Int, + ) + + data class SystemMsgNewSyncId( + val sequence: Long, + val time: Long, + ) + + data class PbPushTransMsgSyncId( + val uid: Long, + val sequence: Int, + val time: Int, + ) + + data class OnlinePushReqPushSyncId( + val uid: Long, + val sequence: Short, + val time: Long, + ) + + data class PendingGroupMessageReceiptSyncId( + val messageRandom: Int, + ) + +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/keys.kt b/mirai-core/src/commonMain/kotlin/network/keys.kt index 8ca5a780821..24acd7f7d8d 100644 --- a/mirai-core/src/commonMain/kotlin/network/keys.kt +++ b/mirai-core/src/commonMain/kotlin/network/keys.kt @@ -127,6 +127,14 @@ internal data class WLoginSigInfo( var encryptedDownloadSession: EncryptedDownloadSession? = null, ) { + /** + * 获取 获取群公告 所需的 bkn 参数 + * */ + val bkn: Int + get() = sKey.data + .fold(5381) { acc: Int, b: Byte -> acc + acc.shl(5) + b.toInt() } + .and(Int.MAX_VALUE) + //图片加密下载 //是否加密从bigdatachannel处得知 @Serializable diff --git a/mirai-core/src/commonMain/kotlin/network/notice/NewContactSupport.kt b/mirai-core/src/commonMain/kotlin/network/notice/NewContactSupport.kt new file mode 100644 index 00000000000..beb8f66bd40 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/notice/NewContactSupport.kt @@ -0,0 +1,113 @@ +/* + * 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.network.notice + +import kotlinx.coroutines.cancel +import net.mamoe.mirai.Mirai +import net.mamoe.mirai.contact.MemberPermission +import net.mamoe.mirai.internal.QQAndroidBot +import net.mamoe.mirai.internal.contact.FriendImpl +import net.mamoe.mirai.internal.contact.GroupImpl +import net.mamoe.mirai.internal.contact.StrangerImpl +import net.mamoe.mirai.internal.contact.impl +import net.mamoe.mirai.internal.contact.info.FriendInfoImpl +import net.mamoe.mirai.internal.contact.info.GroupInfoImpl +import net.mamoe.mirai.internal.contact.info.MemberInfoImpl +import net.mamoe.mirai.internal.contact.info.StrangerInfoImpl +import net.mamoe.mirai.internal.getGroupByUin +import net.mamoe.mirai.internal.network.protocol.data.jce.StTroopNum +import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm +import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList +import net.mamoe.mirai.internal.network.protocol.packet.sendAndExpect + +internal interface NewContactSupport { // can be a marker interface when context receivers are available. + + fun MsgComm.Msg.getNewMemberInfo(): MemberInfoImpl { + return MemberInfoImpl( + nameCard = msgHead.authNick.ifEmpty { msgHead.fromNick }, + permission = MemberPermission.MEMBER, + specialTitle = "", + muteTimestamp = 0, + uin = msgHead.authUin, + nick = msgHead.authNick.ifEmpty { msgHead.fromNick }, + remark = "", + anonymousId = null, + ) + } + + suspend fun QQAndroidBot.addNewGroupByCode(code: Long): GroupImpl? { + if (getGroup(code) != null) return null + return getNewGroup(code)?.apply { groups.delegate.add(this) } + } + + suspend fun QQAndroidBot.addNewGroupByUin(groupUin: Long): GroupImpl? { + if (getGroupByUin(groupUin) != null) return null + return getNewGroup(Mirai.calculateGroupCodeByGroupUin(groupUin))?.apply { groups.delegate.add(this) } + } + + suspend fun QQAndroidBot.addNewGroup(stTroopNum: StTroopNum): GroupImpl? { + if (getGroup(stTroopNum.groupCode) != null) return null + return getNewGroup(stTroopNum)?.apply { groups.delegate.add(this) } + } + + fun QQAndroidBot.removeStranger(id: Long): StrangerImpl? { + val instance = strangers[id] ?: return null + strangers.remove(instance.id) + instance.cancel() + return instance + } + + fun QQAndroidBot.removeFriend(id: Long): FriendImpl? { + val instance = friends[id] ?: return null + friends.remove(instance.id) + instance.cancel() + return instance + } + + fun QQAndroidBot.addNewFriendAndRemoveStranger(info: FriendInfoImpl): FriendImpl? { + if (friends.contains(info.uin)) return null + strangers[info.uin]?.let { removeStranger(it.id) } + val friend = Mirai.newFriend(bot, info).impl() + friends.delegate.add(friend) + return friend + } + + fun QQAndroidBot.addNewStranger(info: StrangerInfoImpl): StrangerImpl? { + if (friends.contains(info.uin)) return null // cannot have both stranger and friend + if (strangers.contains(info.uin)) return null + val stranger = Mirai.newStranger(bot, info).impl() + strangers.delegate.add(stranger) + return stranger + } + + private suspend fun QQAndroidBot.getNewGroup(groupCode: Long): GroupImpl? { + val troopNum = FriendList.GetTroopListSimplify(client) + .sendAndExpect(network, timeoutMillis = 10_000, retry = 5) + .groups.firstOrNull { it.groupCode == groupCode } ?: return null + + return getNewGroup(troopNum) + } + + private suspend fun QQAndroidBot.getNewGroup(troopNum: StTroopNum): GroupImpl? { + return GroupImpl( + bot = this, + parentCoroutineContext = coroutineContext, + id = troopNum.groupCode, + groupInfo = GroupInfoImpl(troopNum), + members = Mirai.getRawGroupMemberList( + this, + troopNum.groupUin, + troopNum.groupCode, + troopNum.dwGroupOwnerUin, + ), + ) + } + +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/notice/PrivateContactSupport.kt b/mirai-core/src/commonMain/kotlin/network/notice/PrivateContactSupport.kt new file mode 100644 index 00000000000..72125c9cd57 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/notice/PrivateContactSupport.kt @@ -0,0 +1,44 @@ +/* + * 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.network.notice + +import net.mamoe.mirai.internal.QQAndroidBot +import net.mamoe.mirai.internal.contact.GroupImpl + +/////////////////////////////////////////////////////////////////////////// +// Extension interfaces ---- should convert to context receivers in the future. +/////////////////////////////////////////////////////////////////////////// + +internal interface BotAware : PrivateContactSupport { + override val bot: QQAndroidBot +} + +internal interface GroupAware : GroupMemberSupport, BotAware { + override val group: GroupImpl + override val bot: QQAndroidBot get() = group.bot +} + +internal interface PrivateContactSupport { + val bot: QQAndroidBot + + fun Long.findFriend() = bot.friends[this] + fun Long.findStranger() = bot.strangers[this] + fun Long.findFriendOrStranger() = findFriend() ?: findStranger() + fun String.findFriend() = this.toLongOrNull()?.findFriend() + fun String.findStranger() = this.toLongOrNull()?.findStranger() + fun String.findFriendOrStranger() = this.toLongOrNull()?.findFriendOrStranger() +} + +internal interface GroupMemberSupport { + val group: GroupImpl + + fun Long.findMember() = group[this] + fun String.findMember() = this.toLongOrNull()?.findMember() +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/notice/TraceLoggingNoticeProcessor.kt b/mirai-core/src/commonMain/kotlin/network/notice/TraceLoggingNoticeProcessor.kt new file mode 100644 index 00000000000..836a9477dd4 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/notice/TraceLoggingNoticeProcessor.kt @@ -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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.network.notice + +import net.mamoe.mirai.internal.network.components.NoticePipelineContext +import net.mamoe.mirai.internal.network.components.SimpleNoticeProcessor +import net.mamoe.mirai.internal.utils.io.ProtocolStruct +import net.mamoe.mirai.utils.MiraiLogger +import net.mamoe.mirai.utils.systemProp +import net.mamoe.mirai.utils.warning +import net.mamoe.mirai.utils.withSwitch + +internal class TraceLoggingNoticeProcessor( + logger: MiraiLogger +) : SimpleNoticeProcessor<ProtocolStruct>(type()) { + private val logger: MiraiLogger = logger.withSwitch(systemProp("mirai.network.notice.trace.logging", false)) + + override suspend fun NoticePipelineContext.processImpl(data: ProtocolStruct) { + logger.warning { "${data::class.simpleName}: isConsumed=$isConsumed" } + } + +// override suspend fun NoticePipelineContext.processImpl(data: MsgType0x210) { +// logger.warning { "MsgType0x210: isConsumed=$isConsumed" } +// } +// +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/notice/UnconsumedNoticesAlerter.kt b/mirai-core/src/commonMain/kotlin/network/notice/UnconsumedNoticesAlerter.kt new file mode 100644 index 00000000000..aca2be60ca1 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/notice/UnconsumedNoticesAlerter.kt @@ -0,0 +1,147 @@ +/* + * 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.network.notice + +import net.mamoe.mirai.internal.message.contextualBugReportException +import net.mamoe.mirai.internal.network.components.MixedNoticeProcessor +import net.mamoe.mirai.internal.network.components.NoticePipelineContext +import net.mamoe.mirai.internal.network.notice.decoders.MsgType0x2DC +import net.mamoe.mirai.internal.network.protocol.data.jce.MsgType0x210 +import net.mamoe.mirai.internal.network.protocol.data.jce.RequestPushStatus +import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm +import net.mamoe.mirai.internal.network.protocol.data.proto.MsgOnlinePush +import net.mamoe.mirai.internal.network.protocol.data.proto.OnlinePushTrans +import net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg +import net.mamoe.mirai.internal.network.protocol.packet.chat.NewContact +import net.mamoe.mirai.internal.utils._miraiContentToString +import net.mamoe.mirai.utils.* + +internal class UnconsumedNoticesAlerter( + logger: MiraiLogger, +) : MixedNoticeProcessor() { + private val logger: MiraiLogger = logger.withSwitch(systemProp("mirai.network.notice.unconsumed.logging", false)) + + override suspend fun NoticePipelineContext.processImpl(data: MsgType0x210) { + if (isConsumed) return + when (data.uSubMsgType) { + 0x26L, // VIP 进群提示 + 0x111L, // 提示共同好友 + 0xD4L, // bot 在其他客户端被踢或主动退出而同步情况 + -> { + // Network(1994701021) 16:03:54 : unknown group 528 type 0x0000000000000026, data: 08 01 12 40 0A 06 08 F4 EF BB 8F 04 10 E7 C1 AD B8 02 18 01 22 2C 10 01 1A 1A 18 B4 DC F8 9B 0C 20 E7 C1 AD B8 02 28 06 30 02 A2 01 04 08 93 D6 03 A8 01 08 20 00 28 00 32 08 18 01 20 FE AF AF F5 05 28 00 + } + + 0xE2L -> { + // unknown + + // 0A 35 08 00 10 A2 FF 8C F0 03 1A 1B E5 90 8C E6 84 8F E4 BD A0 E7 9A 84 E5 8A A0 E5 A5 BD E5 8F 8B E8 AF B7 E6 B1 82 22 0C E6 BD 9C E6 B1 9F E7 BE A4 E5 8F 8B 28 01 + // vProtobuf.loadAs(Msgtype0x210.serializer()) + } + else -> { + logger.debug { "Unknown group 528 type 0x${data.uSubMsgType.toUHexString("")}, data: " + data.vProtobuf.toUHexString() } + } + } + } + + override suspend fun NoticePipelineContext.processImpl(data: MsgType0x2DC) { + if (isConsumed) return + logger.debug { "Unknown group 732 type ${data.kind}, data: " + data.buf.toUHexString() } + } + + override suspend fun NoticePipelineContext.processImpl(data: OnlinePushTrans.PbMsgInfo) { + if (isConsumed) return + when { + data.msgType == 529 && data.msgSubtype == 9 -> { + /* + PbMsgInfo#1773430973 { + fromUin=0x0000000026BA1173(649728371) + generalFlag=0x00000001(1) + msgData=0A 07 70 72 69 6E 74 65 72 10 02 1A CD 02 0A 1F 53 61 6D 73 75 6E 67 20 4D 4C 2D 31 38 36 30 20 53 65 72 69 65 73 20 28 55 53 42 30 30 31 29 0A 16 4F 6E 65 4E 6F 74 65 20 66 6F 72 20 57 69 6E 64 6F 77 73 20 31 30 0A 19 50 68 61 6E 74 6F 6D 20 50 72 69 6E 74 20 74 6F 20 45 76 65 72 6E 6F 74 65 0A 11 4F 6E 65 4E 6F 74 65 20 28 44 65 73 6B 74 6F 70 29 0A 1D 4D 69 63 72 6F 73 6F 66 74 20 58 50 53 20 44 6F 63 75 6D 65 6E 74 20 57 72 69 74 65 72 0A 16 4D 69 63 72 6F 73 6F 66 74 20 50 72 69 6E 74 20 74 6F 20 50 44 46 0A 15 46 6F 78 69 74 20 50 68 61 6E 74 6F 6D 20 50 72 69 6E 74 65 72 0A 03 46 61 78 32 09 0A 03 6A 70 67 10 01 18 00 32 0A 0A 04 6A 70 65 67 10 01 18 00 32 09 0A 03 70 6E 67 10 01 18 00 32 09 0A 03 67 69 66 10 01 18 00 32 09 0A 03 62 6D 70 10 01 18 00 32 09 0A 03 64 6F 63 10 01 18 01 32 0A 0A 04 64 6F 63 78 10 01 18 01 32 09 0A 03 74 78 74 10 00 18 00 32 09 0A 03 70 64 66 10 01 18 01 32 09 0A 03 70 70 74 10 01 18 01 32 0A 0A 04 70 70 74 78 10 01 18 01 32 09 0A 03 78 6C 73 10 01 18 01 32 0A 0A 04 78 6C 73 78 10 01 18 01 + msgSeq=0x00001AFF(6911) + msgSubtype=0x00000009(9) + msgTime=0x5FDF21A3(1608458659) + msgType=0x00000211(529) + msgUid=0x010000005FDEE04C(72057595646369868) + realMsgTime=0x5FDF21A3(1608458659) + svrIp=0x3E689409(1047041033) + toUin=0x0000000026BA1173(649728371) + } + */ + return + } + } + if (logger.isEnabled && logger.isDebugEnabled) { + logger.debug( + contextualBugReportException( + "解析 OnlinePush.PbPushTransMsg, msgType=${data.msgType}", + data._miraiContentToString(), + null, + "并描述此时机器人是否被踢出, 或是否有成员列表变更等动作.", + ) + ) + } + } + + override suspend fun NoticePipelineContext.processImpl(data: MsgOnlinePush.PbPushMsg) { + if (isConsumed) return + + } + + override suspend fun NoticePipelineContext.processImpl(data: MsgComm.Msg) { + if (isConsumed) return + when (data.msgHead.msgType) { + 732 -> { + // 732: 27 0B 60 E7 0C 01 3E 03 3F A2 5E 90 60 E2 00 01 44 71 47 90 00 00 02 58 + // 732: 27 0B 60 E7 11 00 40 08 07 20 E7 C1 AD B8 02 5A 36 08 B4 E7 E0 F0 09 1A 1A 08 9C D4 16 10 F7 D2 D8 F5 05 18 D0 E2 85 F4 06 20 00 28 00 30 B4 E7 E0 F0 09 2A 0E 08 00 12 0A 08 9C D4 16 10 00 18 01 20 00 30 00 38 00 + // 732: 27 0B 60 E7 11 00 33 08 07 20 E7 C1 AD B8 02 5A 29 08 EE 97 85 E9 01 1A 19 08 EE D6 16 10 FF F2 D8 F5 05 18 E9 E7 A3 05 20 00 28 00 30 EE 97 85 E9 01 2A 02 08 00 30 00 38 00 + + // unknown + // 前 4 byte 是群号 + } + 84, 87 -> { // 请求入群验证 和 被要求入群 + bot.network.run { + NewContact.SystemMsgNewGroup(bot.client).sendWithoutExpect() + } + } + 187 -> { // 请求加好友验证 + bot.network.run { + NewContact.SystemMsgNewFriend(bot.client).sendWithoutExpect() + } + } + else -> { + logger.debug { "unknown PbGetMsg type ${data.msgHead.msgType}, data=${data.msgBody.msgContent.toUHexString()}" } + } + } + } + + override suspend fun NoticePipelineContext.processImpl(data: Structmsg.StructMsg) { + if (isConsumed) return + if (logger.isEnabled && logger.isDebugEnabled) { + data.msg?.context { + throw contextualBugReportException( + "解析 NewContact.SystemMsgNewGroup, subType=$subType, groupMsgType=$groupMsgType", + forDebug = this._miraiContentToString(), + additional = "并尽量描述此时机器人是否正被邀请加入群, 或者是有有新群员加入此群", + ) + } + } + } + + override suspend fun NoticePipelineContext.processImpl(data: RequestPushStatus) { + if (isConsumed) return + if (logger.isEnabled && logger.isDebugEnabled) { + throw contextualBugReportException( + "decode SvcRequestPushStatus (PC Client status change)", + data._miraiContentToString(), + additional = "unknown status=${data.status}", + ) + } + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/notice/decoders/GroupNotificationDecoder.kt b/mirai-core/src/commonMain/kotlin/network/notice/decoders/GroupNotificationDecoder.kt new file mode 100644 index 00000000000..19f8cfa6c7b --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/notice/decoders/GroupNotificationDecoder.kt @@ -0,0 +1,34 @@ +/* + * 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.network.notice.decoders + +import net.mamoe.mirai.internal.contact.GroupImpl +import net.mamoe.mirai.internal.network.components.MixedNoticeProcessor +import net.mamoe.mirai.internal.network.components.NoticePipelineContext +import net.mamoe.mirai.internal.network.protocol.data.proto.TroopTips0x857 +import net.mamoe.mirai.internal.utils.io.ProtocolStruct +import net.mamoe.mirai.internal.utils.io.serialization.loadAs + +internal class GroupNotificationDecoder : MixedNoticeProcessor() { + override suspend fun NoticePipelineContext.processImpl(data: MsgType0x2DC) { + when (data.kind) { + 0x10 -> { + val proto = data.buf.loadAs(TroopTips0x857.NotifyMsgBody.serializer(), offset = 1) + processAlso(DecodedNotifyMsgBody(data.kind, data.group, proto)) + } + } + } +} + +internal data class DecodedNotifyMsgBody( + override val kind: Int, + override val group: GroupImpl, + override val buf: TroopTips0x857.NotifyMsgBody, +) : BaseMsgType0x2DC<TroopTips0x857.NotifyMsgBody>, ProtocolStruct \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/notice/decoders/MsgInfoDecoder.kt b/mirai-core/src/commonMain/kotlin/network/notice/decoders/MsgInfoDecoder.kt new file mode 100644 index 00000000000..921c96df77f --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/notice/decoders/MsgInfoDecoder.kt @@ -0,0 +1,109 @@ +/* + * 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.network.notice.decoders + +import kotlinx.io.core.discardExact +import kotlinx.io.core.readBytes +import kotlinx.io.core.readUInt +import net.mamoe.mirai.internal.contact.GroupImpl +import net.mamoe.mirai.internal.contact.checkIsGroupImpl +import net.mamoe.mirai.internal.getGroupByUin +import net.mamoe.mirai.internal.network.components.NoticePipelineContext +import net.mamoe.mirai.internal.network.components.NoticePipelineContext.Companion.KEY_MSG_INFO +import net.mamoe.mirai.internal.network.components.SimpleNoticeProcessor +import net.mamoe.mirai.internal.network.components.SyncController.Companion.syncController +import net.mamoe.mirai.internal.network.components.syncOnlinePush +import net.mamoe.mirai.internal.network.notice.GroupAware +import net.mamoe.mirai.internal.network.protocol.data.jce.MsgInfo +import net.mamoe.mirai.internal.network.protocol.data.jce.MsgType0x210 +import net.mamoe.mirai.internal.network.protocol.data.jce.OnlinePushPack.SvcReqPushMsg +import net.mamoe.mirai.internal.utils.io.ProtocolStruct +import net.mamoe.mirai.internal.utils.io.serialization.loadAs +import net.mamoe.mirai.utils.MiraiLogger +import net.mamoe.mirai.utils.debug +import net.mamoe.mirai.utils.read +import net.mamoe.mirai.utils.toUHexString + +/** + * Decodes [SvcReqPushMsg] to [MsgInfo] then re-fire [MsgType0x210] or [MsgType0x2DC] + */ +internal class MsgInfoDecoder( + private val logger: MiraiLogger, +) : SimpleNoticeProcessor<SvcReqPushMsg>(type()) { + override suspend fun NoticePipelineContext.processImpl(data: SvcReqPushMsg) { + // SvcReqPushMsg is fully handled here, no need to set consumed. + + for (msgInfo in data.vMsgInfos) { + decodeMsgInfo(msgInfo) + } + } + + private suspend fun NoticePipelineContext.decodeMsgInfo(data: MsgInfo) { + if (!bot.syncController.syncOnlinePush(data)) return + @Suppress("MoveVariableDeclarationIntoWhen") // for debug + val id = data.shMsgType.toUShort().toInt() + when (id) { + // 528 + 0x210 -> processAlso(data.vMsg.loadAs(MsgType0x210.serializer()), KEY_MSG_INFO to data) + + // 732 + 0x2dc -> { + data.vMsg.read { + val groupCode = readUInt().toLong() + val group = bot.getGroup(groupCode) ?: bot.getGroupByUin(groupCode) + ?: return // group has not been initialized + group.checkIsGroupImpl() + + val kind = readByte().toInt() + discardExact(1) + + processAlso(MsgType0x2DC(kind, group, this.readBytes()), KEY_MSG_INFO to data) + } + } + else -> { + logger.debug { "Unknown MsgInfo kind ${data.shMsgType.toInt()}, data=${data.vMsg.toUHexString()}" } + } + } + } +} + +internal interface BaseMsgType0x2DC<V> : GroupAware { + val kind: Int + override val group: GroupImpl + val buf: V + + override val bot get() = group.bot +} + +internal data class MsgType0x2DC( + override val kind: Int, // inner kind, read from vMsg + override val group: GroupImpl, + override val buf: ByteArray, +) : ProtocolStruct, BaseMsgType0x2DC<ByteArray> { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MsgType0x2DC + + if (kind != other.kind) return false + if (group != other.group) return false + if (!buf.contentEquals(other.buf)) return false + + return true + } + + override fun hashCode(): Int { + var result = kind + result = 31 * result + group.hashCode() + result = 31 * result + buf.contentHashCode() + return result + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/notice/group/GroupMessageProcessor.kt b/mirai-core/src/commonMain/kotlin/network/notice/group/GroupMessageProcessor.kt new file mode 100644 index 00000000000..c4d4cf9f9a2 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/notice/group/GroupMessageProcessor.kt @@ -0,0 +1,198 @@ +/* + * 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.network.notice.group + +import net.mamoe.mirai.contact.Member +import net.mamoe.mirai.event.AbstractEvent +import net.mamoe.mirai.event.Event +import net.mamoe.mirai.event.broadcast +import net.mamoe.mirai.event.events.GroupMessageEvent +import net.mamoe.mirai.event.events.GroupMessageSyncEvent +import net.mamoe.mirai.event.events.MemberCardChangeEvent +import net.mamoe.mirai.internal.contact.GroupImpl +import net.mamoe.mirai.internal.contact.NormalMemberImpl +import net.mamoe.mirai.internal.contact.info +import net.mamoe.mirai.internal.contact.info.MemberInfoImpl +import net.mamoe.mirai.internal.contact.newAnonymous +import net.mamoe.mirai.internal.message.toMessageChainOnline +import net.mamoe.mirai.internal.network.Packet +import net.mamoe.mirai.internal.network.components.NoticePipelineContext +import net.mamoe.mirai.internal.network.components.SimpleNoticeProcessor +import net.mamoe.mirai.internal.network.components.SyncController.Companion.syncController +import net.mamoe.mirai.internal.network.notice.group.GroupMessageProcessor.MemberNick.Companion.generateMemberNickFromMember +import net.mamoe.mirai.internal.network.notice.priv.PrivateMessageProcessor +import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody +import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm +import net.mamoe.mirai.internal.network.protocol.data.proto.MsgOnlinePush +import net.mamoe.mirai.internal.network.protocol.data.proto.Oidb0x8fc +import net.mamoe.mirai.internal.utils.io.serialization.loadAs +import net.mamoe.mirai.message.data.MessageSourceKind +import net.mamoe.mirai.utils.* + +/** + * Handles [GroupMessageEvent]. For private message events, see [PrivateMessageProcessor] + */ +internal class GroupMessageProcessor( + private val logger: MiraiLogger, +) : SimpleNoticeProcessor<MsgOnlinePush.PbPushMsg>(type()) { + internal data class SendGroupMessageReceipt( + val messageRandom: Int, + val sequenceId: Int, + val fromAppId: Int, + ) : Packet, Event, Packet.NoLog, AbstractEvent() { + override fun toString(): String { + return "OnlinePush.PbPushGroupMsg.SendGroupMessageReceipt(messageRandom=$messageRandom, sequenceId=$sequenceId)" + } + + companion object { + val EMPTY = SendGroupMessageReceipt(0, 0, 0) + } + } + + private data class MemberNick(val nick: String, val isNameCard: Boolean = false) { + companion object { + fun Member.generateMemberNickFromMember(): MemberNick { + return nameCard.takeIf { nameCard.isNotEmpty() }?.let { + MemberNick(nameCard, true) + } ?: MemberNick(nick, false) + } + } + } + + + override suspend fun NoticePipelineContext.processImpl(data: MsgOnlinePush.PbPushMsg) { + val msgHead = data.msg.msgHead + + val isFromSelfAccount = msgHead.fromUin == bot.id + if (isFromSelfAccount) { + val messageRandom = data.msg.msgBody.richText.attr?.random ?: return + + if (bot.syncController.containsGroupMessageReceipt(messageRandom) + || msgHead.fromAppid == 3116 || msgHead.fromAppid == 2021 + ) { + // 3116=group music share + // 2021=group file + // message sent by bot + collect(SendGroupMessageReceipt(messageRandom, msgHead.msgSeq, msgHead.fromAppid)) + return + } + // else: sync form other device + } + + if (msgHead.groupInfo == null) return + + val group = bot.getGroup(msgHead.groupInfo.groupCode) as GroupImpl? ?: return // 机器人还正在进群 + + + // fragmented message + val msgs = group.groupPkgMsgParsingCache.tryMerge(data).ifEmpty { return } + + var extraInfo: ImMsgBody.ExtraInfo? = null + var anonymous: ImMsgBody.AnonymousGroupMsg? = null + + for (msg in msgs) { + for (elem in msg.msg.msgBody.richText.elems) { + when { + elem.extraInfo != null -> extraInfo = elem.extraInfo + elem.anonGroupMsg != null -> anonymous = elem.anonGroupMsg + } + } + } + + + val sender: Member // null if sync from other client + val nameCard: MemberNick + + if (anonymous != null) { // anonymous member + sender = group.newAnonymous(anonymous.anonNick.encodeToString(), anonymous.anonId.encodeBase64()) + nameCard = sender.generateMemberNickFromMember() + } else { // normal member chat + sender = group[msgHead.fromUin] ?: kotlin.run { + logger.warning { "Failed to find member ${msgHead.fromUin} in group ${group.id}" } + return + } + nameCard = findSenderName(extraInfo, msgHead.groupInfo) ?: sender.generateMemberNickFromMember() + } + + sender.info?.castOrNull<MemberInfoImpl>()?.run { + lastSpeakTimestamp = currentTimeSeconds().toInt() + } + + if (isFromSelfAccount) { + collect( + GroupMessageSyncEvent( + message = msgs.map { it.msg }.toMessageChainOnline(bot, group.id, MessageSourceKind.GROUP), + time = msgHead.msgTime, + group = group, + sender = sender, + senderName = nameCard.nick, + ), + ) + return + } else { + + broadcastNameCardChangedEventIfNecessary(sender, nameCard) + + collect( + GroupMessageEvent( + senderName = nameCard.nick, + sender = sender, + message = msgs.map { it.msg }.toMessageChainOnline(bot, group.id, MessageSourceKind.GROUP), + permission = sender.permission, + time = msgHead.msgTime, + ), + ) + return + } + } + + private suspend inline fun broadcastNameCardChangedEventIfNecessary( + sender: Member, + new: MemberNick, + ) { + if (sender is NormalMemberImpl) { + val currentNameCard = sender.nameCard + if (new.isNameCard) { + new.nick.let { name -> + if (currentNameCard != name) { + sender._nameCard = name + MemberCardChangeEvent(currentNameCard, name, sender).broadcast() + } + } + } else { + // 说明删除了群名片 + if (currentNameCard.isNotEmpty()) { + sender._nameCard = "" + MemberCardChangeEvent(currentNameCard, "", sender).broadcast() + } + } + } + } + + private fun findSenderName( + extraInfo: ImMsgBody.ExtraInfo?, + groupInfo: MsgComm.GroupInfo, + ): MemberNick? = + extraInfo?.groupCard?.takeIf { it.isNotEmpty() }?.decodeCommCardNameBuf()?.let { + MemberNick(it, true) + } ?: groupInfo.takeIf { it.groupCard.isNotEmpty() }?.let { + MemberNick(it.groupCard, it.groupCardType != 2) + } + + private fun ByteArray.decodeCommCardNameBuf() = kotlin.runCatching { + if (this[0] == 0x0A.toByte()) { + val nameBuf = loadAs(Oidb0x8fc.CommCardNameBuf.serializer()) + if (nameBuf.richCardName.isNotEmpty()) { + return@runCatching nameBuf.richCardName.joinToString("") { it.text.encodeToString() } + } + } + return@runCatching null + }.getOrNull() ?: encodeToString() +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/notice/group/GroupNotificationProcessor.kt b/mirai-core/src/commonMain/kotlin/network/notice/group/GroupNotificationProcessor.kt new file mode 100644 index 00000000000..5d072acad23 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/notice/group/GroupNotificationProcessor.kt @@ -0,0 +1,340 @@ +/* + * 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.network.notice.group + +import kotlinx.io.core.readUInt +import kotlinx.io.core.readUShort +import net.mamoe.mirai.contact.NormalMember +import net.mamoe.mirai.data.GroupHonorType +import net.mamoe.mirai.event.events.* +import net.mamoe.mirai.internal.QQAndroidBot +import net.mamoe.mirai.internal.contact.GroupImpl +import net.mamoe.mirai.internal.contact.checkIsGroupImpl +import net.mamoe.mirai.internal.contact.checkIsMemberImpl +import net.mamoe.mirai.internal.network.Packet +import net.mamoe.mirai.internal.network.components.MixedNoticeProcessor +import net.mamoe.mirai.internal.network.components.NoticePipelineContext +import net.mamoe.mirai.internal.network.notice.NewContactSupport +import net.mamoe.mirai.internal.network.notice.decoders.MsgType0x2DC +import net.mamoe.mirai.internal.network.protocol.data.jce.MsgType0x210 +import net.mamoe.mirai.internal.network.protocol.data.proto.Submsgtype0x122 +import net.mamoe.mirai.internal.network.protocol.data.proto.Submsgtype0x27 +import net.mamoe.mirai.internal.network.protocol.data.proto.TroopTips0x857 +import net.mamoe.mirai.internal.utils._miraiContentToString +import net.mamoe.mirai.internal.utils.io.serialization.loadAs +import net.mamoe.mirai.utils.* + +internal class GroupNotificationProcessor( + private val logger: MiraiLogger, +) : MixedNoticeProcessor(), NewContactSupport { + + override suspend fun NoticePipelineContext.processImpl(data: MsgType0x210) = data.context { + when (data.uSubMsgType) { + 0x27L -> { + val body = vProtobuf.loadAs(Submsgtype0x27.SubMsgType0x27.SubMsgType0x27MsgBody.serializer()) + for (msgModInfo in body.msgModInfos) { + markAsConsumed(msgModInfo) + when { + msgModInfo.msgModGroupProfile != null -> handleGroupProfileChanged(msgModInfo.msgModGroupProfile) + msgModInfo.msgModGroupMemberProfile != null -> handleGroupMemberProfileChanged(msgModInfo.msgModGroupMemberProfile) + else -> markNotConsumed(msgModInfo) + } + } + } + } + } + + /** + * @see GroupNameChangeEvent + */ + private fun NoticePipelineContext.handleGroupProfileChanged( + modGroupProfile: Submsgtype0x27.SubMsgType0x27.ModGroupProfile + ) { + for (info in modGroupProfile.msgGroupProfileInfos) { + when (info.field) { + 1 -> { + // 群名 + val new = info.value.encodeToString() + + val group = bot.getGroup(modGroupProfile.groupCode) ?: continue + group.checkIsGroupImpl() + val old = group.name + + if (new == old) continue + + if (modGroupProfile.cmdUin == bot.id) continue + val operator = group[modGroupProfile.cmdUin] ?: continue + + group.settings.nameField = new + + collect(GroupNameChangeEvent(old, new, group, operator)) + } + 2 -> { + // 头像 + // top_package/akkz.java:3446 + /* + var4 = var82.byteAt(0); + short var3 = (short) (var82.byteAt(1) | var4 << 8); + var85 = var18.method_77927(var7 + ""); + var85.troopface = var3; + var85.hasSetNewTroopHead = true; + */ + // bot.logger.debug( + // contextualBugReportException( + // "解析 Transformers528 0x27L ModGroupProfile 群头像修改", + // forDebug = "this=${this._miraiContentToString()}" + // ) + // ) + } + 3 -> { // troop.credit.data + // top_package/akkz.java:3475 + // top_package/akkz.java:3498 + // bot.logger.debug( + // contextualBugReportException( + // "解析 Transformers528 0x27L ModGroupProfile 群 troop.credit.data", + // forDebug = "this=${this._miraiContentToString()}" + // ) + // ) + } + else -> { + } + } + } + } + + /** + * @see MemberCardChangeEvent + */ + private fun NoticePipelineContext.handleGroupMemberProfileChanged( + modGroupMemberProfile: Submsgtype0x27.SubMsgType0x27.ModGroupMemberProfile + ) { + for (info in modGroupMemberProfile.msgGroupMemberProfileInfos) { + when (info.field) { + 1 -> { // name card + val new = info.value + val group = bot.getGroup(modGroupMemberProfile.groupCode) ?: continue + group.checkIsGroupImpl() + val member = group[modGroupMemberProfile.uin] ?: continue + member.checkIsMemberImpl() + + val old = member.nameCard + + if (new == old) continue + member._nameCard = new + + collect(MemberCardChangeEvent(old, new, member)) + } + 2 -> { + if (info.value.singleOrNull()?.code != 0) { + logger.debug { + "Unknown Transformers528 0x27L ModGroupMemberProfile, field=${info.field}, value=${info.value}" + } + } + continue + } + else -> { + logger.debug { + "Unknown Transformers528 0x27L ModGroupMemberProfile, field=${info.field}, value=${info.value}" + } + continue + } + } + } + } + + + /////////////////////////////////////////////////////////////////////////// + // MsgType0x2DC + /////////////////////////////////////////////////////////////////////////// + + override suspend fun NoticePipelineContext.processImpl(data: MsgType0x2DC) { + when (data.kind) { + 0x0C -> processMute(data) + 0x0E -> processAllowAnonymousChat(data) + 0x10 -> processAllowConfessTask(data) + 0x14 -> processGrayTip(data) + } + } + + /** + * @see MemberMuteEvent + * @see MemberUnmuteEvent + * @see GroupMuteAllEvent + * @see BotMuteEvent + * @see BotUnmuteEvent + */ + private fun NoticePipelineContext.processMute( + data: MsgType0x2DC, + ) = data.context { + fun handleMuteMemberPacket( + bot: QQAndroidBot, + group: GroupImpl, + operator: NormalMember, + target: Long, + timeSeconds: Int, + ): Packet? { + if (target == 0L) { + val new = timeSeconds != 0 + if (group.settings.isMuteAllField == new) { + return null + } + group.settings.isMuteAllField = new + return GroupMuteAllEvent(!new, new, group, operator) + } + + if (target == bot.id) { + return when { + group.botMuteRemaining == timeSeconds -> null + timeSeconds == 0 || timeSeconds == 0xFFFF_FFFF.toInt() -> { + group.botAsMember.checkIsMemberImpl()._muteTimestamp = 0 + BotUnmuteEvent(operator) + } + else -> { + group.botAsMember.checkIsMemberImpl()._muteTimestamp = + currentTimeSeconds().toInt() + timeSeconds + BotMuteEvent(timeSeconds, operator) + } + } + } + + val member = group[target] ?: return null + member.checkIsMemberImpl() + + if (member.muteTimeRemaining == timeSeconds) return null + + member._muteTimestamp = currentTimeSeconds().toInt() + timeSeconds + return if (timeSeconds == 0) MemberUnmuteEvent(member, operator) + else MemberMuteEvent(member, timeSeconds, operator) + } + + markAsConsumed() + + buf.read { + val operatorUin = readUInt().toLong() + if (operatorUin == bot.id) return + val operator = group[operatorUin] ?: return + readUInt().toLong() // time + val length = readUShort().toInt() + repeat(length) { + val target = readUInt().toLong() + val timeSeconds = readUInt() + collected += handleMuteMemberPacket(bot, group, operator, target, timeSeconds.toInt()) + } + } + } + + /** + * @see GroupAllowAnonymousChatEvent + */ + private fun NoticePipelineContext.processAllowAnonymousChat( + data: MsgType0x2DC, + ) = data.context { + markAsConsumed() + buf.read { + val operator = group[readUInt().toLong()] ?: return + val new = readInt() == 0 + if (group.settings.isAnonymousChatEnabledField == new) return + + group.settings.isAnonymousChatEnabledField = new + collect(GroupAllowAnonymousChatEvent(!new, new, group, operator)) + } + } + + /** + * @see GroupAllowConfessTalkEvent + */ + private fun NoticePipelineContext.processAllowConfessTask( + data: MsgType0x2DC, + ) = data.context { + val proto = data.buf.loadAs(TroopTips0x857.NotifyMsgBody.serializer(), offset = 1) + markAsConsumed() + when (proto.optEnumType) { + 1 -> { + val tipsInfo = proto.optMsgGraytips ?: return + + val message = tipsInfo.optBytesContent.decodeToString() + // 机器人信息 + when (tipsInfo.robotGroupOpt) { + // others + 0 -> { + if (message.endsWith("群聊坦白说")) { + val new = when (message) { + "管理员已关闭群聊坦白说" -> false + "管理员已开启群聊坦白说" -> true + else -> { + logger.debug { "Unknown server confess talk messages $message" } + return + } + } + collect(GroupAllowConfessTalkEvent(new, !new, group, false)) + } + } + } + } + else -> markNotConsumed() + } + } + + /** + * @see NudgeEvent + * @see MemberHonorChangeEvent + * @see GroupTalkativeChangeEvent + */ // gray tip: 聊天中的灰色小框系统提示信息 + private fun NoticePipelineContext.processGrayTip( + data: MsgType0x2DC, + ) = data.context { + val grayTip = buf.loadAs(TroopTips0x857.NotifyMsgBody.serializer(), 1).optGeneralGrayTip + markAsConsumed() + when (grayTip?.templId) { + // 群戳一戳 + 10043L, 1133L, 1132L, 1134L, 1135L, 1136L -> { + // group nudge + // 预置数据,服务器将不会提供己方已知消息 + val action = grayTip.msgTemplParam["action_str"].orEmpty() + val from = grayTip.msgTemplParam["uin_str1"]?.findMember() ?: group.botAsMember + val target = grayTip.msgTemplParam["uin_str2"]?.findMember() ?: group.botAsMember + val suffix = grayTip.msgTemplParam["suffix_str"].orEmpty() + + collected += NudgeEvent( + from = if (from.id == bot.id) bot else from, + target = if (target.id == bot.id) bot else target, + action = action, + suffix = suffix, + subject = group, + ) + } + // 龙王 + 10093L, 1053L, 1054L -> { + val now: NormalMember = grayTip.msgTemplParam["uin"]?.findMember() ?: group.botAsMember + val previous: NormalMember? = grayTip.msgTemplParam["uin_last"]?.findMember() + + if (previous == null) { + collect(MemberHonorChangeEvent.Achieve(now, GroupHonorType.TALKATIVE)) + } else { + collect(GroupTalkativeChangeEvent(group, now, previous)) + collect(MemberHonorChangeEvent.Lose(previous, GroupHonorType.TALKATIVE)) + collect(MemberHonorChangeEvent.Achieve(now, GroupHonorType.TALKATIVE)) + } + } + else -> { + markNotConsumed() + logger.debug { + "Unknown Transformers528 0x14 template\ntemplId=${grayTip?.templId}\nPermList=${grayTip?.msgTemplParam?._miraiContentToString()}" + } + } + } + } +} + +internal operator fun List<TroopTips0x857.TemplParam>.get(name: String) = this.findLast { it.name == name }?.value + +@JvmName("get2") +internal operator fun List<Submsgtype0x122.Submsgtype0x122.TemplParam>.get(name: String) = + this.findLast { it.name == name }?.value diff --git a/mirai-core/src/commonMain/kotlin/network/notice/group/GroupOrMemberListNoticeProcessor.kt b/mirai-core/src/commonMain/kotlin/network/notice/group/GroupOrMemberListNoticeProcessor.kt new file mode 100644 index 00000000000..70cdf7fa687 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/notice/group/GroupOrMemberListNoticeProcessor.kt @@ -0,0 +1,534 @@ +/* + * 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.network.notice.group + +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.cancel +import kotlinx.coroutines.sync.withLock +import kotlinx.io.core.discardExact +import kotlinx.io.core.readUByte +import kotlinx.io.core.readUInt +import net.mamoe.mirai.Mirai +import net.mamoe.mirai.contact.MemberPermission.* +import net.mamoe.mirai.event.events.* +import net.mamoe.mirai.internal.contact.addNewNormalMember +import net.mamoe.mirai.internal.contact.info.MemberInfoImpl +import net.mamoe.mirai.internal.getGroupByUin +import net.mamoe.mirai.internal.getGroupByUinOrCode +import net.mamoe.mirai.internal.message.contextualBugReportException +import net.mamoe.mirai.internal.network.components.ContactUpdater +import net.mamoe.mirai.internal.network.components.MixedNoticeProcessor +import net.mamoe.mirai.internal.network.components.NoticePipelineContext +import net.mamoe.mirai.internal.network.notice.decoders.DecodedNotifyMsgBody +import net.mamoe.mirai.internal.network.protocol.data.jce.MsgType0x210 +import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm +import net.mamoe.mirai.internal.network.protocol.data.proto.OnlinePushTrans +import net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg +import net.mamoe.mirai.internal.network.protocol.data.proto.Submsgtype0x44 +import net.mamoe.mirai.internal.utils._miraiContentToString +import net.mamoe.mirai.internal.utils.io.serialization.loadAs +import net.mamoe.mirai.internal.utils.parseToMessageDataList +import net.mamoe.mirai.internal.utils.toMemberInfo +import net.mamoe.mirai.utils.MiraiLogger +import net.mamoe.mirai.utils.context +import net.mamoe.mirai.utils.read + + +/** + * Member/Bot invited/active join // force/active leave + * Member/Bot permission change + * + * @see BotJoinGroupEvent + * @see MemberJoinEvent + * + * @see BotLeaveEvent + * @see MemberLeaveEvent + * + * @see MemberPermissionChangeEvent + * @see BotGroupPermissionChangeEvent + * + * @see BotInvitedJoinGroupRequestEvent + * @see MemberJoinRequestEvent + */ +internal class GroupOrMemberListNoticeProcessor( + private val logger: MiraiLogger, +) : MixedNoticeProcessor() { + + override suspend fun NoticePipelineContext.processImpl(data: MsgType0x210) { + if (data.uSubMsgType != 0x44L) return + markAsConsumed() + val msg = data.vProtobuf.loadAs(Submsgtype0x44.Submsgtype0x44.MsgBody.serializer()) + if (msg.msgGroupMsgSync == null) return + + when (msg.msgGroupMsgSync.msgType) { + 1, 2 -> { + bot.components[ContactUpdater].groupListModifyLock.withLock { + bot.addNewGroupByCode(msg.msgGroupMsgSync.grpCode)?.let { + collect(BotJoinGroupEvent.Active(it)) + } + } + } + } + } + + /** + * @see MemberJoinEvent.Invite + * @see MemberLeaveEvent.Quit + */ + override suspend fun NoticePipelineContext.processImpl(data: DecodedNotifyMsgBody) = data.context { + val proto = data.buf + if (proto.optEnumType != 1) return + val tipsInfo = proto.optMsgGraytips ?: return + + val message = tipsInfo.optBytesContent.decodeToString() + // 机器人信息 + markAsConsumed() + when (tipsInfo.robotGroupOpt) { + // 添加 + 1 -> { + val dataList = message.parseToMessageDataList() + val invitor = dataList.first().let { messageData -> + group[messageData.data.toLong()] ?: return + } + val member = dataList.last().let { messageData -> + group.addNewNormalMember(messageData.toMemberInfo()) ?: return + } + collect(MemberJoinEvent.Invite(member, invitor)) + } + // 移除 + 2 -> { + message.parseToMessageDataList().first().let { + val member = group.getOrFail(it.data.toLong()) + group.members.delegate.remove(member) + collect(MemberLeaveEvent.Quit(member)) + } + } + else -> markNotConsumed() + } + } + + /** + * @see MemberJoinEvent.Invite + * @see BotJoinGroupEvent.Invite + * @see MemberJoinEvent.Active + * @see BotJoinGroupEvent.Active + */ + override suspend fun NoticePipelineContext.processImpl(data: MsgComm.Msg) = data.context { + bot.components[ContactUpdater].groupListModifyLock.withLock { + when (data.msgHead.msgType) { + 33 -> processGroupJoin33(data) + 34 -> Unit // 34 与 33 重复, 忽略 34 + 38 -> processGroupJoin38(data) + 85 -> processGroupJoin85(data) + else -> return + } + markAsConsumed() + } + } + + // 33 + private suspend fun NoticePipelineContext.processGroupJoin33(data: MsgComm.Msg) = data.context { + msgBody.msgContent.read { + val groupUin = Mirai.calculateGroupUinByGroupCode(readUInt().toLong()) + val group = bot.getGroupByUin(groupUin) ?: bot.addNewGroupByUin(groupUin) ?: return + discardExact(1) + val joinedMemberUin = readUInt().toLong() + val joinType = readByte().toInt() + val invitorUin = readUInt().toLong() + when (joinType) { + // 邀请加入 + -125, 3 -> { + val invitor = group[invitorUin] ?: return + collected += if (joinedMemberUin == bot.id) { + BotJoinGroupEvent.Invite(invitor) + } else { + MemberJoinEvent.Invite(group.addNewNormalMember(getNewMemberInfo()) ?: return, invitor) + } + } + // 通过群员分享的二维码/直接加入 + -126, 2 -> { + collected += if (joinedMemberUin == bot.id) { + BotJoinGroupEvent.Active(group) + } else { + MemberJoinEvent.Active(group.addNewNormalMember(getNewMemberInfo()) ?: return) + } + } + // 忽略 + else -> { + } + } + } + // 邀请入群 + // package: 27 0B 60 E7 01 CA CC 69 8B 83 44 71 47 90 06 B9 DC C0 ED D4 B1 00 30 33 44 30 42 38 46 30 39 37 32 38 35 43 34 31 38 30 33 36 41 34 36 31 36 31 35 32 37 38 46 46 43 30 41 38 30 36 30 36 45 38 31 43 39 41 34 38 37 + // package: groupUin + 01 CA CC 69 8B 83 + invitorUin + length(06) + string + magicKey + + + // 主动入群, 直接加入: msgContent=27 0B 60 E7 01 76 E4 B8 DD 82 3E 03 3F A2 06 B4 B4 BD A8 D5 DF 00 30 42 39 41 30 33 45 38 34 30 39 34 42 46 30 45 32 45 38 42 31 43 43 41 34 32 42 38 42 44 42 35 34 44 42 31 44 32 32 30 46 30 38 39 46 46 35 41 38 + // 主动直接加入 27 0B 60 E7 01 76 E4 B8 DD 82 3E 03 3F A2 06 B4 B4 BD A8 D5 DF 00 30 33 30 45 38 42 31 33 46 41 41 31 33 46 38 31 35 34 41 38 33 32 37 31 43 34 34 38 35 33 35 46 45 31 38 32 43 39 42 43 46 46 32 44 39 39 46 41 37 + + // 有人被邀请(经过同意后)加入 27 0B 60 E7 01 76 E4 B8 DD 83 3E 03 3F A2 06 B4 B4 BD A8 D5 DF 00 30 34 30 34 38 32 33 38 35 37 41 37 38 46 33 45 37 35 38 42 39 38 46 43 45 44 43 32 41 30 31 36 36 30 34 31 36 39 35 39 30 38 39 30 39 45 31 34 34 + // 搜索到群, 直接加入 27 0B 60 E7 01 07 6E 47 BA 82 3E 03 3F A2 06 B4 B4 BD A8 D5 DF 00 30 32 30 39 39 42 39 41 46 32 39 41 35 42 33 46 34 32 30 44 36 44 36 39 35 44 38 45 34 35 30 46 30 45 30 38 45 31 41 39 42 46 46 45 32 30 32 34 35 + } + + // 38 + private suspend fun NoticePipelineContext.processGroupJoin38(data: MsgComm.Msg) = data.context { + if (bot.getGroupByUin(msgHead.fromUin) != null) return + bot.addNewGroupByUin(msgHead.fromUin)?.let { collect(BotJoinGroupEvent.Active(it)) } + } + + // 85 + private suspend fun NoticePipelineContext.processGroupJoin85(data: MsgComm.Msg) = data.context { + // msgHead.authUin: 处理人 + if (msgHead.toUin != bot.id) return + processGroupJoin38(data) + } + + /////////////////////////////////////////////////////////////////////////// + // Structmsg.StructMsg + /////////////////////////////////////////////////////////////////////////// + + override suspend fun NoticePipelineContext.processImpl(data: Structmsg.StructMsg) = data.msg.context { + if (this == null) return + markAsConsumed() + when (subType) { + 0 -> { + if (groupMsgType == 8) { + // #1388: 使用手机TIM邀请入群,我为管理员,成功邀请 bot 入群 + + // 能正常解析 BotInvitedJoinGroupRequestEvent 和 BotJoinGroupEvent.Active, 因此忽略该通知 + return + } else { + throw contextualBugReportException( + "解析 NewContact.SystemMsgNewGroup, subType=5, groupMsgType=$groupMsgType", + data._miraiContentToString(), + null, + "并描述此时机器人是否被邀请加入群等其他", + ) + } + } + + // 处理被邀请入群 或 处理成员入群申请 + 1 -> when (groupMsgType) { + 1 -> { + // 成员申请入群 + collected += MemberJoinRequestEvent( + bot, data.msgSeq, msgAdditional, + data.reqUin, groupCode, groupName, reqUinNick + ) + } + 2 -> { + // Bot 被邀请入群 + collected += BotInvitedJoinGroupRequestEvent( + bot, data.msgSeq, actionUin, + groupCode, groupName, actionUinNick + ) + } + 22 -> { + // 成员邀请入群 + collected += MemberJoinRequestEvent( + bot, data.msgSeq, msgAdditional, + data.reqUin, groupCode, groupName, reqUinNick, actionUin + ) + } + else -> throw contextualBugReportException( + "parse SystemMsgNewGroup, subType=1", + this._miraiContentToString(), + additional = "并尽量描述此时机器人是否正被邀请加入群, 或者是有有新群员加入此群" + ) + } + 2 -> { // 被邀请入群, 自动同意, 不需处理 + + // val group = bot.getNewGroup(groupCode) ?: return null + // val invitor = group[actionUin] + // + // BotJoinGroupEvent.Invite(invitor) + } + 3 -> { // 已被请他管理员处理 + } + 5 -> { + val group = bot.getGroup(groupCode) ?: return + when (groupMsgType) { + 3 -> { + // https://github.com/mamoe/mirai/issues/651 + // msgDescribe=将你设置为管理员 + // msgTitle=管理员设置 + } + 13 -> { + // 成员主动退出, 机器人是管理员, 接到通知 + // 但无法获取是哪个成员. + } + 7 -> { // 机器人被踢 + val operator = group[actionUin] ?: return + collected += BotLeaveEvent.Kick(operator) + } + 6 -> { + // 其他管理员踢出了一个群成员, 测试时能正常解析但没有收到 groupMsgType=6 的消息, 但有 issue 收到这些消息 + // #1429, #1171, #1263 + + // > 这是历史的群系统消息,实际上可以直接进行忽略,其实只是因为缺失了忽略的处理而已 + // https://github.com/mamoe/mirai/issues/1171#issuecomment-907075637 + } + 16 -> { + // #1467 + // 历史消息同步 + } + else -> { + throw contextualBugReportException( + "解析 NewContact.SystemMsgNewGroup, subType=5, groupMsgType=$groupMsgType", + this._miraiContentToString(), + null, + "并描述此时机器人是否被踢出群等", + ) + } + } + } + else -> markNotConsumed() + } + } + + /////////////////////////////////////////////////////////////////////////// + // OnlinePushTrans.PbMsgInfo + /////////////////////////////////////////////////////////////////////////// + + override suspend fun NoticePipelineContext.processImpl(data: OnlinePushTrans.PbMsgInfo) { + markAsConsumed() + when (data.msgType) { + 44 -> data.msgData.read { + // 3D C4 33 DD 01 FF CD 76 F4 03 C3 7E 2E 34 + // 群转让 + // start with 3D C4 33 DD 01 FF + // 3D C4 33 DD 01 FF C3 7E 2E 34 CD 76 F4 03 + // 权限变更 + // 3D C4 33 DD 01 00/01 ..... + // 3D C4 33 DD 01 01 C3 7E 2E 34 01 + discardExact(5) + val kind = readUByte().toInt() + if (kind == 0xFF) { + val from = readUInt().toLong() + val to = readUInt().toLong() + + handleGroupOwnershipTransfer(data, from, to) + } else { + val var5 = if (kind == 0 || kind == 1) 0 else readUInt().toInt() + val target = readUInt().toLong() + + if (var5 == 0) { + val newPermission = if (remaining == 1L) readByte() else return + handlePermissionChange(data, target, newPermission.toInt()) + } + } + } + 34 -> { + /* quit + 27 0B 60 E7 + 01 + 2F 55 7C B8 + 82 + 00 30 42 33 32 46 30 38 33 32 39 32 35 30 31 39 33 45 46 32 45 30 36 35 41 35 41 33 42 37 35 43 41 34 46 37 42 38 42 38 42 44 43 35 35 34 35 44 38 30 + */ + /* kick + 27 0B 60 E7 + 01 + A8 32 51 A1 + 83 3E 03 3F A2 06 B4 B4 BD A8 D5 DF 00 30 39 32 46 45 30 36 31 41 33 37 36 43 44 35 37 35 37 39 45 37 32 34 44 37 37 30 36 46 39 39 43 35 35 33 33 31 34 44 32 44 46 35 45 42 43 31 31 36 + */ + + data.msgData.read { + readUInt().toLong() // groupCode + readByte().toInt() // follow type + val target = readUInt().toLong() + val kind = readUByte().toInt() + val operator = readUInt().toLong() + val groupUin = data.fromUin + handleLeave(target, kind, operator, groupUin) + } + } + else -> markNotConsumed() + } + } + + private fun NoticePipelineContext.handleLeave( + target: Long, + kind: Int, + operator: Long, + groupUin: Long, + ) { + when (kind) { + 2, 0x82 -> bot.getGroupByUinOrCode(groupUin)?.let { group -> + if (target == bot.id) { + collect(BotLeaveEvent.Active(group)) + bot.groups.delegate.remove(group) + group.cancel(CancellationException("Left actively")) + } else { + val member = group[target] ?: return + collect(MemberLeaveEvent.Quit(member)) + group.members.delegate.remove(member) + member.cancel(CancellationException("Left actively")) + } + } + // 03 包括 bot 是群主, 管理员踢出群成员 + 3, 0x83 -> bot.getGroupByUinOrCode(groupUin)?.let { group -> + if (target == bot.id) { + val member = group.members[operator] ?: return + collect(BotLeaveEvent.Kick(member)) + bot.groups.delegate.remove(group) + group.cancel(CancellationException("Being kicked")) + } else { + val member = group[target] ?: return + collect(MemberLeaveEvent.Kick(member, group.members[operator])) + group.members.delegate.remove(member) + member.cancel(CancellationException("Being kicked")) + } + } + } + } + + /** + * Group owner changes permission of a member, when bot is a member. + * + * @see BotGroupPermissionChangeEvent + * @see MemberPermissionChangeEvent + */ + private fun NoticePipelineContext.handlePermissionChange( + data: OnlinePushTrans.PbMsgInfo, + target: Long, + newPermissionByte: Int, + ) { + val group = bot.getGroupByUinOrCode(data.fromUin) ?: return + + val newPermission = if (newPermissionByte == 1) ADMINISTRATOR else MEMBER + + if (target == bot.id) { + if (group.botPermission == newPermission) return + + collect(BotGroupPermissionChangeEvent(group, group.botPermission, newPermission)) + group.botAsMember.permission = newPermission + } else { + val member = group[target] ?: return + if (member.permission == newPermission) return + + collect(MemberPermissionChangeEvent(member, member.permission, newPermission)) + member.permission = newPermission + } + } + + /** + * Owner of the group [from] transfers ownership to another member [to], or retrieve ownership. + */ + private suspend fun NoticePipelineContext.handleGroupOwnershipTransfer( + data: OnlinePushTrans.PbMsgInfo, + from: Long, + to: Long, + ) { + val group = bot.getGroupByUinOrCode(data.fromUin) + if (from == bot.id) { + // bot -> member + group ?: return markAsConsumed() + + // Bot permission changed to MEMBER + if (group.botPermission != MEMBER) { + collect(BotGroupPermissionChangeEvent(group, group.botPermission, MEMBER)) + group.botAsMember.permission = MEMBER + } + + // member Retrieve or permission changed to OWNER + var newOwner = group[to] + if (newOwner == null) { + val nick = Mirai.queryProfile(bot, to).nickname + newOwner = group.addNewNormalMember(MemberInfoImpl(uin = to, nick = nick, permission = OWNER)) ?: return + collect(MemberJoinEvent.Retrieve(newOwner)) + } else if (newOwner.permission != OWNER) { + collect(MemberPermissionChangeEvent(newOwner, newOwner.permission, OWNER)) + newOwner.permission = OWNER + } + } else { + // member -> member/bot + + // bot Retrieve or permission changed to OWNER + if (group == null) { // TODO: 2021/8/25 test this + collect(BotJoinGroupEvent.Retrieve(bot.addNewGroupByUin(data.fromUin) ?: return)) + return + } + + // member permission changed to MEMBER + val member = group[from] + if (member != null && member.permission != MEMBER) { + collect(MemberPermissionChangeEvent(member, member.permission, MEMBER)) + member.permission = MEMBER + } else { + // if member is null, he has already quit the group in another event. + } + + if (to == bot.id) { + // member -> bot + if (group.botPermission != OWNER) { + collect(BotGroupPermissionChangeEvent(group, group.botPermission, OWNER)) + group.botAsMember.permission = OWNER + } + } else { + // member -> member + group[to]?.let { newOwner -> + if (newOwner.permission != OWNER) { + collect(MemberPermissionChangeEvent(newOwner, newOwner.permission, OWNER)) + } + newOwner.permission = OWNER + } + } + } + } + + + // backup, copied from old code + /* + 34 -> { // 主动入群 + + // 回答了问题, 还需要管理员审核 + // msgContent=27 0B 60 E7 01 76 E4 B8 DD 82 00 30 45 41 31 30 35 35 42 44 39 39 42 35 37 46 44 31 41 31 46 36 42 43 42 43 33 43 42 39 34 34 38 31 33 34 42 36 31 46 38 45 43 39 38 38 43 39 37 33 + // msgContent=27 0B 60 E7 01 76 E4 B8 DD 02 00 30 44 44 41 43 44 33 35 43 31 39 34 30 46 42 39 39 34 46 43 32 34 43 39 32 33 39 31 45 42 35 32 33 46 36 30 37 35 42 41 38 42 30 30 37 42 36 42 41 + // 回答正确问题, 直接加入 + + // 27 0B 60 E7 01 76 E4 B8 DD 82 00 30 43 37 37 39 41 38 32 44 38 33 30 35 37 38 31 33 37 45 42 39 35 43 42 45 36 45 43 38 36 34 38 44 34 35 44 42 33 44 45 37 34 41 36 30 33 37 46 45 + // 提交验证消息加入, 需要审核 + + // 被踢了?? + // msgContent=27 0B 60 E7 01 76 E4 B8 DD 83 3E 03 3F A2 06 B4 B4 BD A8 D5 DF 00 30 46 46 32 33 36 39 35 33 31 37 42 44 46 37 43 36 39 34 37 41 45 38 39 43 45 43 42 46 33 41 37 35 39 34 39 45 36 37 33 37 31 41 39 44 33 33 45 33 + + /* + // 搜索后直接加入群 + + soutv 17:43:32 : 33类型的content = 27 0B 60 E7 01 07 6E 47 BA 82 3E 03 3F A2 06 B4 B4 BD A8 D5 DF 00 30 32 30 39 39 42 39 41 46 32 39 41 35 42 33 46 34 32 30 44 36 44 36 39 35 44 38 45 34 35 30 46 30 45 30 38 45 31 41 39 42 46 46 45 32 30 32 34 35 + soutv 17:43:32 : 主动入群content = 2A 3D F5 69 01 35 D7 10 EA 83 4C EF 4F DD 06 B9 DC C0 ED D4 B1 00 30 37 41 39 31 39 34 31 41 30 37 46 38 32 31 39 39 43 34 35 46 39 30 36 31 43 37 39 37 33 39 35 43 34 44 36 31 33 43 31 35 42 37 32 45 46 43 43 36 + */ + + val group = bot.getGroupByUinOrNull(msgHead.fromUin) + group ?: return + + msgBody.msgContent.soutv("主动入群content") + + if (msgBody.msgContent.read { + discardExact(4) // group code + discardExact(1) // 1 + discardExact(4) // requester uin + readByte().toInt().and(0xff) + // 0x02: 回答正确问题直接加入 + // 0x82: 回答了问题, 或者有验证消息, 需要管理员审核 + // 0x83: 回答正确问题直接加入 + } != 0x82) { + + if (group.members.contains(msgHead.authUin)) { + return + } + @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + return MemberJoinEvent.Active(group.newMember(getNewMemberInfo()) + .also { group.members.delegate.addLast(it) }) + } else return + } + */ +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/notice/group/GroupRecallProcessor.kt b/mirai-core/src/commonMain/kotlin/network/notice/group/GroupRecallProcessor.kt new file mode 100644 index 00000000000..24b536a688f --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/notice/group/GroupRecallProcessor.kt @@ -0,0 +1,46 @@ +/* + * 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.network.notice.group + +import net.mamoe.mirai.event.events.MessageRecallEvent +import net.mamoe.mirai.internal.network.components.MixedNoticeProcessor +import net.mamoe.mirai.internal.network.components.NoticePipelineContext +import net.mamoe.mirai.internal.network.notice.decoders.MsgType0x2DC +import net.mamoe.mirai.internal.network.protocol.data.proto.TroopTips0x857 +import net.mamoe.mirai.internal.utils.io.serialization.loadAs +import net.mamoe.mirai.utils.mapToIntArray + +internal class GroupRecallProcessor : MixedNoticeProcessor() { + override suspend fun NoticePipelineContext.processImpl(data: MsgType0x2DC) { + val (kind, group, buf) = data + if (kind != 0x11) return + + val proto = buf.loadAs(TroopTips0x857.NotifyMsgBody.serializer(), 1) + + val recallReminder = proto.optMsgRecall ?: return + val operator = group[recallReminder.uin] ?: return + markAsConsumed() + for (firstPkg in recallReminder.recalledMsgList) { + if (firstPkg.authorUin == bot.id && operator.id == bot.id) continue // already broadcast + val author = group[firstPkg.authorUin] ?: continue + + collected += MessageRecallEvent.GroupRecall( + bot = bot, + authorId = firstPkg.authorUin, + messageIds = recallReminder.recalledMsgList.mapToIntArray { it.seq }, + messageInternalIds = recallReminder.recalledMsgList.mapToIntArray { it.msgRandom }, + messageTime = firstPkg.time, + operator = operator, + group = group, + author = author, + ) + } + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/notice/priv/FriendNoticeProcessor.kt b/mirai-core/src/commonMain/kotlin/network/notice/priv/FriendNoticeProcessor.kt new file mode 100644 index 00000000000..1b97351af7b --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/notice/priv/FriendNoticeProcessor.kt @@ -0,0 +1,291 @@ +/* + * 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.network.notice.priv + +import kotlinx.io.core.discardExact +import kotlinx.io.core.readUByte +import kotlinx.io.core.readUShort +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber +import net.mamoe.mirai.contact.User +import net.mamoe.mirai.event.events.* +import net.mamoe.mirai.internal.contact.impl +import net.mamoe.mirai.internal.contact.info.FriendInfoImpl +import net.mamoe.mirai.internal.contact.info.StrangerInfoImpl +import net.mamoe.mirai.internal.contact.toMiraiFriendInfo +import net.mamoe.mirai.internal.network.components.MixedNoticeProcessor +import net.mamoe.mirai.internal.network.components.NoticePipelineContext +import net.mamoe.mirai.internal.network.components.NoticePipelineContext.Companion.msgInfo +import net.mamoe.mirai.internal.network.notice.NewContactSupport +import net.mamoe.mirai.internal.network.notice.group.get +import net.mamoe.mirai.internal.network.protocol.data.jce.MsgType0x210 +import net.mamoe.mirai.internal.network.protocol.data.proto.FrdSysMsg +import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm +import net.mamoe.mirai.internal.network.protocol.data.proto.Submsgtype0x115.SubMsgType0x115 +import net.mamoe.mirai.internal.network.protocol.data.proto.Submsgtype0x122 +import net.mamoe.mirai.internal.network.protocol.data.proto.Submsgtype0x27.SubMsgType0x27.* +import net.mamoe.mirai.internal.network.protocol.data.proto.Submsgtype0x44.Submsgtype0x44 +import net.mamoe.mirai.internal.network.protocol.data.proto.Submsgtype0xb3.SubMsgType0xb3 +import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList.GetFriendGroupList +import net.mamoe.mirai.internal.network.protocol.packet.sendAndExpect +import net.mamoe.mirai.internal.utils._miraiContentToString +import net.mamoe.mirai.internal.utils.io.ProtoBuf +import net.mamoe.mirai.internal.utils.io.serialization.loadAs +import net.mamoe.mirai.utils.* + +/** + * All [FriendEvent] except [FriendMessageEvent] + * + * @see FriendInputStatusChangedEvent + * @see FriendAddEvent + * @see StrangerRelationChangeEvent.Friended + */ +internal class FriendNoticeProcessor( + private val logger: MiraiLogger, +) : MixedNoticeProcessor(), NewContactSupport { + override suspend fun NoticePipelineContext.processImpl(data: MsgComm.Msg) = data.context { + if (msgHead.msgType != 191) return + + var fromGroup = 0L + var pbNick = "" + msgBody.msgContent.read { + readUByte() // version + discardExact(readUByte().toInt()) //skip + readUShort() //source id + readUShort() //SourceSubID + discardExact(readUShort().toLong()) //skip size + if (readUShort().toInt() != 0) { //hasExtraInfo + discardExact(readUShort().toInt()) //mail address info, skip + } + discardExact(4 + readUShort().toInt()) //skip + for (i in 1..readUByte().toInt()) { //pb size + val type = readUShort().toInt() + val pbArray = ByteArray(readUShort().toInt() and 0xFF) + readAvailable(pbArray) + when (type) { + 1000 -> pbArray.loadAs(FrdSysMsg.GroupInfo.serializer()).let { fromGroup = it.groupUin } + 1002 -> pbArray.loadAs(FrdSysMsg.FriendMiscInfo.serializer()) + .let { pbNick = it.fromuinNick } + else -> { + } //ignore + } + } + } + + msgHead.context { + if (fromUin == authUin) { + logger.error { "Could not determine uin since `fromUin` = `authUin` = $fromUin" } + return + } + val id = fromUin or authUin // 对方 qq + if (bot.getStranger(id) != null) return + + val nick = fromNick.ifEmpty { authNick }.ifEmpty { pbNick } + collect(StrangerAddEvent(bot.addNewStranger(StrangerInfoImpl(id, nick, fromGroup)) ?: return)) + } + + } + + override suspend fun NoticePipelineContext.processImpl(data: MsgType0x210) = data.context { + markAsConsumed() + when (data.uSubMsgType) { + 0xB3L -> { + // 08 01 12 52 08 A2 FF 8C F0 03 10 00 1D 15 3D 90 5E 22 2E E6 88 91 E4 BB AC E5 B7 B2 E7 BB 8F E6 98 AF E5 A5 BD E5 8F 8B E5 95 A6 EF BC 8C E4 B8 80 E8 B5 B7 E6 9D A5 E8 81 8A E5 A4 A9 E5 90 A7 21 2A 09 48 69 6D 31 38 38 6D 6F 65 30 07 38 03 48 DD F1 92 B7 07 + val body: SubMsgType0xb3.MsgBody = vProtobuf.loadAs(SubMsgType0xb3.MsgBody.serializer()) + handleFriendAddedB(data, body) + } + 0x44L -> { + val body = vProtobuf.loadAs(Submsgtype0x44.MsgBody.serializer()) + handleFriendAddedA(body) + } + 0x27L -> { + val body = vProtobuf.loadAs(SubMsgType0x27MsgBody.serializer()) + for (msgModInfo in body.msgModInfos) { + when { + msgModInfo.msgModFriendRemark != null -> handleRemarkChanged(msgModInfo.msgModFriendRemark) + msgModInfo.msgDelFriend != null -> handleFriendDeleted(msgModInfo.msgDelFriend) + msgModInfo.msgModCustomFace != null -> handleAvatarChanged(msgModInfo.msgModCustomFace) + msgModInfo.msgModProfile != null -> handleProfileChanged(msgModInfo.msgModProfile) + } + } + } + 0x115L -> { + val body = vProtobuf.loadAs(SubMsgType0x115.MsgBody.serializer()) + handleInputStatusChanged(body) + } + 0x122L -> { + val body = vProtobuf.loadAs(Submsgtype0x122.Submsgtype0x122.MsgBody.serializer()) + when (body.templId) { + //戳一戳 + 1132L, 1133L, 1134L, 1135L, 1136L, 10043L -> handlePrivateNudge(body) + } + } + 0x8AL -> { + val body = vProtobuf.loadAs(Sub8A.serializer()) + processFriendRecall(body) + } + else -> markNotConsumed() + } + } + + + @Serializable + private class Wording( + @ProtoNumber(1) val itemID: Int = 0, + @ProtoNumber(2) val itemName: String = "", + ) : ProtoBuf + + @Serializable + private class Sub8AMsgInfo( + @ProtoNumber(1) val fromUin: Long, + @ProtoNumber(2) val botUin: Long, + @ProtoNumber(3) val srcId: Int, + @ProtoNumber(4) val srcInternalId: Long, + @ProtoNumber(5) val time: Long, + @ProtoNumber(6) val random: Int, + @ProtoNumber(7) val pkgNum: Int, // 1 + @ProtoNumber(8) val pkgIndex: Int, // 0 + @ProtoNumber(9) val devSeq: Int, // 0 + @ProtoNumber(12) val flag: Int, // 1 + @ProtoNumber(13) val wording: Wording, + ) : ProtoBuf + + @Serializable + private class Sub8A( + @ProtoNumber(1) val msgInfo: List<Sub8AMsgInfo>, + @ProtoNumber(2) val appId: Int, // 1 + @ProtoNumber(3) val instId: Int, // 1 + @ProtoNumber(4) val longMessageFlag: Int, // 0 + @ProtoNumber(5) val reserved: ByteArray? = null, // struct{ boolean(1), boolean(2) } + ) : ProtoBuf + + private fun NoticePipelineContext.processFriendRecall(body: Sub8A) { + for (info in body.msgInfo) { + if (info.botUin != bot.id) continue + collected += MessageRecallEvent.FriendRecall( + bot = bot, + messageIds = intArrayOf(info.srcId), + messageInternalIds = intArrayOf(info.srcInternalId.toInt()), + messageTime = info.time.toInt(), + operatorId = info.fromUin, + operator = bot.getFriend(info.fromUin) ?: continue, + ) + } + } + + + private fun NoticePipelineContext.handleInputStatusChanged(body: SubMsgType0x115.MsgBody) { + val friend = bot.getFriend(body.fromUin) ?: return + val item = body.msgNotifyItem ?: return + collect(FriendInputStatusChangedEvent(friend, item.eventType == 1)) + } + + private fun NoticePipelineContext.handleProfileChanged(body: ModProfile) { + var containsUnknown = false + for (profileInfo in body.msgProfileInfos) { + when (profileInfo.field) { + 20002 -> { // 昵称修改 + val to = profileInfo.value + if (body.uin == bot.id) { + val from = bot.nick + if (from == to) continue + collect(BotNickChangedEvent(bot, from, to)) + bot.nick = to + } else { + val friend = bot.getFriend(body.uin)?.impl() ?: continue + val from = bot.nick + if (from == to) continue + collect(FriendNickChangedEvent(friend, from, to)) + friend.info.nick = to + } + } + else -> containsUnknown = true + } + } + if (body.msgProfileInfos.isEmpty() || containsUnknown) { + logger.debug { "Transformers528 0x27L: ProfileChanged new data: ${body._miraiContentToString()}" } + } + } + + private fun NoticePipelineContext.handleRemarkChanged(body: ModFriendRemark) { + for (new in body.msgFrdRmk) { + val friend = bot.getFriend(new.fuin)?.impl() ?: continue + + collect(FriendRemarkChangeEvent(friend, friend.remark, new.rmkName)) + friend.info.remark = new.rmkName + } + } + + private fun NoticePipelineContext.handleAvatarChanged(body: ModCustomFace) { + if (body.uin == bot.id) { + collect(BotAvatarChangedEvent(bot)) + } else { + collect(FriendAvatarChangedEvent(bot.getFriend(body.uin) ?: return)) + } + } + + private fun NoticePipelineContext.handleFriendDeleted(body: DelFriend) { + for (id in body.uint64Uins) { + collect(FriendDeleteEvent(bot.removeFriend(id) ?: continue)) + } + } + + private suspend fun NoticePipelineContext.handleFriendAddedA( + body: Submsgtype0x44.MsgBody, + ) = body.msgFriendMsgSync.context { + if (this == null) return + + when (processtype) { + 3, 9, 10 -> { + if (bot.getFriend(fuin) != null) return + + val response = GetFriendGroupList.forSingleFriend(bot.client, fuin).sendAndExpect(bot) + val info = response.friendList.firstOrNull() ?: return + collect( + FriendAddEvent(bot.addNewFriendAndRemoveStranger(info.toMiraiFriendInfo()) ?: return), + ) + + } + } + } + + private fun NoticePipelineContext.handleFriendAddedB(data: MsgType0x210, body: SubMsgType0xb3.MsgBody) = + data.context { + val info = FriendInfoImpl( + uin = body.msgAddFrdNotify.fuin, + nick = body.msgAddFrdNotify.fuinNick, + remark = "", + ) + + val removed = bot.removeStranger(info.uin) + val added = bot.addNewFriendAndRemoveStranger(info) ?: return + collect(FriendAddEvent(added)) + if (removed != null) collect(StrangerRelationChangeEvent.Friended(removed, added)) + } + + private fun NoticePipelineContext.handlePrivateNudge(body: Submsgtype0x122.Submsgtype0x122.MsgBody) { + val action = body.msgTemplParam["action_str"].orEmpty() + val from = body.msgTemplParam["uin_str1"]?.findFriendOrStranger() ?: bot.asFriend + val target = body.msgTemplParam["uin_str2"]?.findFriendOrStranger() ?: bot.asFriend + val suffix = body.msgTemplParam["suffix_str"].orEmpty() + + val subject: User = bot.getFriend(msgInfo.lFromUin) + ?: bot.getStranger(msgInfo.lFromUin) + ?: return + + collected += NudgeEvent( + from = if (from.id == bot.id) bot else from, + target = if (target.id == bot.id) bot else target, + action = action, + suffix = suffix, + subject = subject, + ) + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/notice/priv/OtherClientNoticeProcessor.kt b/mirai-core/src/commonMain/kotlin/network/notice/priv/OtherClientNoticeProcessor.kt new file mode 100644 index 00000000000..b470ce1e93b --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/notice/priv/OtherClientNoticeProcessor.kt @@ -0,0 +1,141 @@ +/* + * 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.network.notice.priv + +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.sync.withLock +import net.mamoe.mirai.Mirai +import net.mamoe.mirai.contact.ClientKind +import net.mamoe.mirai.contact.OtherClientInfo +import net.mamoe.mirai.contact.Platform +import net.mamoe.mirai.event.events.OtherClientMessageEvent +import net.mamoe.mirai.event.events.OtherClientOfflineEvent +import net.mamoe.mirai.event.events.OtherClientOnlineEvent +import net.mamoe.mirai.internal.contact.appId +import net.mamoe.mirai.internal.contact.createOtherClient +import net.mamoe.mirai.internal.message.OnlineMessageSourceFromFriendImpl +import net.mamoe.mirai.internal.message.contextualBugReportException +import net.mamoe.mirai.internal.network.components.ContactUpdater +import net.mamoe.mirai.internal.network.components.MixedNoticeProcessor +import net.mamoe.mirai.internal.network.components.NoticePipelineContext +import net.mamoe.mirai.internal.network.handler.logger +import net.mamoe.mirai.internal.network.protocol.data.jce.RequestPushStatus +import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm +import net.mamoe.mirai.internal.network.protocol.data.proto.SubMsgType0x7 +import net.mamoe.mirai.internal.utils._miraiContentToString +import net.mamoe.mirai.internal.utils.io.serialization.loadAs +import net.mamoe.mirai.message.data.PlainText +import net.mamoe.mirai.message.data.buildMessageChain +import net.mamoe.mirai.utils.context + +/** + * @see OtherClientOnlineEvent + * @see OtherClientOfflineEvent + * + * @see OtherClientMessageEvent + */ +internal class OtherClientNoticeProcessor : MixedNoticeProcessor() { + /** + * @see OtherClientOnlineEvent + * @see OtherClientOfflineEvent + */ + override suspend fun NoticePipelineContext.processImpl(data: RequestPushStatus) { + markAsConsumed() + bot.components[ContactUpdater].otherClientsLock.withLock { + val instanceInfo = data.vecInstanceList?.firstOrNull() + val appId = instanceInfo?.iAppId ?: 1 + when (data.status.toInt()) { + 1 -> { // online + if (bot.otherClients.any { appId == it.appId }) return + + suspend fun tryFindInQuery(): OtherClientInfo? { + return Mirai.getOnlineOtherClientsList(bot).find { it.appId == appId } + ?: kotlin.run { + delay(2000) // sometimes server sync slow + Mirai.getOnlineOtherClientsList(bot).find { it.appId == appId } + } + } + + val info = + tryFindInQuery() ?: kotlin.run { + bot.network.logger.warning( + contextualBugReportException( + "SvcRequestPushStatus (OtherClient online)", + "packet: \n" + data._miraiContentToString() + + "\n\nquery: \n" + + Mirai.getOnlineOtherClientsList(bot)._miraiContentToString(), + additional = "Failed to find corresponding instanceInfo.", + ), + ) + OtherClientInfo(appId, Platform.WINDOWS, "", "电脑") + } + + val client = bot.createOtherClient(info) + bot.otherClients.delegate.add(client) + collected += OtherClientOnlineEvent( + client, + ClientKind[data.nClientType?.toInt() ?: 0], + ) + } + + 2 -> { // off + val client = bot.otherClients.find { it.appId == appId } ?: return + client.cancel(CancellationException("Offline")) + bot.otherClients.delegate.remove(client) + collected += OtherClientOfflineEvent(client) + } + + else -> markNotConsumed() + } + } + } + + + /** + * @see OtherClientMessageEvent + */ + override suspend fun NoticePipelineContext.processImpl(data: MsgComm.Msg) = data.context { + if (msgHead.msgType != 529) return + + // top_package/awbk.java:3765 + markAsConsumed() // todo check + if (msgHead.c2cCmd != 7) { + // 各种垃圾 + // 08 04 12 1E 08 E9 07 10 B7 F7 8B 80 02 18 E9 07 20 00 28 DD F1 92 B7 07 30 DD F1 92 B7 07 48 02 50 03 32 1E 08 88 80 F8 92 CD 84 80 80 10 10 01 18 00 20 01 2A 0C 0A 0A 08 01 12 06 E5 95 8A E5 95 8A + return + } + val body = msgBody.msgContent.loadAs(SubMsgType0x7.MsgBody.serializer()) + + val textMsg = + body.msgSubcmd0x4Generic?.buf?.loadAs(SubMsgType0x7.MsgBody.QQDataTextMsg.serializer()) + ?: return + + with(body.msgHeader ?: return) { + if (dstUin != bot.id) return + val client = bot.otherClients.find { it.appId == srcInstId } + ?: return // don't compare with dstAppId. diff. + + val chain = buildMessageChain { + +OnlineMessageSourceFromFriendImpl(bot, listOf(data)) + for (msgItem in textMsg.msgItems) { + when (msgItem.type) { + 1 -> +PlainText(msgItem.text) + else -> { + } + } + } + } + + collect(OtherClientMessageEvent(client, chain, msgHead.msgTime)) + } + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/notice/priv/PrivateMessageProcessor.kt b/mirai-core/src/commonMain/kotlin/network/notice/priv/PrivateMessageProcessor.kt new file mode 100644 index 00000000000..deb3f571098 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/notice/priv/PrivateMessageProcessor.kt @@ -0,0 +1,99 @@ +/* + * 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.network.notice.priv + +import net.mamoe.mirai.event.events.* +import net.mamoe.mirai.internal.contact.* +import net.mamoe.mirai.internal.getGroupByUinOrCode +import net.mamoe.mirai.internal.message.toMessageChainOnline +import net.mamoe.mirai.internal.network.components.NoticePipelineContext +import net.mamoe.mirai.internal.network.components.NoticePipelineContext.Companion.fromSync +import net.mamoe.mirai.internal.network.components.SimpleNoticeProcessor +import net.mamoe.mirai.internal.network.components.SsoProcessor +import net.mamoe.mirai.internal.network.notice.group.GroupMessageProcessor +import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm +import net.mamoe.mirai.utils.assertUnreachable +import net.mamoe.mirai.utils.context + +/** + * Handles [UserMessageEvent] and their sync events. For [GroupMessageEvent], see [GroupMessageProcessor] + * + * @see StrangerMessageEvent + * @see StrangerMessageSyncEvent + * + * @see FriendMessageEvent + * @see FriendMessageSyncEvent + * + * @see GroupTempMessageEvent + * @see GroupTempMessageSyncEvent + */ +internal class PrivateMessageProcessor : SimpleNoticeProcessor<MsgComm.Msg>(type()) { + override suspend fun NoticePipelineContext.processImpl(data: MsgComm.Msg) = data.context { + markAsConsumed() + if (msgHead.fromUin == bot.id && fromSync) { + // Bot send message to himself? or from other client? I am not the implementer. + bot.client.sendFriendMessageSeq.updateIfSmallerThan(msgHead.msgSeq) + return + } + if (!bot.components[SsoProcessor].firstLoginSucceed) return + val senderUin = if (fromSync) msgHead.toUin else msgHead.fromUin + when (msgHead.msgType) { + 166, 167, // 单向好友 + 208, // friend ptt, maybe also support stranger + -> { + handlePrivateMessage( + data, + bot.getFriend(senderUin)?.impl() + ?: bot.getStranger(senderUin)?.impl() + ?: return + ) + } + + 141, // group temp + -> { + val tmpHead = msgHead.c2cTmpMsgHead ?: return + val group = bot.getGroupByUinOrCode(tmpHead.groupUin) ?: return + handlePrivateMessage(data, group[senderUin] ?: return) + } + else -> markNotConsumed() + } + + } + + private suspend fun NoticePipelineContext.handlePrivateMessage( + data: MsgComm.Msg, + user: AbstractUser, + ) = data.context { + if (!user.messageSeq.updateIfDifferentWith(msgHead.msgSeq)) return + if (contentHead?.autoReply == 1) return + + val msgs = user.fragmentedMessageMerger.tryMerge(this) + if (msgs.isEmpty()) return + + val chain = msgs.toMessageChainOnline(bot, 0, user.correspondingMessageSourceKind) + val time = msgHead.msgTime + + collected += if (fromSync) { + when (user) { + is FriendImpl -> FriendMessageSyncEvent(user, chain, time) + is StrangerImpl -> StrangerMessageSyncEvent(user, chain, time) + is NormalMemberImpl -> GroupTempMessageSyncEvent(user, chain, time) + is AnonymousMemberImpl -> assertUnreachable() + } + } else { + when (user) { + is FriendImpl -> FriendMessageEvent(user, chain, time) + is StrangerImpl -> StrangerMessageEvent(user, chain, time) + is NormalMemberImpl -> GroupTempMessageEvent(user, chain, time) + is AnonymousMemberImpl -> assertUnreachable() + } + } + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/jce/PushNotifyPack.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/jce/PushNotifyPack.kt index 12ddbff037f..3d45060cd6e 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/data/jce/PushNotifyPack.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/jce/PushNotifyPack.kt @@ -12,6 +12,10 @@ package net.mamoe.mirai.internal.network.protocol.data.jce import kotlinx.serialization.Serializable import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.internal.utils.io.JceStruct +import net.mamoe.mirai.internal.utils.io.NestedStructure +import net.mamoe.mirai.internal.utils.io.NestedStructureDesensitizer +import net.mamoe.mirai.internal.utils.io.ProtocolStruct +import net.mamoe.mirai.internal.utils.io.serialization.loadAs import net.mamoe.mirai.internal.utils.io.serialization.tars.TarsId import net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY @@ -46,6 +50,7 @@ internal class MsgInfo( @TarsId(3) @JvmField val shMsgSeq: Short, @TarsId(4) @JvmField val strMsg: String?, @TarsId(5) @JvmField val uRealMsgTime: Int?, + @param:NestedStructure(VMsgDesensitizationSerializer::class) @TarsId(6) @JvmField val vMsg: ByteArray, @TarsId(7) @JvmField val uAppShareID: Long?, @TarsId(8) @JvmField val vMsgCookies: ByteArray? = EMPTY_BYTE_ARRAY, @@ -62,6 +67,15 @@ internal class MsgInfo( //@SerialId(19) @JvmField val stC2CTmpMsgHead: TempMsgHead? ) : JceStruct +internal object VMsgDesensitizationSerializer : NestedStructureDesensitizer<MsgInfo, ProtocolStruct> { + override fun deserialize(context: MsgInfo, byteArray: ByteArray): ProtocolStruct? { + return when (context.shMsgType.toUShort().toInt()) { + 0x210 -> byteArray.loadAs(MsgType0x210.serializer()) + else -> null + } + } +} + @Serializable internal class ShareData( diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/jce/ReqPushStatus.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/jce/ReqPushStatus.kt index 3ccac2f0265..145cefb7d32 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/data/jce/ReqPushStatus.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/jce/ReqPushStatus.kt @@ -10,6 +10,7 @@ package net.mamoe.mirai.internal.network.protocol.data.jce import kotlinx.serialization.Serializable +import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.internal.utils.io.JceStruct import net.mamoe.mirai.internal.utils.io.serialization.tars.TarsId @@ -25,6 +26,5 @@ internal class RequestPushStatus( @JvmField @TarsId(6) val nClientType: Long? = null, @JvmField @TarsId(7) val nInstanceId: Long? = null, @JvmField @TarsId(8) val vecInstanceList: List<InstanceInfo>? = null, -) : JceStruct - +) : JceStruct, Packet diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Cmd0x857.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Cmd0x857.kt index 33859b3fb45..666566b7d18 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Cmd0x857.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Cmd0x857.kt @@ -320,8 +320,8 @@ internal class TroopTips0x857 : ProtoBuf { @Serializable internal class TemplParam( - @ProtoNumber(1) @JvmField val name: ByteArray = EMPTY_BYTE_ARRAY, - @ProtoNumber(2) @JvmField val value: ByteArray = EMPTY_BYTE_ARRAY, + @ProtoNumber(1) @JvmField val name: String = "", + @ProtoNumber(2) @JvmField val value: String = "", ) : ProtoBuf @Serializable diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Msg.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Msg.kt index 3cbd886164c..744dd1ff734 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Msg.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Msg.kt @@ -414,7 +414,7 @@ internal class ImMsgBody : ProtoBuf { @Serializable internal class ExtraInfo( - @ProtoNumber(1) @JvmField val nick: ByteArray = EMPTY_BYTE_ARRAY, + @ProtoNumber(1) @JvmField val nick: String = "", @ProtoNumber(2) @JvmField val groupCard: ByteArray = EMPTY_BYTE_ARRAY, @ProtoNumber(3) @JvmField val level: Int = 0, @ProtoNumber(4) @JvmField val flags: Int = 0, diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/msgType0x210.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/msgType0x210.kt index 8e53e991208..f35d3d29a5e 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/msgType0x210.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/msgType0x210.kt @@ -408,8 +408,8 @@ internal class Submsgtype0x122 { @Serializable internal class TemplParam( - @ProtoNumber(1) @JvmField val name: ByteArray = EMPTY_BYTE_ARRAY, - @ProtoNumber(2) @JvmField val value: ByteArray = EMPTY_BYTE_ARRAY, + @ProtoNumber(1) @JvmField val name: String = "", + @ProtoNumber(2) @JvmField val value: String = "", ) : ProtoBuf } } @@ -952,7 +952,7 @@ internal class Submsgtype0x27 { @Serializable internal class ProfileInfo( @ProtoNumber(1) @JvmField val field: Int = 0, - @ProtoNumber(2) @JvmField val value: ByteArray = EMPTY_BYTE_ARRAY, + @ProtoNumber(2) @JvmField val value: String = "", ) : ProtoBuf @Serializable diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/OutgoingPacketAndroid.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/OutgoingPacketAndroid.kt index 62b86398220..386be4637d3 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/OutgoingPacketAndroid.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/OutgoingPacketAndroid.kt @@ -146,7 +146,7 @@ internal inline fun <R : Packet?> IncomingPacketFactory<R>.buildResponseUniPacke key: ByteArray = client.wLoginSigInfo.d2Key, extraData: ByteReadPacket = BRP_STUB, sequenceId: Int = client.nextSsoSequenceId(), - body: BytePacketBuilder.(sequenceId: Int) -> Unit + body: BytePacketBuilder.(sequenceId: Int) -> Unit = {} ): OutgoingPacketWithRespType<R> { @Suppress("DuplicatedCode") return OutgoingPacketWithRespType(name, commandName, sequenceId, buildPacket { diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/NewContact.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/NewContact.kt index 9eeac2b45cf..a8fd824b1d4 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/NewContact.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/NewContact.kt @@ -13,28 +13,25 @@ package net.mamoe.mirai.internal.network.protocol.packet.chat import kotlinx.io.core.ByteReadPacket import kotlinx.io.core.readBytes -import net.mamoe.mirai.event.events.BotInvitedJoinGroupRequestEvent -import net.mamoe.mirai.event.events.BotLeaveEvent -import net.mamoe.mirai.event.events.MemberJoinRequestEvent import net.mamoe.mirai.event.events.NewFriendRequestEvent import net.mamoe.mirai.internal.QQAndroidBot -import net.mamoe.mirai.internal.message.contextualBugReportException -import net.mamoe.mirai.internal.network.MultiPacketByIterable import net.mamoe.mirai.internal.network.Packet -import net.mamoe.mirai.internal.network.ParseErrorPacket import net.mamoe.mirai.internal.network.QQAndroidClient +import net.mamoe.mirai.internal.network.components.NoticeProcessorPipeline.Companion.processPacketThroughPipeline +import net.mamoe.mirai.internal.network.components.SyncController.Companion.syncController import net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketFactory import net.mamoe.mirai.internal.network.protocol.packet.buildOutgoingUniPacket -import net.mamoe.mirai.internal.utils._miraiContentToString +import net.mamoe.mirai.internal.network.toPacket import net.mamoe.mirai.internal.utils.io.serialization.loadAs +import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf import net.mamoe.mirai.internal.utils.io.serialization.writeProtoBuf import kotlin.math.max internal class NewContact { internal object SystemMsgNewFriend : - OutgoingPacketFactory<Packet?>("ProfileService.Pb.ReqSystemMsgNew.Friend") { + OutgoingPacketFactory<Packet>("ProfileService.Pb.ReqSystemMsgNew.Friend") { operator fun invoke(client: QQAndroidClient) = buildOutgoingUniPacket(client) { writeProtoBuf( @@ -46,30 +43,24 @@ internal class NewContact { frdMsgGetBusiCard = 1, frdMsgNeedWaitingMsg = 1, frdMsgUint32NeedAllUnreadMsg = 1, - grpMsgMaskInviteAutoJoin = 1 + grpMsgMaskInviteAutoJoin = 1, ), friendMsgTypeFlag = 1, isGetFrdRibbon = false, isGetGrpRibbon = false, msgNum = 20, - version = 1000 - ) + version = 1000, + ), ) } - override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Packet? { - readBytes().loadAs(Structmsg.RspSystemMsgNew.serializer()).run { + override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Packet { + readProtoBuf(Structmsg.RspSystemMsgNew.serializer()).run { return friendmsgs.filter { - it.msgTime >= bot.client.syncingController.latestMsgNewFriendTime + it.msgTime >= bot.syncController.latestMsgNewFriendTime }.mapNotNull { struct -> - if (!bot.client.syncingController.systemMsgNewFriendCacheList.addCache( - QQAndroidClient.MessageSvcSyncData.SystemMsgNewSyncId( - struct.msgSeq, - struct.msgTime - ) - ) - ) { // duplicate + if (!bot.syncController.syncNewFriend(struct.msgSeq, struct.msgTime)) { // duplicate return@mapNotNull null } struct.msg?.run { @@ -79,17 +70,11 @@ internal class NewContact { msgAdditional, struct.reqUin, groupCode, - reqUinNick + reqUinNick, ) } - }.let { packets -> - when { - packets.isEmpty() -> null - packets.size == 1 -> packets[0] - else -> MultiPacketByIterable(packets) - } - }.also { - bot.client.syncingController.run { + }.toPacket().also { + bot.syncController.run { latestMsgNewFriendTime = max(latestMsgNewFriendTime, friendmsgs.maxOfOrNull { it.msgTime } ?: 0) } } @@ -103,7 +88,7 @@ internal class NewContact { eventId: Long, fromId: Long, accept: Boolean, - blackList: Boolean = false + blackList: Boolean = false, ) = buildOutgoingUniPacket(client) { writeProtoBuf( @@ -114,14 +99,14 @@ internal class NewContact { addFrdSNInfo = Structmsg.AddFrdSNInfo(), msg = "", remark = "", - blacklist = !accept && blackList + blacklist = !accept && blackList, ), msgSeq = eventId, reqUin = fromId, srcId = 6, subSrcId = 7, - subType = 1 - ) + subType = 1, + ), ) } @@ -155,125 +140,29 @@ internal class NewContact { grpMsgNeedAutoAdminWording = 1, grpMsgNotAllowJoinGrpInviteNotFrd = 1, grpMsgSupportInviteAutoJoin = 1, - grpMsgWordingDown = 1 + grpMsgWordingDown = 1, ), friendMsgTypeFlag = 1, isGetFrdRibbon = false, isGetGrpRibbon = false, msgNum = 5, - version = 1000 - ) + version = 1000, + ), ) } - override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Packet? { - fun handleStruct(struct: Structmsg.StructMsg): Packet? { - return struct.msg?.run { - when (subType) { - 1 -> { // 处理被邀请入群 或 处理成员入群申请 - when (groupMsgType) { - 1 -> { - // 成员申请入群 - MemberJoinRequestEvent( - bot, struct.msgSeq, msgAdditional, - struct.reqUin, groupCode, groupName, reqUinNick - ) - } - 2 -> { - // Bot 被邀请入群 - BotInvitedJoinGroupRequestEvent( - bot, struct.msgSeq, actionUin, - groupCode, groupName, actionUinNick - ) - } - 22 -> { - // 成员邀请入群 - MemberJoinRequestEvent( - bot, struct.msgSeq, msgAdditional, - struct.reqUin, groupCode, groupName, reqUinNick, actionUin - ) - } - else -> throw contextualBugReportException( - "parse SystemMsgNewGroup, subType=1", - this._miraiContentToString(), - additional = "并尽量描述此时机器人是否正被邀请加入群, 或者是有有新群员加入此群" - ) - } - } - 2 -> { // 被邀请入群, 自动同意, 不需处理 - -// val group = bot.getNewGroup(groupCode) ?: return null -// val invitor = group[actionUin] -// -// BotJoinGroupEvent.Invite(invitor) - null - } - 3 -> { // 已被请他管理员处理 - null - } - 5 -> { - val group = bot.getGroup(groupCode) ?: return null - when (groupMsgType) { - 3 -> { - // https://github.com/mamoe/mirai/issues/651 - // msgDescribe=将你设置为管理员 - // msgTitle=管理员设置 - null - } - 13 -> { // 成员主动退出, 机器人是管理员, 接到通知 - // 但无法获取是哪个成员. - null - } - 7 -> { // 机器人被踢 - val operator = group[actionUin] ?: return null - BotLeaveEvent.Kick(operator) - } - else -> { - throw contextualBugReportException( - "解析 NewContact.SystemMsgNewGroup, subType=5, groupMsgType=$groupMsgType", - this._miraiContentToString(), - null, - "并描述此时机器人是否被踢出群等" - ) - } - } - } - else -> throw contextualBugReportException( - "解析 NewContact.SystemMsgNewGroup, subType=$subType, groupMsgType=$groupMsgType", - forDebug = this._miraiContentToString(), - additional = "并尽量描述此时机器人是否正被邀请加入群, 或者是有有新群员加入此群" - ) - } - } - } - + override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Packet { return readBytes().loadAs(Structmsg.RspSystemMsgNew.serializer()).run { groupmsgs.filter { - it.msgTime >= bot.client.syncingController.latestMsgNewGroupTime + it.msgTime >= bot.syncController.latestMsgNewGroupTime }.mapNotNull { struct -> - if (!bot.client.syncingController.systemMsgNewGroupCacheList.addCache( - QQAndroidClient.MessageSvcSyncData.SystemMsgNewSyncId( - struct.msgSeq, - struct.msgTime - ) - ) - ) { // duplicate + if (!bot.syncController.syncNewGroup(struct.msgSeq, struct.msgTime)) { // duplicate return@mapNotNull null } - try { - handleStruct(struct) - } catch (e: Throwable) { - ParseErrorPacket(e) - } - }.let { packets -> - when { - packets.isEmpty() -> null - packets.size == 1 -> packets[0] - else -> MultiPacketByIterable(packets) - } - }.also { - bot.client.syncingController.run { + bot.processPacketThroughPipeline(struct) + }.toPacket().also { + bot.syncController.run { latestMsgNewGroupTime = max(latestMsgNewGroupTime, groupmsgs.maxOfOrNull { it.msgTime } ?: 0) } } @@ -290,7 +179,7 @@ internal class NewContact { isInvited: Boolean, accept: Boolean?, blackList: Boolean = false, - message: String = "" + message: String = "", ) = buildOutgoingUniPacket(client) { writeProtoBuf( @@ -305,7 +194,7 @@ internal class NewContact { groupCode = groupId, msg = message, remark = "", - blacklist = blackList + blacklist = blackList, ), groupMsgType = if (isInvited) 2 else 1, language = 1000, @@ -313,8 +202,8 @@ internal class NewContact { reqUin = fromId, srcId = 3, subSrcId = if (isInvited) 10016 else 31, - subType = 1 - ) + subType = 1, + ), ) } diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbDeleteMsg.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbDeleteMsg.kt index 2f7c2aaecd9..21a7128c70a 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbDeleteMsg.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbDeleteMsg.kt @@ -9,9 +9,6 @@ package net.mamoe.mirai.internal.network.protocol.packet.chat.receive -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.toList import kotlinx.io.core.ByteReadPacket import net.mamoe.mirai.internal.QQAndroidBot import net.mamoe.mirai.internal.network.QQAndroidClient @@ -34,9 +31,8 @@ internal object MessageSvcPbDeleteMsg : OutgoingPacketFactory<Nothing?>("Message ) } - internal suspend fun delete(bot: QQAndroidBot, messages: Flow<MsgComm.Msg>) = + internal suspend fun delete(bot: QQAndroidBot, messages: List<MsgComm.Msg>) = bot.network.run { - val map = messages.map { MsgSvc.PbDeleteMsgReq.MsgItem( fromUin = it.msgHead.fromUin, @@ -44,9 +40,9 @@ internal object MessageSvcPbDeleteMsg : OutgoingPacketFactory<Nothing?>("Message // 群为84、好友为187。群通过其他方法删除,但测试结果显示通过187也能删除群消息。 msgType = 187, msgSeq = it.msgHead.msgSeq, - msgUid = it.msgHead.msgUid + msgUid = it.msgHead.msgUid, ) - }.toList() + } MessageSvcPbDeleteMsg(bot.client, map).sendWithoutExpect() } diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbGetMsg.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbGetMsg.kt index 71323cf461b..a47cf93ebdb 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbGetMsg.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbGetMsg.kt @@ -12,51 +12,27 @@ package net.mamoe.mirai.internal.network.protocol.packet.chat.receive import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.withLock -import kotlinx.io.core.* +import kotlinx.io.core.ByteReadPacket import net.mamoe.mirai.Bot -import net.mamoe.mirai.Mirai -import net.mamoe.mirai.contact.Group -import net.mamoe.mirai.contact.MemberPermission -import net.mamoe.mirai.contact.NormalMember -import net.mamoe.mirai.data.MemberInfo import net.mamoe.mirai.event.AbstractEvent -import net.mamoe.mirai.event.broadcast -import net.mamoe.mirai.event.events.* +import net.mamoe.mirai.event.events.BotEvent import net.mamoe.mirai.internal.QQAndroidBot -import net.mamoe.mirai.internal.contact.* -import net.mamoe.mirai.internal.contact.info.GroupInfoImpl -import net.mamoe.mirai.internal.contact.info.MemberInfoImpl -import net.mamoe.mirai.internal.contact.info.StrangerInfoImpl -import net.mamoe.mirai.internal.message.OnlineMessageSourceFromFriendImpl -import net.mamoe.mirai.internal.message.toMessageChainOnline import net.mamoe.mirai.internal.network.MultiPacket import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.internal.network.QQAndroidClient -import net.mamoe.mirai.internal.network.components.ContactUpdater -import net.mamoe.mirai.internal.network.components.SsoProcessor -import net.mamoe.mirai.internal.network.handler.logger -import net.mamoe.mirai.internal.network.protocol.data.proto.FrdSysMsg +import net.mamoe.mirai.internal.network.components.NoticePipelineContext.Companion.KEY_FROM_SYNC +import net.mamoe.mirai.internal.network.components.NoticeProcessorPipeline.Companion.processPacketThroughPipeline +import net.mamoe.mirai.internal.network.components.SyncController.Companion.syncController +import net.mamoe.mirai.internal.network.components.syncGetMessage import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm import net.mamoe.mirai.internal.network.protocol.data.proto.MsgSvc -import net.mamoe.mirai.internal.network.protocol.data.proto.SubMsgType0x7 import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketFactory import net.mamoe.mirai.internal.network.protocol.packet.buildOutgoingUniPacket -import net.mamoe.mirai.internal.network.protocol.packet.chat.NewContact -import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList -import net.mamoe.mirai.internal.utils.io.serialization.loadAs import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf import net.mamoe.mirai.internal.utils.io.serialization.writeProtoBuf -import net.mamoe.mirai.message.data.* -import net.mamoe.mirai.message.data.MessageSourceKind.* -import net.mamoe.mirai.utils.cast -import net.mamoe.mirai.utils.debug -import net.mamoe.mirai.utils.read -import net.mamoe.mirai.utils.toUHexString +import net.mamoe.mirai.utils.toLongUnsigned import kotlin.random.Random @@ -85,12 +61,12 @@ internal object MessageSvcPbGetMsg : OutgoingPacketFactory<MessageSvcPbGetMsg.Re whisperSessionId = 0, syncFlag = syncFlag, // serverBuf = from.serverBuf ?: EMPTY_BYTE_ARRAY, - syncCookie = syncCookie ?: client.syncingController.syncCookie - ?: byteArrayOf()//.also { client.c2cMessageSync.syncCookie = it }, + syncCookie = syncCookie ?: client.bot.syncController.syncCookie + ?: byteArrayOf(), //.also { client.c2cMessageSync.syncCookie = it }, // syncFlag = client.c2cMessageSync.syncFlag, //msgCtrlBuf = client.c2cMessageSync.msgCtrlBuf, //pubaccountCookie = client.c2cMessageSync.pubAccountCookie - ) + ), ) } @@ -105,24 +81,26 @@ internal object MessageSvcPbGetMsg : OutgoingPacketFactory<MessageSvcPbGetMsg.Re */ open class Response( internal val syncFlagFromServer: MsgSvc.SyncFlag, - delegate: List<Packet>, - val syncCookie: ByteArray?, override val bot: Bot - ) : - AbstractEvent(), - MultiPacket<Packet>, - Iterable<Packet> by (delegate), + private val delegate: List<Packet>, + val syncCookie: ByteArray?, override val bot: Bot, + ) : AbstractEvent(), + MultiPacket, Packet.NoEventLog, BotEvent { + override val isMeaningful: Boolean get() = true + + override fun children(): Iterator<Packet> { + return delegate.iterator() + } override fun toString(): String = "MessageSvcPbGetMsg.Response(flag=$syncFlagFromServer)" } class EmptyResponse( - bot: QQAndroidBot + bot: QQAndroidBot, ) : GetMsgSuccess(emptyList(), null, bot) - @OptIn(FlowPreview::class) override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response { // 00 00 01 0F 08 00 12 00 1A 34 08 FF C1 C4 F1 05 10 FF C1 C4 F1 05 18 E6 ED B9 C3 02 20 89 FE BE A4 06 28 8A CA 91 D1 0C 48 9B A5 BD 9B 0A 58 DE 9D 99 F8 08 60 1D 68 FF C1 C4 F1 05 70 00 20 02 2A 9D 01 08 F3 C1 C4 F1 05 10 A2 FF 8C F0 03 18 01 22 8A 01 0A 2A 08 A2 FF 8C F0 03 10 DD F1 92 B7 07 18 A6 01 20 0B 28 AE F9 01 30 F4 C1 C4 F1 05 38 A7 E3 D8 D4 84 80 80 80 01 B8 01 CD B5 01 12 08 08 01 10 00 18 00 20 00 1A 52 0A 50 0A 27 08 00 10 F4 C1 C4 F1 05 18 A7 E3 D8 D4 04 20 00 28 0C 30 00 38 86 01 40 22 4A 0C E5 BE AE E8 BD AF E9 9B 85 E9 BB 91 12 08 0A 06 0A 04 4E 4D 53 4C 12 15 AA 02 12 9A 01 0F 80 01 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 12 04 4A 02 08 00 30 01 2A 15 08 97 A2 C1 F1 05 10 95 A6 F5 E5 0C 18 01 30 01 40 01 48 81 01 2A 10 08 D3 F7 B5 F1 05 10 DD F1 92 B7 07 18 01 30 01 38 00 42 00 48 00 val resp = readProtoBuf(MsgSvc.PbGetMsgResp.serializer()) @@ -131,8 +109,8 @@ internal object MessageSvcPbGetMsg : OutgoingPacketFactory<MessageSvcPbGetMsg.Re // this is normally recoverable, no need to log -// bot.network.logger -// .warning { "MessageSvcPushNotify: result != 0, result = ${resp.result}, errorMsg=${resp.errmsg}" } + // bot.network.logger + // .warning { "MessageSvcPushNotify: result != 0, result = ${resp.result}, errorMsg=${resp.errmsg}" } bot.network.launch(CoroutineName("MessageSvcPushNotify.retry")) { delay(500 + Random.nextLong(0, 1000)) bot.network.run { @@ -143,51 +121,42 @@ internal object MessageSvcPbGetMsg : OutgoingPacketFactory<MessageSvcPbGetMsg.Re } when (resp.msgRspType) { 0 -> { - bot.client.syncingController.syncCookie = resp.syncCookie - bot.client.syncingController.pubAccountCookie = resp.pubAccountCookie + bot.syncController.syncCookie = resp.syncCookie + bot.syncController.pubAccountCookie = resp.pubAccountCookie } 1 -> { - bot.client.syncingController.syncCookie = resp.syncCookie + bot.syncController.syncCookie = resp.syncCookie } 2 -> { - bot.client.syncingController.pubAccountCookie = resp.pubAccountCookie + bot.syncController.pubAccountCookie = resp.pubAccountCookie } } -// bot.logger.debug(resp.msgRspType._miraiContentToString()) -// bot.logger.debug(resp.syncCookie._miraiContentToString()) + // bot.logger.debug(resp.msgRspType._miraiContentToString()) + // bot.logger.debug(resp.syncCookie._miraiContentToString()) - bot.client.syncingController.msgCtrlBuf = resp.msgCtrlBuf + bot.syncController.msgCtrlBuf = resp.msgCtrlBuf - if (resp.uinPairMsgs.isEmpty()) { - return EmptyResponse(bot) - } + if (resp.uinPairMsgs.isEmpty()) return EmptyResponse(bot) - val messages = resp.uinPairMsgs.asFlow() + val messages = resp.uinPairMsgs.asSequence() .filterNot { it.msg.isEmpty() } - .flatMapConcat { - it.msg.asFlow() - .filter { msg: MsgComm.Msg -> msg.msgHead.msgTime > it.lastReadTime.toLong() and 4294967295L } - }.also { - MessageSvcPbDeleteMsg.delete(bot, it) // 删除消息 + .flatMap { pair -> + pair.msg.asSequence() + .filter { msg: MsgComm.Msg -> msg.msgHead.msgTime > pair.lastReadTime.toLongUnsigned() } } + .toList() + .also { MessageSvcPbDeleteMsg.delete(bot, it) } // 删除消息 .filter { msg -> - bot.client.syncingController.pbGetMessageCacheList.addCache( - QQAndroidClient.MessageSvcSyncData.PbGetMessageSyncId( - uid = msg.msgHead.msgUid, - sequence = msg.msgHead.msgSeq, - time = msg.msgHead.msgTime - ) - ) + bot.syncController.syncGetMessage(msg.msgHead) } - .flatMapConcat { msg -> - val result = msg.transform(bot) - if (result == null) emptyFlow() else flowOf(result) + .map { msg -> + bot.processPacketThroughPipeline(msg, KEY_FROM_SYNC to false) } - val list: List<Packet> = messages.toList() + val list: List<Packet> = messages if (resp.syncFlag == MsgSvc.SyncFlag.STOP) { return GetMsgSuccess(list, resp.syncCookie, bot) } @@ -205,7 +174,7 @@ internal object MessageSvcPbGetMsg : OutgoingPacketFactory<MessageSvcPbGetMsg.Re MessageSvcPbGetMsg( client, MsgSvc.SyncFlag.CONTINUE, - bot.client.syncingController.syncCookie + bot.syncController.syncCookie, ).sendAndExpect() } return @@ -216,7 +185,7 @@ internal object MessageSvcPbGetMsg : OutgoingPacketFactory<MessageSvcPbGetMsg.Re MessageSvcPbGetMsg( client, MsgSvc.SyncFlag.CONTINUE, - bot.client.syncingController.syncCookie + bot.syncController.syncCookie, ).sendAndExpect() } return @@ -224,398 +193,3 @@ internal object MessageSvcPbGetMsg : OutgoingPacketFactory<MessageSvcPbGetMsg.Re } } } - -internal suspend fun QQAndroidBot.createGroupForBot(groupUin: Long): Group? { - val group = getGroupByUinOrNull(groupUin) - if (group != null) { - return null - } - - return getNewGroup(Mirai.calculateGroupCodeByGroupUin(groupUin))?.apply { groups.delegate.add(this) } -} - -private fun MsgComm.Msg.getNewMemberInfo(): MemberInfo { - return MemberInfoImpl( - nameCard = msgHead.authNick.ifEmpty { msgHead.fromNick }, - permission = MemberPermission.MEMBER, - specialTitle = "", - muteTimestamp = 0, - uin = msgHead.authUin, - nick = msgHead.authNick.ifEmpty { msgHead.fromNick }, - remark = "", - anonymousId = null - ) -} - -internal suspend fun MsgComm.Msg.transform(bot: QQAndroidBot, fromSync: Boolean = false): Packet? { - when (msgHead.msgType) { - 33 -> bot.components[ContactUpdater].groupListModifyLock.withLock { - msgBody.msgContent.read { - val groupUin = Mirai.calculateGroupUinByGroupCode(readUInt().toLong()) - val group = bot.getGroupByUinOrNull(groupUin) ?: bot.createGroupForBot(groupUin) ?: return null - discardExact(1) - val joinedMemberUin = readUInt().toLong() - val joinType = readByte().toInt() - val invitorUin = readUInt().toLong() - return when (joinType) { - //邀请加入 - -125, 3 -> { - val invitor = if (invitorUin == bot.id) { - group.botAsMember - } else { - group[invitorUin] - } ?: return null - if (joinedMemberUin == bot.id) { - BotJoinGroupEvent.Invite(invitor) - } else { - MemberJoinEvent.Invite( - group.newMember(getNewMemberInfo()).cast<NormalMember>() - .also { group.members.delegate.add(it) }, invitor - ) - } - } - //通过群员分享的二维码/直接加入 - -126, 2 -> { - if (joinedMemberUin == bot.id) { - BotJoinGroupEvent.Active(group) - } else { - MemberJoinEvent.Active( - group.newMember(getNewMemberInfo()).cast<NormalMember>() - .also { group.members.delegate.add(it) }) - } - } - //忽略 - else -> { - null - } - } - } - // 邀请入群 - // package: 27 0B 60 E7 01 CA CC 69 8B 83 44 71 47 90 06 B9 DC C0 ED D4 B1 00 30 33 44 30 42 38 46 30 39 37 32 38 35 43 34 31 38 30 33 36 41 34 36 31 36 31 35 32 37 38 46 46 43 30 41 38 30 36 30 36 45 38 31 43 39 41 34 38 37 - // package: groupUin + 01 CA CC 69 8B 83 + invitorUin + length(06) + string + magicKey - - - // 主动入群, 直接加入: msgContent=27 0B 60 E7 01 76 E4 B8 DD 82 3E 03 3F A2 06 B4 B4 BD A8 D5 DF 00 30 42 39 41 30 33 45 38 34 30 39 34 42 46 30 45 32 45 38 42 31 43 43 41 34 32 42 38 42 44 42 35 34 44 42 31 44 32 32 30 46 30 38 39 46 46 35 41 38 - // 主动直接加入 27 0B 60 E7 01 76 E4 B8 DD 82 3E 03 3F A2 06 B4 B4 BD A8 D5 DF 00 30 33 30 45 38 42 31 33 46 41 41 31 33 46 38 31 35 34 41 38 33 32 37 31 43 34 34 38 35 33 35 46 45 31 38 32 43 39 42 43 46 46 32 44 39 39 46 41 37 - - // 有人被邀请(经过同意后)加入 27 0B 60 E7 01 76 E4 B8 DD 83 3E 03 3F A2 06 B4 B4 BD A8 D5 DF 00 30 34 30 34 38 32 33 38 35 37 41 37 38 46 33 45 37 35 38 42 39 38 46 43 45 44 43 32 41 30 31 36 36 30 34 31 36 39 35 39 30 38 39 30 39 45 31 34 34 - // 搜索到群, 直接加入 27 0B 60 E7 01 07 6E 47 BA 82 3E 03 3F A2 06 B4 B4 BD A8 D5 DF 00 30 32 30 39 39 42 39 41 46 32 39 41 35 42 33 46 34 32 30 44 36 44 36 39 35 44 38 45 34 35 30 46 30 45 30 38 45 31 41 39 42 46 46 45 32 30 32 34 35 - } - - 34 -> { // 与 33 重复 - return null - } - - 38 -> bot.components[ContactUpdater].groupListModifyLock.withLock { // 建群 - return bot.createGroupForBot(msgHead.fromUin) - ?.let { BotJoinGroupEvent.Active(it) } - } - - 85 -> bot.components[ContactUpdater].groupListModifyLock.withLock { // 其他客户端入群 - // msgHead.authUin: 处理人 - - return if (msgHead.toUin == bot.id) { - bot.createGroupForBot(msgHead.fromUin) - ?.let { BotJoinGroupEvent.Active(it) } - } else { - null - } - } - - /* - 34 -> { // 主动入群 - - // 回答了问题, 还需要管理员审核 - // msgContent=27 0B 60 E7 01 76 E4 B8 DD 82 00 30 45 41 31 30 35 35 42 44 39 39 42 35 37 46 44 31 41 31 46 36 42 43 42 43 33 43 42 39 34 34 38 31 33 34 42 36 31 46 38 45 43 39 38 38 43 39 37 33 - // msgContent=27 0B 60 E7 01 76 E4 B8 DD 02 00 30 44 44 41 43 44 33 35 43 31 39 34 30 46 42 39 39 34 46 43 32 34 43 39 32 33 39 31 45 42 35 32 33 46 36 30 37 35 42 41 38 42 30 30 37 42 36 42 41 - // 回答正确问题, 直接加入 - - // 27 0B 60 E7 01 76 E4 B8 DD 82 00 30 43 37 37 39 41 38 32 44 38 33 30 35 37 38 31 33 37 45 42 39 35 43 42 45 36 45 43 38 36 34 38 44 34 35 44 42 33 44 45 37 34 41 36 30 33 37 46 45 - // 提交验证消息加入, 需要审核 - - // 被踢了?? - // msgContent=27 0B 60 E7 01 76 E4 B8 DD 83 3E 03 3F A2 06 B4 B4 BD A8 D5 DF 00 30 46 46 32 33 36 39 35 33 31 37 42 44 46 37 43 36 39 34 37 41 45 38 39 43 45 43 42 46 33 41 37 35 39 34 39 45 36 37 33 37 31 41 39 44 33 33 45 33 - - /* - // 搜索后直接加入群 - - soutv 17:43:32 : 33类型的content = 27 0B 60 E7 01 07 6E 47 BA 82 3E 03 3F A2 06 B4 B4 BD A8 D5 DF 00 30 32 30 39 39 42 39 41 46 32 39 41 35 42 33 46 34 32 30 44 36 44 36 39 35 44 38 45 34 35 30 46 30 45 30 38 45 31 41 39 42 46 46 45 32 30 32 34 35 - soutv 17:43:32 : 主动入群content = 2A 3D F5 69 01 35 D7 10 EA 83 4C EF 4F DD 06 B9 DC C0 ED D4 B1 00 30 37 41 39 31 39 34 31 41 30 37 46 38 32 31 39 39 43 34 35 46 39 30 36 31 43 37 39 37 33 39 35 43 34 44 36 31 33 43 31 35 42 37 32 45 46 43 43 36 - */ - - val group = bot.getGroupByUinOrNull(msgHead.fromUin) - group ?: return null - - msgBody.msgContent.soutv("主动入群content") - - if (msgBody.msgContent.read { - discardExact(4) // group code - discardExact(1) // 1 - discardExact(4) // requester uin - readByte().toInt().and(0xff) - // 0x02: 回答正确问题直接加入 - // 0x82: 回答了问题, 或者有验证消息, 需要管理员审核 - // 0x83: 回答正确问题直接加入 - } != 0x82) { - - if (group.members.contains(msgHead.authUin)) { - return null - } - @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") - return MemberJoinEvent.Active(group.newMember(getNewMemberInfo()) - .also { group.members.delegate.addLast(it) }) - } else return null - } - */ - - //167 单向好友 - 166, 167 -> { - //我也不知道为什么要这样写,但它就是能跑 - if (msgHead.fromUin == bot.id && !fromSync) { - loop@ while (true) { - val instance = bot.client.getFriendSeq() - if (instance < msgHead.msgSeq) { - if (bot.client.setFriendSeq(instance, msgHead.msgSeq)) { - break@loop - } - } else break@loop - } - return null - } - if (!bot.components[SsoProcessor].firstLoginSucceed) { - return null - } - val fromUin = if (fromSync) { - msgHead.toUin - } else { - msgHead.fromUin - } - bot.getFriend(fromUin)?.let { friend -> - friend.checkIsFriendImpl() - friend.lastMessageSequence.loop { - //我也不知道为什么要这样写,但它就是能跑 - return if (friend.lastMessageSequence.value != msgHead.msgSeq - && friend.lastMessageSequence.compareAndSet(it, msgHead.msgSeq) - && contentHead?.autoReply != 1 - ) { - val msgs = friend.friendPkgMsgParsingCache.tryMerge(this) - if (msgs.isNotEmpty()) { - if (fromSync) { - FriendMessageSyncEvent( - friend, - msgs.toMessageChainOnline(bot, 0, MessageSourceKind.FRIEND), - msgHead.msgTime - ) - } else { - FriendMessageEvent( - friend, - msgs.toMessageChainOnline(bot, 0, MessageSourceKind.FRIEND), - msgHead.msgTime - ) - } - } else return null - } else null - } - } ?: bot.getStranger(fromUin)?.let { stranger -> - stranger.checkIsImpl() - stranger.lastMessageSequence.loop { - //我也不知道为什么要这样写,但它就是能跑 - return if (stranger.lastMessageSequence.value != msgHead.msgSeq && stranger.lastMessageSequence.compareAndSet( - it, - msgHead.msgSeq - ) && contentHead?.autoReply != 1 - ) { - if (fromSync) { - StrangerMessageSyncEvent( - stranger, - listOf(this).toMessageChainOnline(bot, 0, STRANGER), - msgHead.msgTime - ) - } else { - StrangerMessageEvent( - stranger, - listOf(this).toMessageChainOnline(bot, 0, STRANGER), - msgHead.msgTime - ) - } - } else null - } - } ?: return null - } - 208 -> { - // friend ptt - val target = bot.getFriend(msgHead.fromUin) - ?: return null - val lsc = listOf(this).toMessageChainOnline(bot, 0, FRIEND) - - return FriendMessageEvent(target, lsc, msgHead.msgTime) - } - 529 -> { - - // top_package/awbk.java:3765 - - return when (msgHead.c2cCmd) { - // other client sync - 7 -> { - val data = msgBody.msgContent.loadAs(SubMsgType0x7.MsgBody.serializer()) - - val textMsg = - data.msgSubcmd0x4Generic?.buf?.loadAs(SubMsgType0x7.MsgBody.QQDataTextMsg.serializer()) - ?: return null - - with(data.msgHeader ?: return null) { - if (dstUin != bot.id) return null - val client = bot.otherClients.find { it.appId == srcInstId } - ?: return null// don't compare with dstAppId. diff. - - val chain = buildMessageChain { - +OnlineMessageSourceFromFriendImpl(bot, listOf(this@transform)) - for (msgItem in textMsg.msgItems) { - when (msgItem.type) { - 1 -> +PlainText(msgItem.text) - else -> { - } - } - } - } - - return OtherClientMessageEvent(client, chain, msgHead.msgTime) - } - } - - else -> null - } - - // 各种垃圾 - // 08 04 12 1E 08 E9 07 10 B7 F7 8B 80 02 18 E9 07 20 00 28 DD F1 92 B7 07 30 DD F1 92 B7 07 48 02 50 03 32 1E 08 88 80 F8 92 CD 84 80 80 10 10 01 18 00 20 01 2A 0C 0A 0A 08 01 12 06 E5 95 8A E5 95 8A - } - 141 -> { - - if (!bot.components[SsoProcessor].firstLoginSucceed || msgHead.fromUin == bot.id && !fromSync) { - return null - } - val tmpHead = msgHead.c2cTmpMsgHead ?: return null - val member = bot.getGroupByUinOrNull(tmpHead.groupUin)?.get( - if (fromSync) { - msgHead.toUin - } else { - msgHead.fromUin - } - ) - ?: return null - - member.checkIsMemberImpl() - - member.lastMessageSequence.loop { instant -> - if (member.lastMessageSequence.value != msgHead.msgSeq && contentHead?.autoReply != 1) { - if (member.lastMessageSequence.compareAndSet(instant, msgHead.msgSeq)) { - return if (fromSync) { - GroupTempMessageSyncEvent( - member, - listOf(this).toMessageChainOnline(bot, 0, TEMP), - msgHead.msgTime - ) - } else { - GroupTempMessageEvent( - member, - listOf(this).toMessageChainOnline(bot, 0, TEMP), - msgHead.msgTime - ) - } - } - } else return null - } - } - 84, 87 -> { // 请求入群验证 和 被要求入群 - bot.network.run { - NewContact.SystemMsgNewGroup(bot.client).sendWithoutExpect() - } - return null - } - 187 -> { // 请求加好友验证 - bot.network.run { - NewContact.SystemMsgNewFriend(bot.client).sendWithoutExpect() - } - return null - } - 732 -> { - // unknown - // 前 4 byte 是群号 - return null - } - //陌生人添加信息 - 191 -> { - var fromGroup = 0L - var pbNick = "" - msgBody.msgContent.read { - readUByte()// version - discardExact(readUByte().toInt())//skip - readUShort()//source id - readUShort()//SourceSubID - discardExact(readUShort().toLong())//skip size - if (readUShort().toInt() != 0) {//hasExtraInfo - discardExact(readUShort().toInt())//mail address info, skip - } - discardExact(4 + readUShort().toInt())//skip - for (i in 1..readUByte().toInt()) {//pb size - val type = readUShort().toInt() - val pbArray = ByteArray(readUShort().toInt() and 0xFF) - readAvailable(pbArray) - when (type) { - 1000 -> pbArray.loadAs(FrdSysMsg.GroupInfo.serializer()).let { fromGroup = it.groupUin } - 1002 -> pbArray.loadAs(FrdSysMsg.FriendMiscInfo.serializer()).let { pbNick = it.fromuinNick } - else -> { - }//ignore - } - } - } - val nick = sequenceOf(msgHead.fromNick, msgHead.authNick, pbNick).filter { it.isNotEmpty() }.firstOrNull() - ?: return null - val id = sequenceOf(msgHead.fromUin, msgHead.authUin).filter { it != 0L }.firstOrNull() ?: return null//对方QQ - Mirai.newStranger(bot, StrangerInfoImpl(id, nick, fromGroup)).let { - bot.getStranger(id)?.let { previous -> - bot.strangers.remove(id) - StrangerRelationChangeEvent.Deleted(previous).broadcast() - } - bot.strangers.delegate.add(it) - - return StrangerAddEvent(it) - } - } - // 732: 27 0B 60 E7 0C 01 3E 03 3F A2 5E 90 60 E2 00 01 44 71 47 90 00 00 02 58 - // 732: 27 0B 60 E7 11 00 40 08 07 20 E7 C1 AD B8 02 5A 36 08 B4 E7 E0 F0 09 1A 1A 08 9C D4 16 10 F7 D2 D8 F5 05 18 D0 E2 85 F4 06 20 00 28 00 30 B4 E7 E0 F0 09 2A 0E 08 00 12 0A 08 9C D4 16 10 00 18 01 20 00 30 00 38 00 - // 732: 27 0B 60 E7 11 00 33 08 07 20 E7 C1 AD B8 02 5A 29 08 EE 97 85 E9 01 1A 19 08 EE D6 16 10 FF F2 D8 F5 05 18 E9 E7 A3 05 20 00 28 00 30 EE 97 85 E9 01 2A 02 08 00 30 00 38 00 - else -> { - bot.network.logger.debug { "unknown PbGetMsg type ${msgHead.msgType}, data=${msgBody.msgContent.toUHexString()}" } - return null - } - } -} - -// kotlin bug, don't remove -private inline fun kotlinx.atomicfu.AtomicInt.loop(action: (Int) -> Unit): Nothing { - while (true) { - action(value) - } -} - - -internal suspend fun QQAndroidBot.getNewGroup(groupCode: Long): Group? { - val troopNum = network.run { - FriendList.GetTroopListSimplify(client) - .sendAndExpect<FriendList.GetTroopListSimplify.Response>(timeoutMillis = 10_000, retry = 5) - }.groups.firstOrNull { it.groupCode == groupCode } ?: return null - - return GroupImpl( - bot = this, - coroutineContext = coroutineContext, - id = groupCode, - groupInfo = GroupInfoImpl(troopNum), - members = Mirai.getRawGroupMemberList( - this, - troopNum.groupUin, - troopNum.groupCode, - troopNum.dwGroupOwnerUin - ) - ) -} diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbSendMsg.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbSendMsg.kt index fc5f801a8d0..bcf57243dd3 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbSendMsg.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbSendMsg.kt @@ -23,7 +23,8 @@ import net.mamoe.mirai.internal.contact.uin import net.mamoe.mirai.internal.message.* import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.internal.network.QQAndroidClient -import net.mamoe.mirai.internal.network.QQAndroidClient.MessageSvcSyncData.PendingGroupMessageReceiptSyncId +import net.mamoe.mirai.internal.network.components.SyncController.Companion.syncController +import net.mamoe.mirai.internal.network.components.SyncController.Companion.syncCookie import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm import net.mamoe.mirai.internal.network.protocol.data.proto.MsgCtrl @@ -104,13 +105,13 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg. msgBody: ImMsgBody.MsgBody, msgSeq: Int, msgRand: Int, - contentHead: MsgComm.ContentHead + contentHead: MsgComm.ContentHead, ) -> MsgSvc.PbSendMsgReq, sequenceIds: AtomicReference<IntArray>, sequenceIdsInitializer: (Int) -> IntArray, randIds: AtomicReference<IntArray>, doFragmented: Boolean = true, - postInit: () -> Unit + postInit: () -> Unit, ): List<OutgoingPacket> { val fragmented = if (doFragmented) message.fragmented() @@ -126,21 +127,23 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg. randIds.set(randIds0) postInit() fragmented.forEachIndexed { pkgIndex, fMsg -> - response.add(buildOutgoingUniPacket(client) { - writeProtoBuf( - MsgSvc.PbSendMsgReq.serializer(), - pbSendMsgReq( - fragmentTranslator(fMsg), - seqIds[pkgIndex], - randIds0[pkgIndex], - MsgComm.ContentHead( - pkgNum = pkgNum, - divSeq = div, - pkgIndex = pkgIndex - ) + response.add( + buildOutgoingUniPacket(client) { + writeProtoBuf( + MsgSvc.PbSendMsgReq.serializer(), + pbSendMsgReq( + fragmentTranslator(fMsg), + seqIds[pkgIndex], + randIds0[pkgIndex], + MsgComm.ContentHead( + pkgNum = pkgNum, + divSeq = div, + pkgIndex = pkgIndex, + ), + ), ) - ) - }) + }, + ) } return response } @@ -162,7 +165,7 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg. } else { 0 } - } + }, ) } @@ -175,7 +178,7 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg. target: Stranger, message: MessageChain, fragmented: Boolean, - source: (OnlineMessageSourceToStrangerImpl) -> Unit + source: (OnlineMessageSourceToStrangerImpl) -> Unit, ): List<OutgoingPacket> { val sequenceIds = AtomicReference<IntArray>() @@ -186,8 +189,8 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg. fragmentTranslator = { ImMsgBody.MsgBody( richText = ImMsgBody.RichText( - elems = it.toRichTextElems(messageTarget = target, withGeneralFlags = true) - ) + elems = it.toRichTextElems(messageTarget = target, withGeneralFlags = true), + ), ) }, pbSendMsgReq = { msgBody, msgSeq, msgRand, contentHead -> @@ -197,7 +200,7 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg. msgBody = msgBody, msgSeq = msgSeq, msgRand = msgRand, - syncCookie = client.syncingController.syncCookie ?: byteArrayOf() + syncCookie = client.syncCookie ?: byteArrayOf(), // msgVia = 1 ) }, @@ -214,11 +217,11 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg. target = target, time = currentTimeSeconds().toInt(), sequenceIds = sequenceIds.get(), - originalMessage = message - ) + originalMessage = message, + ), ) }, - doFragmented = fragmented + doFragmented = fragmented, ) } @@ -231,7 +234,7 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg. targetFriend: Friend, message: MessageChain, fragmented: Boolean, - crossinline sourceCallback: (OnlineMessageSourceToFriendImpl) -> Unit + crossinline sourceCallback: (OnlineMessageSourceToFriendImpl) -> Unit, ): List<OutgoingPacket> { contract { callsInPlace(sourceCallback, InvocationKind.EXACTLY_ONCE) @@ -247,7 +250,7 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg. richText = ImMsgBody.RichText( elems = subChain.toRichTextElems(messageTarget = targetFriend, withGeneralFlags = true), ptt = subChain.findPtt(), - ) + ), ) }, pbSendMsgReq = { msgBody, msgSeq, msgRand, contentHead -> @@ -257,14 +260,14 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg. msgBody = msgBody, msgSeq = msgSeq, msgRand = msgRand, - syncCookie = client.syncingController.syncCookie ?: byteArrayOf() + syncCookie = client.syncCookie ?: byteArrayOf(), // msgVia = 1 ) }, sequenceIds = sequenceIds, randIds = randIds, sequenceIdsInitializer = { size -> - IntArray(size) { client.nextFriendSeq() } + IntArray(size) { client.sendFriendMessageSeq.next() } }, postInit = { sourceCallback( @@ -274,11 +277,11 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg. target = targetFriend, time = currentTimeSeconds().toInt(), sequenceIds = sequenceIds.get(), - originalMessage = message - ) + originalMessage = message, + ), ) }, - doFragmented = fragmented + doFragmented = fragmented, ) } /*= buildOutgoingUniPacket(client) { @@ -307,7 +310,7 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg. ), msgSeq = source.sequenceIds.single(), msgRand = source.internalIds.single(), - syncCookie = client.syncingController.syncCookie ?: byteArrayOf() + syncCookie = client.syncCookie ?: byteArrayOf() // msgVia = 1 ) ) @@ -322,23 +325,24 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg. client: QQAndroidClient, targetMember: Member, message: MessageChain, - source: OnlineMessageSourceToTempImpl + source: OnlineMessageSourceToTempImpl, ) = buildOutgoingUniPacket(client) { writeProtoBuf( - MsgSvc.PbSendMsgReq.serializer(), MsgSvc.PbSendMsgReq( + MsgSvc.PbSendMsgReq.serializer(), + MsgSvc.PbSendMsgReq( routingHead = MsgSvc.RoutingHead( - grpTmp = MsgSvc.GrpTmp(targetMember.group.uin, targetMember.id) + grpTmp = MsgSvc.GrpTmp(targetMember.group.uin, targetMember.id), ), contentHead = MsgComm.ContentHead(pkgNum = 1), msgBody = ImMsgBody.MsgBody( richText = ImMsgBody.RichText( - elems = message.toRichTextElems(messageTarget = targetMember, withGeneralFlags = true) - ) + elems = message.toRichTextElems(messageTarget = targetMember, withGeneralFlags = true), + ), ), msgSeq = source.sequenceIds.single(), msgRand = source.internalIds.single(), - syncCookie = client.syncingController.syncCookie ?: byteArrayOf() - ) + syncCookie = client.syncCookie ?: byteArrayOf(), + ), ) } @@ -352,7 +356,7 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg. targetGroup: Group, message: MessageChain, fragmented: Boolean, - crossinline sourceCallback: (OnlineMessageSourceToGroupImpl) -> Unit + crossinline sourceCallback: (OnlineMessageSourceToGroupImpl) -> Unit, ): List<OutgoingPacket> { val sequenceIds = AtomicReference<IntArray>() val randIds = AtomicReference<IntArray>() @@ -363,9 +367,9 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg. ImMsgBody.MsgBody( richText = ImMsgBody.RichText( elems = subChain.toRichTextElems(messageTarget = targetGroup, withGeneralFlags = true), - ptt = subChain.findPtt() + ptt = subChain.findPtt(), - ) + ), ) }, pbSendMsgReq = { msgBody, msgSeq, msgRand, contentHead -> @@ -375,12 +379,12 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg. msgBody = msgBody, msgSeq = msgSeq, msgRand = msgRand, - syncCookie = client.syncingController.syncCookie ?: byteArrayOf(), + syncCookie = client.syncCookie ?: byteArrayOf(), msgVia = 1, msgCtrl = if (message[ForwardMessageInternal] != null) MsgCtrl.MsgCtrl(msgFlag = 4) - else null + else null, ) }, sequenceIds = sequenceIds, @@ -389,13 +393,7 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg. IntArray(size) { client.atomicNextMessageSequenceId() } }, postInit = { - randIds.get().forEach { id -> - client.syncingController.pendingGroupMessageReceiptCacheList.addCache( - PendingGroupMessageReceiptSyncId( - messageRandom = id, - ) - ) - } + randIds.get().forEach { id -> client.syncController.syncGroupMessageReceipt(id) } sourceCallback( OnlineMessageSourceToGroupImpl( targetGroup, @@ -403,12 +401,12 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg. sender = client.bot, target = targetGroup, time = currentTimeSeconds().toInt(), - originalMessage = message//, + originalMessage = message, //, // sourceMessage = message - ) + ), ) }, - doFragmented = fragmented + doFragmented = fragmented, ) } @@ -468,7 +466,7 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg. else -> Response.Failed( response.result, response.errtype, - response.errmsg + response.errmsg, ) } @@ -481,7 +479,7 @@ internal inline fun MessageSvcPbSendMsg.createToTemp( member: Member, message: MessageChain, fragmented: Boolean, - crossinline sourceCallback: (Deferred<OnlineMessageSourceToTempImpl>) -> Unit + crossinline sourceCallback: (Deferred<OnlineMessageSourceToTempImpl>) -> Unit, ): List<OutgoingPacket> { contract { callsInPlace(sourceCallback, InvocationKind.EXACTLY_ONCE) @@ -492,14 +490,14 @@ internal inline fun MessageSvcPbSendMsg.createToTemp( target = member, time = currentTimeSeconds().toInt(), sequenceIds = intArrayOf(client.atomicNextMessageSequenceId()), - originalMessage = message + originalMessage = message, ) sourceCallback(CompletableDeferred(source)) return createToTempImpl( client, member, message, - source + source, ).let { listOf(it) } } @@ -508,7 +506,7 @@ internal inline fun MessageSvcPbSendMsg.createToStranger( stranger: Stranger, message: MessageChain, fragmented: Boolean, - crossinline sourceCallback: (Deferred<OnlineMessageSourceToStrangerImpl>) -> Unit + crossinline sourceCallback: (Deferred<OnlineMessageSourceToStrangerImpl>) -> Unit, ): List<OutgoingPacket> { contract { callsInPlace(sourceCallback, InvocationKind.EXACTLY_ONCE) @@ -517,7 +515,7 @@ internal inline fun MessageSvcPbSendMsg.createToStranger( client, stranger, message, - fragmented + fragmented, ) { sourceCallback(CompletableDeferred(it)) } } @@ -526,7 +524,7 @@ internal inline fun MessageSvcPbSendMsg.createToFriend( qq: Friend, message: MessageChain, fragmented: Boolean, - crossinline sourceCallback: (Deferred<OnlineMessageSourceToFriendImpl>) -> Unit + crossinline sourceCallback: (Deferred<OnlineMessageSourceToFriendImpl>) -> Unit, ): List<OutgoingPacket> { contract { callsInPlace(sourceCallback, InvocationKind.EXACTLY_ONCE) @@ -535,7 +533,7 @@ internal inline fun MessageSvcPbSendMsg.createToFriend( client, qq, message, - fragmented + fragmented, ) { sourceCallback(CompletableDeferred(it)) } } @@ -545,7 +543,7 @@ internal inline fun MessageSvcPbSendMsg.createToGroup( group: Group, message: MessageChain, fragmented: Boolean, - crossinline sourceCallback: (Deferred<OnlineMessageSourceToGroupImpl>) -> Unit + crossinline sourceCallback: (Deferred<OnlineMessageSourceToGroupImpl>) -> Unit, ): List<OutgoingPacket> { contract { callsInPlace(sourceCallback, InvocationKind.EXACTLY_ONCE) @@ -554,6 +552,6 @@ internal inline fun MessageSvcPbSendMsg.createToGroup( client, group, message, - fragmented + fragmented, ) { sourceCallback(CompletableDeferred(it)) } } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PushNotify.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PushNotify.kt index 100f31a1d09..794b43394b1 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PushNotify.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PushNotify.kt @@ -13,6 +13,7 @@ import kotlinx.atomicfu.loop import kotlinx.io.core.ByteReadPacket import kotlinx.io.core.discardExact import net.mamoe.mirai.internal.QQAndroidBot +import net.mamoe.mirai.internal.network.components.SyncController.Companion.syncController import net.mamoe.mirai.internal.network.protocol.data.jce.RequestPushNotify import net.mamoe.mirai.internal.network.protocol.data.proto.MsgSvc import net.mamoe.mirai.internal.network.protocol.packet.IncomingPacketFactory @@ -30,18 +31,17 @@ internal object MessageSvcPushNotify : IncomingPacketFactory<RequestPushNotify>( } override suspend fun QQAndroidBot.handle(packet: RequestPushNotify, sequenceId: Int): OutgoingPacket { - - client.syncingController.firstNotify.loop { firstNotify -> + syncController.firstNotify.loop { firstNotify -> network.run { return MessageSvcPbGetMsg( client, MsgSvc.SyncFlag.START, if (firstNotify) { - if (!client.syncingController.firstNotify.compareAndSet(firstNotify, false)) { + if (!syncController.firstNotify.compareAndSet(firstNotify, false)) { return@loop } null - } else packet.vNotifyCookie + } else packet.vNotifyCookie, ) } } diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.RequestPushStatus.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.RequestPushStatus.kt index 5e76e1ced89..b143e9ff834 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.RequestPushStatus.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.RequestPushStatus.kt @@ -9,84 +9,19 @@ package net.mamoe.mirai.internal.network.protocol.packet.chat.receive -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.sync.withLock import kotlinx.io.core.ByteReadPacket -import net.mamoe.mirai.Mirai -import net.mamoe.mirai.contact.ClientKind -import net.mamoe.mirai.contact.OtherClientInfo -import net.mamoe.mirai.contact.Platform -import net.mamoe.mirai.event.events.OtherClientOfflineEvent -import net.mamoe.mirai.event.events.OtherClientOnlineEvent import net.mamoe.mirai.internal.QQAndroidBot -import net.mamoe.mirai.internal.contact.appId -import net.mamoe.mirai.internal.contact.createOtherClient -import net.mamoe.mirai.internal.message.contextualBugReportException import net.mamoe.mirai.internal.network.Packet -import net.mamoe.mirai.internal.network.components.ContactUpdater -import net.mamoe.mirai.internal.network.handler.logger +import net.mamoe.mirai.internal.network.components.NoticeProcessorPipeline.Companion.processPacketThroughPipeline import net.mamoe.mirai.internal.network.protocol.data.jce.RequestPushStatus import net.mamoe.mirai.internal.network.protocol.packet.IncomingPacketFactory -import net.mamoe.mirai.internal.utils._miraiContentToString import net.mamoe.mirai.internal.utils.io.serialization.readUniPacket -internal object MessageSvcRequestPushStatus : IncomingPacketFactory<Packet?>( - "MessageSvc.RequestPushStatus", "" +internal object MessageSvcRequestPushStatus : IncomingPacketFactory<Packet>( + "MessageSvc.RequestPushStatus", "", ) { - override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): Packet? { + override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): Packet { val packet = readUniPacket(RequestPushStatus.serializer()) - bot.components[ContactUpdater].otherClientsLock.withLock { - val instanceInfo = packet.vecInstanceList?.firstOrNull() - val appId = instanceInfo?.iAppId ?: 1 - return when (packet.status.toInt()) { - 1 -> { // online - if (bot.otherClients.any { appId == it.appId }) return null - - suspend fun tryFindInQuery(): OtherClientInfo? { - return Mirai.getOnlineOtherClientsList(bot).find { it.appId == appId } - ?: kotlin.run { - delay(2000) // sometimes server sync slow - Mirai.getOnlineOtherClientsList(bot).find { it.appId == appId } - } - } - - val info = - tryFindInQuery() ?: kotlin.run { - bot.network.logger.warning( - contextualBugReportException( - "SvcRequestPushStatus (OtherClient online)", - "packet: \n" + packet._miraiContentToString() + - "\n\nquery: \n" + - Mirai.getOnlineOtherClientsList(bot)._miraiContentToString(), - additional = "Failed to find corresponding instanceInfo." - ) - ) - OtherClientInfo(appId, Platform.WINDOWS, "", "电脑") - } - - val client = bot.createOtherClient(info) - bot.otherClients.delegate.add(client) - OtherClientOnlineEvent( - client, - ClientKind[packet.nClientType?.toInt() ?: 0] - ) - } - - 2 -> { // off - val client = bot.otherClients.find { it.appId == appId } ?: return null - client.cancel(CancellationException("Offline")) - bot.otherClients.delegate.remove(client) - OtherClientOfflineEvent(client) - } - - else -> throw contextualBugReportException( - "decode SvcRequestPushStatus (PC Client status change)", - packet._miraiContentToString(), - additional = "unknown status=${packet.status}" - ) - } - } + return bot.processPacketThroughPipeline(packet) } } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbC2CMsgSync.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbC2CMsgSync.kt index 131820a1477..0c98ffb09d2 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbC2CMsgSync.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbC2CMsgSync.kt @@ -12,14 +12,19 @@ package net.mamoe.mirai.internal.network.protocol.packet.chat.receive import kotlinx.io.core.ByteReadPacket import net.mamoe.mirai.internal.QQAndroidBot import net.mamoe.mirai.internal.network.Packet +import net.mamoe.mirai.internal.network.components.NoticePipelineContext.Companion.KEY_FROM_SYNC +import net.mamoe.mirai.internal.network.components.NoticeProcessorPipeline.Companion.processPacketThroughPipeline import net.mamoe.mirai.internal.network.protocol.data.proto.MsgOnlinePush import net.mamoe.mirai.internal.network.protocol.packet.IncomingPacketFactory import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf -internal object PbC2CMsgSync : IncomingPacketFactory<Packet?>( - "OnlinePush.PbC2CMsgSync", "" +internal object PbC2CMsgSync : IncomingPacketFactory<Packet>( + "OnlinePush.PbC2CMsgSync", "", ) { - override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): Packet? { - return readProtoBuf(MsgOnlinePush.PbPushMsg.serializer()).msg.transform(bot, true) + override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): Packet { + return bot.processPacketThroughPipeline( + readProtoBuf(MsgOnlinePush.PbPushMsg.serializer()).msg, + KEY_FROM_SYNC to true, + ) } } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt index e88ad0e98a0..90f72b0e1d3 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt @@ -7,190 +7,25 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") - package net.mamoe.mirai.internal.network.protocol.packet.chat.receive import kotlinx.io.core.ByteReadPacket -import net.mamoe.mirai.contact.Member -import net.mamoe.mirai.event.AbstractEvent -import net.mamoe.mirai.event.Event -import net.mamoe.mirai.event.broadcast -import net.mamoe.mirai.event.events.GroupMessageEvent -import net.mamoe.mirai.event.events.GroupMessageSyncEvent -import net.mamoe.mirai.event.events.MemberCardChangeEvent import net.mamoe.mirai.internal.QQAndroidBot -import net.mamoe.mirai.internal.contact.GroupImpl -import net.mamoe.mirai.internal.contact.NormalMemberImpl -import net.mamoe.mirai.internal.contact.info -import net.mamoe.mirai.internal.contact.info.MemberInfoImpl -import net.mamoe.mirai.internal.contact.newAnonymous -import net.mamoe.mirai.internal.message.toMessageChainOnline import net.mamoe.mirai.internal.network.Packet +import net.mamoe.mirai.internal.network.components.NoticeProcessorPipeline.Companion.processPacketThroughPipeline import net.mamoe.mirai.internal.network.components.SsoProcessor -import net.mamoe.mirai.internal.network.handler.logger -import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody -import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm import net.mamoe.mirai.internal.network.protocol.data.proto.MsgOnlinePush -import net.mamoe.mirai.internal.network.protocol.data.proto.Oidb0x8fc import net.mamoe.mirai.internal.network.protocol.packet.IncomingPacketFactory -import net.mamoe.mirai.internal.utils.io.serialization.loadAs -import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.OnlinePushPbPushGroupMsg.MemberNick.Companion.generateMemberNickFromMember import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf -import net.mamoe.mirai.message.data.MessageSourceKind.GROUP -import net.mamoe.mirai.utils.* /** * 接受群消息 */ internal object OnlinePushPbPushGroupMsg : IncomingPacketFactory<Packet?>("OnlinePush.PbPushGroupMsg") { - internal class SendGroupMessageReceipt( - val messageRandom: Int, - val sequenceId: Int, - val fromAppId: Int, - ) : Packet, Event, Packet.NoLog, AbstractEvent() { - override fun toString(): String { - return "OnlinePush.PbPushGroupMsg.SendGroupMessageReceipt(messageRandom=$messageRandom, sequenceId=$sequenceId)" - } - - companion object { - val EMPTY = SendGroupMessageReceipt(0, 0, 0) - } - } - internal data class MemberNick(val nick: String, val isNameCard: Boolean = false) { - companion object { - fun Member.generateMemberNickFromMember(): MemberNick { - return nameCard.takeIf { nameCard.isNotEmpty() }?.let { - MemberNick(nameCard, true) - } ?: MemberNick(nick, false) - } - } - } - - @OptIn(ExperimentalStdlibApi::class) override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): Packet? { // 00 00 02 E4 0A D5 05 0A 4F 08 A2 FF 8C F0 03 10 DD F1 92 B7 07 18 52 20 00 28 BC 3D 30 8C 82 AB F1 05 38 D2 80 E0 8C 80 80 80 80 02 4A 21 08 E7 C1 AD B8 02 10 01 18 BA 05 22 09 48 69 6D 31 38 38 6D 6F 65 30 06 38 02 42 05 4D 69 72 61 69 50 01 58 01 60 00 88 01 08 12 06 08 01 10 00 18 00 1A F9 04 0A F6 04 0A 26 08 00 10 87 82 AB F1 05 18 B7 B4 BF 30 20 00 28 0C 30 00 38 86 01 40 22 4A 0C E5 BE AE E8 BD AF E9 9B 85 E9 BB 91 12 E6 03 42 E3 03 12 2A 7B 34 45 31 38 35 38 32 32 2D 30 45 37 42 2D 46 38 30 46 2D 43 35 42 31 2D 33 34 34 38 38 33 37 34 44 33 39 43 7D 2E 6A 70 67 22 00 2A 04 03 00 00 00 32 60 15 36 20 39 36 6B 45 31 41 38 35 32 32 39 64 63 36 39 38 34 37 39 37 37 62 20 20 20 20 20 20 35 30 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 7B 34 45 31 38 35 38 32 32 2D 30 45 37 42 2D 46 38 30 46 2D 43 35 42 31 2D 33 34 34 38 38 33 37 34 44 33 39 43 7D 2E 6A 70 67 31 32 31 32 41 38 C6 BB 8A A9 08 40 FB AE 9E C2 09 48 50 50 41 5A 00 60 01 6A 10 4E 18 58 22 0E 7B F8 0F C5 B1 34 48 83 74 D3 9C 72 59 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 36 35 35 30 35 37 31 32 37 2D 32 32 33 33 36 33 38 33 34 32 2D 34 45 31 38 35 38 32 32 30 45 37 42 46 38 30 46 43 35 42 31 33 34 34 38 38 33 37 34 44 33 39 43 2F 31 39 38 3F 74 65 72 6D 3D 32 82 01 57 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 36 35 35 30 35 37 31 32 37 2D 32 32 33 33 36 33 38 33 34 32 2D 34 45 31 38 35 38 32 32 30 45 37 42 46 38 30 46 43 35 42 31 33 34 34 38 38 33 37 34 44 33 39 43 2F 30 3F 74 65 72 6D 3D 32 B0 01 4D B8 01 2E C8 01 FF 05 D8 01 4D E0 01 2E FA 01 59 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 36 35 35 30 35 37 31 32 37 2D 32 32 33 33 36 33 38 33 34 32 2D 34 45 31 38 35 38 32 32 30 45 37 42 46 38 30 46 43 35 42 31 33 34 34 38 38 33 37 34 44 33 39 43 2F 34 30 30 3F 74 65 72 6D 3D 32 80 02 4D 88 02 2E 12 45 AA 02 42 50 03 60 00 68 00 9A 01 39 08 09 20 BF 50 80 01 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 04 08 02 08 01 90 04 80 80 80 10 B8 04 00 C0 04 00 12 06 4A 04 08 00 40 01 12 14 82 01 11 0A 09 48 69 6D 31 38 38 6D 6F 65 18 06 20 08 28 03 10 8A CA 9D A1 07 1A 00 if (!bot.components[SsoProcessor].firstLoginSucceed) return null - val pbPushMsg = readProtoBuf(MsgOnlinePush.PbPushMsg.serializer()) - - val msgHead = pbPushMsg.msg.msgHead - - val isFromSelfAccount = msgHead.fromUin == bot.id - if (isFromSelfAccount) { - val messageRandom = pbPushMsg.msg.msgBody.richText.attr?.random ?: return null - - if (bot.client.syncingController.pendingGroupMessageReceiptCacheList.contains { it.messageRandom == messageRandom } - || msgHead.fromAppid == 3116 || msgHead.fromAppid == 2021) { - // 3116=group music share - // 2021=group file - // message sent by bot - return SendGroupMessageReceipt( - messageRandom, - msgHead.msgSeq, - msgHead.fromAppid - ) - } - // else: sync form other device - } - - if (msgHead.groupInfo == null) return null - - val group = bot.getGroup(msgHead.groupInfo.groupCode) as GroupImpl? ?: return null // 机器人还正在进群 - - - // fragmented message - val msgs = group.groupPkgMsgParsingCache.tryMerge(pbPushMsg).ifEmpty { return null } - - var extraInfo: ImMsgBody.ExtraInfo? = null - var anonymous: ImMsgBody.AnonymousGroupMsg? = null - - for (msg in msgs) { - for (elem in msg.msg.msgBody.richText.elems) { - when { - elem.extraInfo != null -> extraInfo = elem.extraInfo - elem.anonGroupMsg != null -> anonymous = elem.anonGroupMsg - } - } - } - - - val sender: Member // null if sync from other client - val nameCard: MemberNick - - if (anonymous != null) { // anonymous member - sender = group.newAnonymous(anonymous.anonNick.encodeToString(), anonymous.anonId.encodeBase64()) - nameCard = sender.generateMemberNickFromMember() - } else { // normal member chat - sender = group[msgHead.fromUin] as NormalMemberImpl? ?: kotlin.run { - bot.network.logger.warning { "Failed to find member ${msgHead.fromUin} in group ${group.id}" } - return null - } - nameCard = findSenderName(extraInfo, msgHead.groupInfo) ?: sender.generateMemberNickFromMember() - } - - sender.info?.castOrNull<MemberInfoImpl>()?.run { - lastSpeakTimestamp = currentTimeSeconds().toInt() - } - - if (isFromSelfAccount) { - return GroupMessageSyncEvent( - message = msgs.map { it.msg }.toMessageChainOnline(bot, group.id, GROUP), - time = msgHead.msgTime, - group = group, - sender = sender, - senderName = nameCard.nick, - ) - } else { - - broadcastNameCardChangedEventIfNecessary(sender, nameCard) - - return GroupMessageEvent( - senderName = nameCard.nick, - sender = sender, - message = msgs.map { it.msg }.toMessageChainOnline(bot, group.id, GROUP), - permission = sender.permission, - time = msgHead.msgTime - ) - } - } - - private suspend inline fun broadcastNameCardChangedEventIfNecessary(sender: Member, new: MemberNick) { - if (sender is NormalMemberImpl) { - val currentNameCard = sender.nameCard - if (new.isNameCard) { - new.nick.let { name -> - if (currentNameCard != name) { - sender._nameCard = name - MemberCardChangeEvent(currentNameCard, name, sender).broadcast() - } - } - } else { - // 说明删除了群名片 - if (currentNameCard.isNotEmpty()) { - sender._nameCard = "" - MemberCardChangeEvent(currentNameCard, "", sender).broadcast() - } - } - } + return bot.processPacketThroughPipeline(readProtoBuf(MsgOnlinePush.PbPushMsg.serializer())) } - - private fun findSenderName( - extraInfo: ImMsgBody.ExtraInfo?, - groupInfo: MsgComm.GroupInfo - ): MemberNick? = extraInfo?.groupCard?.takeIf { it.isNotEmpty() }?.decodeCommCardNameBuf()?.let { - MemberNick(it, true) - } ?: groupInfo.takeIf { it.groupCard.isNotEmpty() }?.let { - MemberNick(it.groupCard, it.groupCardType != 2) - } - - private fun ByteArray.decodeCommCardNameBuf() = kotlin.runCatching { - if (this[0] == 0x0A.toByte()) { - val nameBuf = loadAs(Oidb0x8fc.CommCardNameBuf.serializer()) - if (nameBuf.richCardName.isNotEmpty()) { - return@runCatching nameBuf.richCardName.joinToString("") { it.text.encodeToString() } - } - } - return@runCatching null - }.getOrNull() ?: encodeToString() } diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushTransMsg.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushTransMsg.kt index 51ddebf8540..9ef57b5bc4a 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushTransMsg.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushTransMsg.kt @@ -9,33 +9,17 @@ package net.mamoe.mirai.internal.network.protocol.packet.chat.receive -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.cancel import kotlinx.io.core.ByteReadPacket -import kotlinx.io.core.discardExact -import kotlinx.io.core.readUByte -import kotlinx.io.core.readUInt -import net.mamoe.mirai.contact.MemberPermission -import net.mamoe.mirai.contact.NormalMember -import net.mamoe.mirai.event.events.* import net.mamoe.mirai.internal.QQAndroidBot -import net.mamoe.mirai.internal.contact.GroupImpl -import net.mamoe.mirai.internal.contact.NormalMemberImpl -import net.mamoe.mirai.internal.contact.checkIsMemberImpl -import net.mamoe.mirai.internal.contact.info.MemberInfoImpl -import net.mamoe.mirai.internal.contact.newMember -import net.mamoe.mirai.internal.message.contextualBugReportException -import net.mamoe.mirai.internal.network.MultiPacketByIterable import net.mamoe.mirai.internal.network.Packet -import net.mamoe.mirai.internal.network.QQAndroidClient +import net.mamoe.mirai.internal.network.components.NoticeProcessorPipeline.Companion.processPacketThroughPipeline +import net.mamoe.mirai.internal.network.components.SyncController.Companion.syncController +import net.mamoe.mirai.internal.network.components.syncPushTrans import net.mamoe.mirai.internal.network.protocol.data.proto.OnlinePushTrans import net.mamoe.mirai.internal.network.protocol.packet.IncomingPacketFactory import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket import net.mamoe.mirai.internal.network.protocol.packet.buildResponseUniPacket -import net.mamoe.mirai.internal.utils._miraiContentToString import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf -import net.mamoe.mirai.utils.cast -import net.mamoe.mirai.utils.read internal object OnlinePushPbPushTransMsg : @@ -44,275 +28,12 @@ internal object OnlinePushPbPushTransMsg : override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): Packet? { val content = this.readProtoBuf(OnlinePushTrans.PbMsgInfo.serializer()) - - if (!bot.client.syncingController.pbPushTransMsgCacheList.addCache( - content.run { - QQAndroidClient.MessageSvcSyncData.PbPushTransMsgSyncId(msgUid, msgSeq, msgTime) - } - ) - ) { - return null - } - // bot.network.logger.debug { content._miraiContentToString() } - - - content.msgData.read<Unit> { - when (content.msgType) { - 44 -> { - // 3D C4 33 DD 01 FF CD 76 F4 03 C3 7E 2E 34 - // 群转让 - // start with 3D C4 33 DD 01 FF - // 3D C4 33 DD 01 FF C3 7E 2E 34 CD 76 F4 03 - // 权限变更 - // 3D C4 33 DD 01 00/01 ..... - // 3D C4 33 DD 01 01 C3 7E 2E 34 01 - this.discardExact(5) - when (val mode = readUByte().toInt()) { - 0xFF -> { - // 群转让 / huifu.qq.com - // From -> to - val from = readUInt().toLong() - val to = readUInt().toLong() - val results = ArrayList<Packet>() - // println("$from -> $to") - if (to == bot.id) { - if (bot.getGroupByUinOrNull(content.fromUin) == null) { - MessageSvcPbGetMsg.run { - results.add( - BotJoinGroupEvent.Retrieve( - bot.createGroupForBot(content.fromUin)!! - ) - ) - } - } - } - val group = bot.getGroupByUin(content.fromUin) as GroupImpl - if (from == bot.id) { - if (group.botPermission != MemberPermission.MEMBER) - results.add( - BotGroupPermissionChangeEvent( - group, group.botPermission.also { - group.botAsMember.checkIsMemberImpl().permission = - MemberPermission.MEMBER - }, - MemberPermission.MEMBER - ) - ) - } else { - val member = group[from] as NormalMemberImpl - if (member.permission != MemberPermission.MEMBER) { - results.add( - MemberPermissionChangeEvent( - member, - member.permission.also { member.permission = MemberPermission.MEMBER }, - MemberPermission.MEMBER - ) - ) - } - } - if (to == bot.id) { - if (group.botPermission != MemberPermission.OWNER) { - results.add( - BotGroupPermissionChangeEvent( - group, - group.botAsMember.permission.also { - group.botAsMember.checkIsMemberImpl().permission = - MemberPermission.OWNER - }, - MemberPermission.OWNER - ) - ) - } - } else { - val newOwner = (group[to] ?: group.newMember( - MemberInfoImpl( - to, - "", - MemberPermission.OWNER, - "", - "", - "", - 0, - null - ) - )).also { owner -> - owner.checkIsMemberImpl().permission = MemberPermission.OWNER - group.members.delegate.add(owner) - results.add(MemberJoinEvent.Retrieve(owner)) - }.cast<NormalMember>() - - if (newOwner.permission != MemberPermission.OWNER) { - results.add( - MemberPermissionChangeEvent( - newOwner, - newOwner.permission.also { - newOwner.checkIsMemberImpl().permission = MemberPermission.OWNER - }, - MemberPermission.OWNER - ) - ) - } - } - return MultiPacketByIterable(results) - } - else -> { - var var5 = 0L - val target = readUInt().toLong() - if (mode != 0 && mode != 1) { - var5 = readUInt().toLong() - } - - val group = bot.getGroupByUin(content.fromUin) as GroupImpl - - if (var5 == 0L && this.remaining == 1L) {//管理员变更 - val newPermission = - if (this.readByte().toInt() == 1) MemberPermission.ADMINISTRATOR - else MemberPermission.MEMBER - - if (target == bot.id) { - if (group.botPermission == newPermission) { - return null - } - - return BotGroupPermissionChangeEvent( - group, - group.botPermission.also { - group.botAsMember.checkIsMemberImpl().permission = newPermission - }, - newPermission - ) - } else { - val member = group[target] as NormalMemberImpl - if (member.permission == newPermission) { - return null - } - - return MemberPermissionChangeEvent( - member, - member.permission.also { member.permission = newPermission }, - newPermission - ) - } - } - } - } - } - 34 -> { - /* quit - 27 0B 60 E7 - 01 - 2F 55 7C B8 - 82 - 00 30 42 33 32 46 30 38 33 32 39 32 35 30 31 39 33 45 46 32 45 30 36 35 41 35 41 33 42 37 35 43 41 34 46 37 42 38 42 38 42 44 43 35 35 34 35 44 38 30 - */ - /* kick - 27 0B 60 E7 - 01 - A8 32 51 A1 - 83 3E 03 3F A2 06 B4 B4 BD A8 D5 DF 00 30 39 32 46 45 30 36 31 41 33 37 36 43 44 35 37 35 37 39 45 37 32 34 44 37 37 30 36 46 39 39 43 35 35 33 33 31 34 44 32 44 46 35 45 42 43 31 31 36 - */ - readUInt().toLong() // groupCode - readByte().toInt() // follow type - val target = readUInt().toLong() - val type = readUByte().toInt() - val operator = readUInt().toLong() - val groupUin = content.fromUin - - when (type) { - 2, 0x82 -> bot.getGroupByUinOrNull(groupUin)?.let { group -> - if (target == bot.id) { - return BotLeaveEvent.Active(group).also { - group.cancel(CancellationException("Leaved actively")) - bot.groups.delegate.remove(group) - } - } else { - val member = group.get(target) as? NormalMemberImpl ?: return null - return MemberLeaveEvent.Quit(member.also { - member.cancel(CancellationException("Leaved actively")) - group.members.delegate.remove(member) - }) - } - } - 3, 0x83 -> bot.getGroupByUin(groupUin).let { group -> - if (target == bot.id) { - val member = group.members[operator] ?: return@let null - return BotLeaveEvent.Kick(member).also { - group.cancel(CancellationException("Being kicked")) - bot.groups.delegate.remove(group) - } - } else { - val member = group.get(target) as? NormalMemberImpl ?: return null - return MemberLeaveEvent.Kick(member.also { - member.cancel(CancellationException("Being kicked")) - group.members.delegate.remove(member) - }, group.members[operator]) - } - } - } - } - else -> { - when { - content.msgType == 529 && content.msgSubtype == 9 -> { - /* - PbMsgInfo#1773430973 { -fromUin=0x0000000026BA1173(649728371) -generalFlag=0x00000001(1) -msgData=0A 07 70 72 69 6E 74 65 72 10 02 1A CD 02 0A 1F 53 61 6D 73 75 6E 67 20 4D 4C 2D 31 38 36 30 20 53 65 72 69 65 73 20 28 55 53 42 30 30 31 29 0A 16 4F 6E 65 4E 6F 74 65 20 66 6F 72 20 57 69 6E 64 6F 77 73 20 31 30 0A 19 50 68 61 6E 74 6F 6D 20 50 72 69 6E 74 20 74 6F 20 45 76 65 72 6E 6F 74 65 0A 11 4F 6E 65 4E 6F 74 65 20 28 44 65 73 6B 74 6F 70 29 0A 1D 4D 69 63 72 6F 73 6F 66 74 20 58 50 53 20 44 6F 63 75 6D 65 6E 74 20 57 72 69 74 65 72 0A 16 4D 69 63 72 6F 73 6F 66 74 20 50 72 69 6E 74 20 74 6F 20 50 44 46 0A 15 46 6F 78 69 74 20 50 68 61 6E 74 6F 6D 20 50 72 69 6E 74 65 72 0A 03 46 61 78 32 09 0A 03 6A 70 67 10 01 18 00 32 0A 0A 04 6A 70 65 67 10 01 18 00 32 09 0A 03 70 6E 67 10 01 18 00 32 09 0A 03 67 69 66 10 01 18 00 32 09 0A 03 62 6D 70 10 01 18 00 32 09 0A 03 64 6F 63 10 01 18 01 32 0A 0A 04 64 6F 63 78 10 01 18 01 32 09 0A 03 74 78 74 10 00 18 00 32 09 0A 03 70 64 66 10 01 18 01 32 09 0A 03 70 70 74 10 01 18 01 32 0A 0A 04 70 70 74 78 10 01 18 01 32 09 0A 03 78 6C 73 10 01 18 01 32 0A 0A 04 78 6C 73 78 10 01 18 01 -msgSeq=0x00001AFF(6911) -msgSubtype=0x00000009(9) -msgTime=0x5FDF21A3(1608458659) -msgType=0x00000211(529) -msgUid=0x010000005FDEE04C(72057595646369868) -realMsgTime=0x5FDF21A3(1608458659) -svrIp=0x3E689409(1047041033) -toUin=0x0000000026BA1173(649728371) -} - */ - /* - * -printer -Samsung ML-1860 Series (USB001) -OneNote for Windows 10 -Phantom Print to Evernote -OneNote (Desktop) -Microsoft XPS Document Writer -Microsoft Print to PDF -Foxit Phantom Printer -Fax2 -jpg2 - -jpeg2 -png2 -gif2 -bmp2 -doc2 - -docx2 -txt2 -pdf2 -ppt2 - -pptx2 -xls2 - -xlsx*/ - return null - } - } - throw contextualBugReportException( - "解析 OnlinePush.PbPushTransMsg, msgType=${content.msgType}", - content._miraiContentToString(), - null, - "并描述此时机器人是否被踢出, 或是否有成员列表变更等动作." - ) - } - } - } - return null + if (!bot.syncController.syncPushTrans(content)) return null + return bot.processPacketThroughPipeline(content) } - override suspend fun QQAndroidBot.handle(packet: Packet?, sequenceId: Int): OutgoingPacket? { - return buildResponseUniPacket(client, sequenceId = sequenceId) {} + override suspend fun QQAndroidBot.handle(packet: Packet?, sequenceId: Int): OutgoingPacket { + return buildResponseUniPacket(client, sequenceId = sequenceId) } } diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.ReqPush.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.ReqPush.kt index 314110c3d30..26b94bcaf82 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.ReqPush.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.ReqPush.kt @@ -7,898 +7,53 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") -@file:OptIn( - JavaFriendlyAPI::class -) - package net.mamoe.mirai.internal.network.protocol.packet.chat.receive -import kotlinx.coroutines.sync.withLock -import kotlinx.io.core.* -import kotlinx.serialization.Serializable -import kotlinx.serialization.protobuf.ProtoNumber -import net.mamoe.mirai.Mirai -import net.mamoe.mirai.contact.* -import net.mamoe.mirai.data.GroupHonorType -import net.mamoe.mirai.event.events.* +import kotlinx.io.core.ByteReadPacket import net.mamoe.mirai.internal.QQAndroidBot -import net.mamoe.mirai.internal.contact.* -import net.mamoe.mirai.internal.contact.info.FriendInfoImpl -import net.mamoe.mirai.internal.contact.info.MemberInfoImpl -import net.mamoe.mirai.internal.network.MultiPacketBySequence +import net.mamoe.mirai.internal.network.MultiPacket import net.mamoe.mirai.internal.network.Packet -import net.mamoe.mirai.internal.network.QQAndroidClient -import net.mamoe.mirai.internal.network.components.ContactUpdater -import net.mamoe.mirai.internal.network.handler.logger -import net.mamoe.mirai.internal.network.protocol.data.jce.MsgInfo -import net.mamoe.mirai.internal.network.protocol.data.jce.MsgType0x210 +import net.mamoe.mirai.internal.network.components.NoticeProcessorPipeline.Companion.processPacketThroughPipeline import net.mamoe.mirai.internal.network.protocol.data.jce.OnlinePushPack -import net.mamoe.mirai.internal.network.protocol.data.jce.RequestPacket -import net.mamoe.mirai.internal.network.protocol.data.proto.Submsgtype0x115 -import net.mamoe.mirai.internal.network.protocol.data.proto.Submsgtype0x122 -import net.mamoe.mirai.internal.network.protocol.data.proto.Submsgtype0x27.SubMsgType0x27.* -import net.mamoe.mirai.internal.network.protocol.data.proto.Submsgtype0x44.Submsgtype0x44 -import net.mamoe.mirai.internal.network.protocol.data.proto.Submsgtype0xb3 -import net.mamoe.mirai.internal.network.protocol.data.proto.TroopTips0x857 import net.mamoe.mirai.internal.network.protocol.packet.IncomingPacketFactory import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket import net.mamoe.mirai.internal.network.protocol.packet.buildResponseUniPacket -import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList -import net.mamoe.mirai.internal.network.protocol.packet.sendAndExpect -import net.mamoe.mirai.internal.utils.* -import net.mamoe.mirai.internal.utils.io.ProtoBuf -import net.mamoe.mirai.internal.utils.io.serialization.* -import net.mamoe.mirai.utils.* - +import net.mamoe.mirai.internal.utils.io.serialization.readUniPacket +import net.mamoe.mirai.internal.utils.io.serialization.writeJceRequestPacket -//0C 01 B1 89 BE 09 5E 3D 72 A6 00 01 73 68 FC 06 00 00 00 3C internal object OnlinePushReqPush : IncomingPacketFactory<OnlinePushReqPush.ReqPushDecoded>( "OnlinePush.ReqPush", - "OnlinePush.RespPush" + "OnlinePush.RespPush", ) { - // to reduce nesting depth - private suspend fun List<MsgInfo>.deco( - client: QQAndroidClient, - mapper: suspend ByteReadPacket.(msgInfo: MsgInfo) -> Sequence<Packet> - ): Sequence<Packet> { - return mapNotNull { msg -> - val successful = client.syncingController.onlinePushReqPushCacheList.addCache( - QQAndroidClient.MessageSvcSyncData.OnlinePushReqPushSyncId( - uid = msg.lMsgUid ?: 0, sequence = msg.shMsgSeq, time = msg.uMsgTime - ) - ) - if (!successful) return@mapNotNull null - msg.vMsg.read { mapper(msg) } - }.asSequence().flatten() - } - - - @ExperimentalUnsignedTypes - @OptIn(ExperimentalStdlibApi::class) override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): ReqPushDecoded { val reqPushMsg = readUniPacket(OnlinePushPack.SvcReqPushMsg.serializer(), "req") - //bot.network.logger.debug { reqPushMsg._miraiContentToString() } - val packets: Sequence<Packet> = reqPushMsg.vMsgInfos.deco(bot.client) { msgInfo -> - when (msgInfo.shMsgType.toInt()) { - 732 -> { - val group = bot.getGroup(readUInt().toLong()) - ?: return@deco emptySequence() // group has not been initialized - - group.checkIsGroupImpl() - - val internalType = readByte().toInt() - discardExact(1) - - Transformers732[internalType] - ?.let { it(this@deco, group, bot) } - ?: kotlin.run { - bot.network.logger.debug { - "unknown group 732 type $internalType, data: " + readBytes().toUHexString() - } - return@deco emptySequence() - } - } - - // 00 27 1A 0C 1C 2C 3C 4C 5D 00 0C 6D 00 0C 7D 00 0C 8D 00 0C 9C AC BC CC DD 00 0C EC FC 0F 0B 2A 0C 1C 2C 3C 4C 5C 6C 0B 3A 0C 1C 2C 3C 4C 5C 6C 7C 8D 00 0C 9D 00 0C AC BD 00 0C CD 00 0C DC ED 00 0C FC 0F FC 10 0B 4A 0C 1C 2C 3C 4C 5C 6C 7C 8C 96 00 0B 5A 0C 1C 2C 3C 4C 5C 6C 7C 8C 9D 00 0C 0B 6A 0C 1A 0C 1C 26 00 0B 2A 0C 0B 3A 0C 16 00 0B 4A 09 0C 0B 5A 09 0C 0B 0B 7A 0C 1C 2C 36 00 0B 8A 0C 1C 2C 36 00 0B 9A 09 0C 0B AD 00 00 1E 0A 1C 10 28 4A 18 0A 16 08 00 10 A2 FF 8C F0 03 1A 0C E6 BD 9C E6 B1 9F E7 BE A4 E5 8F 8B - 528 -> { - val notifyMsgBody = readJceStruct(MsgType0x210.serializer()) - Transformers528[notifyMsgBody.uSubMsgType] - ?.let { processor -> processor(notifyMsgBody, bot, msgInfo) } - ?: kotlin.run { - bot.network.logger.debug { - "unknown group 528 type 0x${notifyMsgBody.uSubMsgType.toUHexString("")}, data: " + notifyMsgBody.vProtobuf.toUHexString() - } - return@deco emptySequence() - } - } - else -> { - bot.network.logger.debug { "unknown sh type ${msgInfo.shMsgType.toInt()}" } - bot.network.logger.debug { "data=${readBytes().toUHexString()}" } - return@deco emptySequence() - } - } - } - return ReqPushDecoded(reqPushMsg, packets) + return ReqPushDecoded(reqPushMsg, bot.processPacketThroughPipeline(reqPushMsg)) } - @Suppress("SpellCheckingInspection") - internal data class ReqPushDecoded(val request: OnlinePushPack.SvcReqPushMsg, val sequence: Sequence<Packet>) : - MultiPacketBySequence<Packet>(sequence), Packet.NoLog { - override fun toString(): String { - return "OnlinePush.ReqPush.ReqPushDecoded" - } + internal class ReqPushDecoded(val request: OnlinePushPack.SvcReqPushMsg, packet: Packet) : + MultiPacket by MultiPacket(packet), Packet.NoLog { + override fun toString(): String = "OnlinePush.ReqPush.ReqPushDecoded" } override suspend fun QQAndroidBot.handle(packet: ReqPushDecoded, sequenceId: Int): OutgoingPacket { return buildResponseUniPacket(client) { - writeJceStruct( - RequestPacket.serializer(), - RequestPacket( - servantName = "OnlinePush", - funcName = "SvcRespPushMsg", - requestId = sequenceId, - sBuffer = jceRequestSBuffer( - "resp", - OnlinePushPack.SvcRespPushMsg.serializer(), - OnlinePushPack.SvcRespPushMsg( - packet.request.uin, - packet.request.vMsgInfos.map { msg -> - OnlinePushPack.DelMsgInfo( - fromUin = msg.lFromUin, - shMsgSeq = msg.shMsgSeq, - vMsgCookies = msg.vMsgCookies, - uMsgTime = msg.uMsgTime // captured 0 - ) - } + writeJceRequestPacket( + servantName = "OnlinePush", + funcName = "SvcRespPushMsg", + name = "resp", + serializer = OnlinePushPack.SvcRespPushMsg.serializer(), + body = OnlinePushPack.SvcRespPushMsg( + packet.request.uin, + packet.request.vMsgInfos.map { msg -> + OnlinePushPack.DelMsgInfo( + fromUin = msg.lFromUin, + shMsgSeq = msg.shMsgSeq, + vMsgCookies = msg.vMsgCookies, + uMsgTime = msg.uMsgTime, // captured 0 ) - ) - ) - ) - } - } -} - - -internal interface Lambda732 { - operator fun invoke(pk: ByteReadPacket, group: GroupImpl, bot: QQAndroidBot): Sequence<Packet> -} - -internal inline fun lambda732(crossinline block: ByteReadPacket.(GroupImpl, QQAndroidBot) -> Sequence<Packet>): Lambda732 { - return object : Lambda732 { - override fun invoke(pk: ByteReadPacket, group: GroupImpl, bot: QQAndroidBot): Sequence<Packet> { - return block(pk, group, bot) - } - } -} - -private fun handleMuteMemberPacket( - bot: QQAndroidBot, - group: GroupImpl, - operator: NormalMember, - target: Long, - timeSeconds: Int -): Packet? { - if (target == 0L) { - val new = timeSeconds != 0 - if (group.settings.isMuteAllField == new) { - return null - } - group.settings.isMuteAllField = new - return GroupMuteAllEvent(!new, new, group, operator) - } - - if (target == bot.id) { - return when { - group.botMuteRemaining == timeSeconds -> null - timeSeconds == 0 || timeSeconds == 0xFFFF_FFFF.toInt() -> { - group.botAsMember.checkIsMemberImpl()._muteTimestamp = 0 - BotUnmuteEvent(operator) - } - else -> { - group.botAsMember.checkIsMemberImpl()._muteTimestamp = - currentTimeSeconds().toInt() + timeSeconds - BotMuteEvent(timeSeconds, operator) - } - } - } - - val member = group[target] ?: return null - member.checkIsMemberImpl() - - if (member.muteTimeRemaining == timeSeconds) { - return null - } - - member._muteTimestamp = currentTimeSeconds().toInt() + timeSeconds - return if (timeSeconds == 0) MemberUnmuteEvent(member, operator) - else MemberMuteEvent(member, timeSeconds, operator) -} - -private object Transformers732 : Map<Int, Lambda732> by mapOf( - // mute - 0x0c to lambda732 { group: GroupImpl, bot: QQAndroidBot -> - val operatorUin = readUInt().toLong() - if (operatorUin == bot.id) { - return@lambda732 emptySequence() - } - val operator = group[operatorUin] ?: return@lambda732 emptySequence() - readUInt().toLong() // time - val length = readUShort().toInt() - val packetList: MutableList<Packet> = mutableListOf() - repeat(length) { - val target = readUInt().toLong() - val timeSeconds = readUInt() - handleMuteMemberPacket(bot, group, operator, target, timeSeconds.toInt())?.let { - packetList.add(it) - } - } - return@lambda732 packetList.asSequence() - }, - - // anonymous - 0x0e to lambda732 { group: GroupImpl, _: QQAndroidBot -> - // 匿名 - val operator = group[readUInt().toLong()] ?: return@lambda732 emptySequence() - val new = readInt() == 0 - if (group.settings.isAnonymousChatEnabledField == new) { - return@lambda732 emptySequence() - } - - group.settings.isAnonymousChatEnabledField = new - return@lambda732 sequenceOf(GroupAllowAnonymousChatEvent(!new, new, group, operator)) - }, - - //系统提示 - 0x14 to lambda732 { group: GroupImpl, bot: QQAndroidBot -> - - discardExact(1) - val grayTip = readProtoBuf(TroopTips0x857.NotifyMsgBody.serializer()).optGeneralGrayTip - when (grayTip?.templId) { - //戳一戳 - 10043L, 1133L, 1132L, 1134L, 1135L, 1136L -> { - //预置数据,服务器将不会提供己方已知消息 - var action = "" - var from: Member = group.botAsMember - var target: Member = group.botAsMember - var suffix = "" - grayTip.msgTemplParam.map { - Pair(it.name.decodeToString(), it.value.decodeToString()) - }.asSequence().forEach { (key, value) -> - run { - when (key) { - "action_str" -> action = value - "uin_str1" -> from = group[value.toLong()] ?: return@lambda732 emptySequence() - "uin_str2" -> target = group[value.toLong()] ?: return@lambda732 emptySequence() - "suffix_str" -> suffix = value - } - } - } - return@lambda732 sequenceOf( - NudgeEvent( - from = if (from.id == bot.id) bot else from, - target = if (target.id == bot.id) bot else target, - action = action, - suffix = suffix, - subject = group - ) - ) - } - //龙王 - 10093L, 1053L, 1054L -> { - var now: NormalMember = group.botAsMember - var previous: NormalMember? = null - grayTip.msgTemplParam.asSequence().map { - it.name.decodeToString() to it.value.decodeToString() - }.forEach { (key, value) -> - when (key) { - "uin" -> now = group[value.toLong()] ?: return@lambda732 emptySequence() - "uin_last" -> previous = group[value.toLong()] ?: return@lambda732 emptySequence() - } - } - return@lambda732 previous?.let { - sequenceOf( - GroupTalkativeChangeEvent(group, now, it), - MemberHonorChangeEvent.Lose(it, GroupHonorType.TALKATIVE), - MemberHonorChangeEvent.Achieve(now, GroupHonorType.TALKATIVE) - ) - } ?: sequenceOf(MemberHonorChangeEvent.Achieve(now, GroupHonorType.TALKATIVE)) - } - else -> { - bot.network.logger.debug { - "Unknown Transformers528 0x14 template\ntemplId=${grayTip?.templId}\nPermList=${grayTip?.msgTemplParam?._miraiContentToString()}" - } - return@lambda732 emptySequence() - } - } - }, - // 传字符串信息 - 0x10 to lambda732 { group: GroupImpl, bot: QQAndroidBot -> - discardExact(1) - readProtoBuf(TroopTips0x857.NotifyMsgBody.serializer()).let { body -> - when (body.optEnumType) { - 1 -> body.optMsgGraytips?.let { tipsInfo -> - val message = tipsInfo.optBytesContent.decodeToString() - //机器人信息 - if (tipsInfo.robotGroupOpt != 0) { - when (tipsInfo.robotGroupOpt) { - //添加 - 1 -> { - val dataList = message.parseToMessageDataList() - val invitor = dataList.first().let { messageData -> - group.getOrFail(messageData.data.toLong()) - } - val member = dataList.last().let { messageData -> - group.newMember( - MemberInfoImpl( - uin = messageData.data.toLong(), - nick = messageData.text, - permission = MemberPermission.MEMBER, - remark = "", - nameCard = "", - specialTitle = "", - muteTimestamp = 0, - anonymousId = null, - isOfficialBot = true - ) - ).cast<NormalMember>().also { - group.members.delegate.add(it) - } - } - return@lambda732 sequenceOf(MemberJoinEvent.Invite(member, invitor)) - } - //移除 - 2 -> { - message.parseToMessageDataList().first().let { - val member = group.getOrFail(it.data.toLong()) - group.members.delegate.remove(member) - return@lambda732 sequenceOf(MemberLeaveEvent.Quit(member)) - } - } - - else -> { - bot.network.logger.debug { "Unknown robotGroupOpt ${tipsInfo.robotGroupOpt}, message=$message" } - return@lambda732 emptySequence() - } - } - } else when { - message.endsWith("群聊坦白说") -> { - val new = when (message) { - "管理员已关闭群聊坦白说" -> false - "管理员已开启群聊坦白说" -> true - else -> { - bot.network.logger.debug { "Unknown server confess talk messages $message" } - return@lambda732 emptySequence() - } - } - return@lambda732 sequenceOf( - GroupAllowConfessTalkEvent( - new, - !new, - group, - false - ) - ) - } - else -> { - bot.network.logger.debug { "Unknown server messages $message" } - return@lambda732 emptySequence() - } - } - } - else -> { - bot.network.logger.debug { - "Unknown Transformers732 0x10 optEnumType\noptEnumType=${body.optEnumType}\ncontent=${body._miraiContentToString()}" - } - return@lambda732 emptySequence() - } - } ?: return@lambda732 emptySequence() - } - /* - val dataBytes = readBytes(26) - - when (dataBytes[0].toInt() and 0xFF) { - 59 -> { // TODO 应该在 Transformers528 处理 - val size = readByte().toInt() // orthodox, don't `readUByte` - if (size < 0) { - // java.lang.IllegalStateException: negative array size: -100, remaining bytes=B0 E6 99 90 D8 E8 02 98 06 01 - // java.lang.IllegalStateException: negative array size: -121, remaining bytes=03 10 D9 F7 A2 93 0D 18 E0 DB E8 CA 0B 32 22 61 34 64 31 34 64 61 64 65 65 38 32 32 34 62 64 32 35 34 65 63 37 62 62 30 33 30 66 61 36 66 61 6D 6A 38 0E 48 00 58 01 70 C8 E8 9B 07 7A AD 02 3C 7B 22 69 63 6F 6E 22 3A 22 71 71 77 61 6C 6C 65 74 5F 63 75 73 74 6F 6D 5F 74 69 70 73 5F 69 64 69 6F 6D 5F 69 63 6F 6E 2E 70 6E 67 22 2C 22 61 6C 74 22 3A 22 22 7D 3E 3C 7B 22 63 6D 64 22 3A 31 2C 22 64 61 74 61 22 3A 22 6C 69 73 74 69 64 3D 31 30 30 30 30 34 35 32 30 31 32 30 30 34 30 38 31 32 30 30 31 30 39 36 31 32 33 31 34 35 30 30 26 67 72 6F 75 70 74 79 70 65 3D 31 22 2C 22 74 65 78 74 43 6F 6C 6F 72 22 3A 22 30 78 38 37 38 42 39 39 22 2C 22 74 65 78 74 22 3A 22 E6 8E A5 E9 BE 99 E7 BA A2 E5 8C 85 E4 B8 8B E4 B8 80 E4 B8 AA E6 8B BC E9 9F B3 EF BC 9A 22 7D 3E 3C 7B 22 63 6D 64 22 3A 31 2C 22 64 61 74 61 22 3A 22 6C 69 73 74 69 64 3D 31 30 30 30 30 34 35 32 30 31 32 30 30 34 30 38 31 32 30 30 31 30 39 36 31 32 33 31 34 35 30 30 26 67 72 6F 75 70 74 79 70 65 3D 31 22 2C 22 74 65 78 74 43 6F 6C 6F 72 22 3A 22 30 78 45 36 32 35 35 35 22 2C 22 74 65 78 74 22 3A 22 64 69 6E 67 22 7D 3E 82 01 0C E8 80 81 E5 83 A7 E5 85 A5 E5 AE 9A 88 01 03 92 01 04 64 69 6E 67 A0 01 00 - // negative array size: -40, remaining bytes=D6 94 C3 8C D8 E8 02 98 06 01 - error("negative array size: $size, remaining bytes=${readBytes().toUHexString()}") - } - - // println(dataBytes.toUHexString()) - //println(message + ":" + dataBytes.toUHexString()) - - val new = when (val message = readString(size)) { - "管理员已关闭群聊坦白说" -> false - "管理员已开启群聊坦白说" -> true - else -> { - bot.network.logger.debug { "Unknown server messages $message" } - return@lambda732 emptySequence() - } - } - - // @Suppress("DEPRECATION") - // if (group.settings.isConfessTalkEnabled == new) { - // return@lambda732 emptySequence() - // } - - return@lambda732 sequenceOf( - GroupAllowConfessTalkEvent( - new, - false, - group, - false - ) - ) - } - - 0x2D -> { - // 修改群名. 在 Transformers528 0x27L 处理 - return@lambda732 emptySequence() - } - else -> { - /* - bot.network.logger.debug("unknown Transformer732 0xunknown type: ${dataBytes[0].toString(16) - .uppercase()}") - bot.network.logger.debug("unknown Transformer732 0xdata= ${readBytes().toUHexString()}") - */ - return@lambda732 emptySequence() - - /* - if (group.name == message) { - return@lambda732 emptySequence() - } - - return@lambda732 sequenceOf( - GroupNameChangeEvent( - group.name.also { group._name = message }, - message, group, false - ) - )*/ - } - }*/ - }, - - // recall - 0x11 to lambda732 { group: GroupImpl, bot: QQAndroidBot -> - discardExact(1) - val proto = readProtoBuf(TroopTips0x857.NotifyMsgBody.serializer()) - - val recallReminder = proto.optMsgRecall ?: return@lambda732 emptySequence() - - val operator = - if (recallReminder.uin == bot.id) group.botAsMember - else group[recallReminder.uin] ?: return@lambda732 emptySequence() - val firstPkg = recallReminder.recalledMsgList.firstOrNull() ?: return@lambda732 emptySequence() - - return@lambda732 when { - firstPkg.authorUin == bot.id && operator.id == bot.id -> emptySequence() - else -> sequenceOf( - MessageRecallEvent.GroupRecall( - bot, - firstPkg.authorUin, - recallReminder.recalledMsgList.mapToIntArray { it.seq }, - recallReminder.recalledMsgList.mapToIntArray { it.msgRandom }, - firstPkg.time, - operator, - group, - group[firstPkg.authorUin] ?: return@lambda732 emptySequence() - ) + }, + ), ) } } -) - -internal val ignoredLambda528: Lambda528 = lambda528 { _, _ -> emptySequence() } - -internal interface Lambda528 { - suspend operator fun invoke(msg: MsgType0x210, bot: QQAndroidBot, msgInfo: MsgInfo): Sequence<Packet> -} - -@kotlin.internal.LowPriorityInOverloadResolution -internal inline fun lambda528(crossinline block: suspend MsgType0x210.(QQAndroidBot) -> Sequence<Packet>): Lambda528 { - return object : Lambda528 { - override suspend fun invoke(msg: MsgType0x210, bot: QQAndroidBot, msgInfo: MsgInfo): Sequence<Packet> { - return block(msg, bot) - } - - } -} - -internal inline fun lambda528(crossinline block: suspend MsgType0x210.(QQAndroidBot, MsgInfo) -> Sequence<Packet>): Lambda528 { - return object : Lambda528 { - override suspend fun invoke(msg: MsgType0x210, bot: QQAndroidBot, msgInfo: MsgInfo): Sequence<Packet> { - return block(msg, bot, msgInfo) - } - - } -} - -@Serializable -private class Wording( - @ProtoNumber(1) val itemID: Int = 0, - @ProtoNumber(2) val itemName: String = "" -) : ProtoBuf - -@Serializable -private class Sub8AMsgInfo( - @ProtoNumber(1) val fromUin: Long, - @ProtoNumber(2) val botUin: Long, - @ProtoNumber(3) val srcId: Int, - @ProtoNumber(4) val srcInternalId: Long, - @ProtoNumber(5) val time: Long, - @ProtoNumber(6) val random: Int, - @ProtoNumber(7) val pkgNum: Int, // 1 - @ProtoNumber(8) val pkgIndex: Int, // 0 - @ProtoNumber(9) val devSeq: Int, // 0 - @ProtoNumber(12) val flag: Int, // 1 - @ProtoNumber(13) val wording: Wording -) : ProtoBuf - -@Serializable -private class Sub8A( - @ProtoNumber(1) val msgInfo: List<Sub8AMsgInfo>, - @ProtoNumber(2) val appId: Int, // 1 - @ProtoNumber(3) val instId: Int, // 1 - @ProtoNumber(4) val longMessageFlag: Int, // 0 - @ProtoNumber(5) val reserved: ByteArray? = null // struct{ boolean(1), boolean(2) } -) : ProtoBuf - - -// uSubMsgType to vProtobuf -// 138 or 139: top_package/akln.java:1568 -// 66: top_package/nhz.java:269 -/** - * @see MsgType0x210 - */ -@OptIn(ExperimentalStdlibApi::class) -internal object Transformers528 : Map<Long, Lambda528> by mapOf( - - 0x8AL to lambda528 { bot -> - - return@lambda528 vProtobuf.loadAs(Sub8A.serializer()).msgInfo.asSequence() - .filter { it.botUin == bot.id }.mapNotNull { info -> - MessageRecallEvent.FriendRecall( - bot = bot, - messageIds = intArrayOf(info.srcId), - messageInternalIds = intArrayOf(info.srcInternalId.toInt()), - messageTime = info.time.toInt(), - operatorId = info.fromUin, - operator = bot.getFriend(info.fromUin) ?: return@mapNotNull null - ) - } - }, - - // Network(1994701021) 16:03:54 : unknown group 528 type 0x0000000000000026, data: 08 01 12 40 0A 06 08 F4 EF BB 8F 04 10 E7 C1 AD B8 02 18 01 22 2C 10 01 1A 1A 18 B4 DC F8 9B 0C 20 E7 C1 AD B8 02 28 06 30 02 A2 01 04 08 93 D6 03 A8 01 08 20 00 28 00 32 08 18 01 20 FE AF AF F5 05 28 00 - // VIP 进群提示 - 0x26L to ignoredLambda528, - // 提示共同好友 - 0x111L to ignoredLambda528, - // 新好友 - 0xB3L to lambda528 { bot -> - // 08 01 12 52 08 A2 FF 8C F0 03 10 00 1D 15 3D 90 5E 22 2E E6 88 91 E4 BB AC E5 B7 B2 E7 BB 8F E6 98 AF E5 A5 BD E5 8F 8B E5 95 A6 EF BC 8C E4 B8 80 E8 B5 B7 E6 9D A5 E8 81 8A E5 A4 A9 E5 90 A7 21 2A 09 48 69 6D 31 38 38 6D 6F 65 30 07 38 03 48 DD F1 92 B7 07 - val body = vProtobuf.loadAs(Submsgtype0xb3.SubMsgType0xb3.MsgBody.serializer()) - val new = Mirai.newFriend( - bot, FriendInfoImpl( - uin = body.msgAddFrdNotify.fuin, - nick = body.msgAddFrdNotify.fuinNick, - remark = "", - ) - ) - bot.friends.delegate.add(new) - return@lambda528 bot.getStranger(new.id)?.let { - bot.strangers.remove(new.id) - sequenceOf(StrangerRelationChangeEvent.Friended(it, new), FriendAddEvent(new)) - } ?: sequenceOf(FriendAddEvent(new)) - }, - 0xE2L to lambda528 { _ -> - // TODO: unknown. maybe messages. - // 0A 35 08 00 10 A2 FF 8C F0 03 1A 1B E5 90 8C E6 84 8F E4 BD A0 E7 9A 84 E5 8A A0 E5 A5 BD E5 8F 8B E8 AF B7 E6 B1 82 22 0C E6 BD 9C E6 B1 9F E7 BE A4 E5 8F 8B 28 01 - // vProtobuf.loadAs(Msgtype0x210.serializer()) - - return@lambda528 emptySequence() - }, - 0x44L to lambda528 { bot -> - val msg = vProtobuf.loadAs(Submsgtype0x44.MsgBody.serializer()) - - val packetList = mutableListOf<Packet>() - if (msg.msgFriendMsgSync != null) { - when (msg.msgFriendMsgSync.processtype) { - 3, 9, 10 -> { - if (bot.getFriend(msg.msgFriendMsgSync.fuin) == null) { - val response: FriendList.GetFriendGroupList.Response = - FriendList.GetFriendGroupList.forSingleFriend( - bot.client, - msg.msgFriendMsgSync.fuin - ).sendAndExpect(bot) - response.friendList.firstOrNull()?.let { - val friend = Mirai.newFriend(bot, it.toMiraiFriendInfo()) - bot.friends.delegate.add(friend) - packetList.add(FriendAddEvent(friend)) - } - } - } - } - } - if (msg.msgGroupMsgSync != null) { - when (msg.msgGroupMsgSync.msgType) { - 1, 2 -> { - bot.components[ContactUpdater].groupListModifyLock.withLock { - bot.createGroupForBot(Mirai.calculateGroupUinByGroupCode(msg.msgGroupMsgSync.grpCode))?.let { - packetList.add(BotJoinGroupEvent.Active(it)) - } - } - } - } - } - return@lambda528 packetList.asSequence() - }, - // bot 在其他客户端被踢或主动退出而同步情况 - 0xD4L to lambda528 { _ -> - // this.soutv("0x210") - /* @Serializable - data class SubD4( - // ok - val uin: Long - ) : ProtoBuf - - val uin = vProtobuf.loadAs(SubD4.serializer()).uin - val group = bot.getGroupByUinOrNull(uin) ?: bot.getGroupOrNull(uin) - return@lambda528 if (group != null && bot.groups.delegate.remove(group)) { - group.cancel(CancellationException("Being kicked")) - sequenceOf(BotLeaveEvent.Active(group)) - } else emptySequence()*/ - - //ignore - return@lambda528 emptySequence() - }, - //戳一戳信息等 - 0x122L to lambda528 { bot, msgInfo -> - val body = vProtobuf.loadAs(Submsgtype0x122.Submsgtype0x122.MsgBody.serializer()) - when (body.templId) { - //戳一戳 - 1132L, 1133L, 1134L, 1135L, 1136L, 10043L -> { - //预置数据,服务器将不会提供己方已知消息 - var from: User? = null - var action = "" - var target: User? = null - var suffix = "" - body.msgTemplParam.asSequence().map { param -> - param.name.decodeToString() to param.value.decodeToString() - }.forEach { (key, value) -> - when (key) { - "action_str" -> action = value - "uin_str1" -> from = bot.getFriend(value.toLong()) ?: bot.getStranger(value.toLong()) - ?: return@lambda528 emptySequence() - "uin_str2" -> target = bot.getFriend(value.toLong()) ?: bot.getStranger(value.toLong()) - ?: return@lambda528 emptySequence() - "suffix_str" -> suffix = value - } - } - - val subject: User = bot.getFriend(msgInfo.lFromUin) - ?: bot.getStranger(msgInfo.lFromUin) - ?: return@lambda528 emptySequence() - - sequenceOf( - when { - target == null && from == null || target?.id == from?.id && from?.id == bot.id -> { - //机器人自己戳自己 - NudgeEvent(from = bot, target = bot, subject = subject, action, suffix) - } - target == null || target!!.id == bot.id -> { - //机器人自身为目标 - NudgeEvent(from = subject, target = bot, subject = subject, action, suffix) - } - from == null || from!!.id == bot.id -> { - //机器人自身为发起者 - NudgeEvent(from = bot, target = subject, subject = subject, action, suffix) - } - else -> NudgeEvent(from = subject, target = subject, subject = subject, action, suffix) - } - ) - } - else -> { - bot.logger.debug { - "Unknown Transformers528 0x122L template\ntemplId=${body.templId}\nPermList=${body.msgTemplParam._miraiContentToString()}" - } - return@lambda528 emptySequence() - } - } - }, - //好友输入状态 - 0x115L to lambda528 { bot -> - val body = vProtobuf.loadAs(Submsgtype0x115.SubMsgType0x115.MsgBody.serializer()) - val friend = bot.getFriend(body.fromUin) - val item = body.msgNotifyItem - return@lambda528 if (friend != null && item != null) { - sequenceOf(FriendInputStatusChangedEvent(friend, item.eventType == 1)) - } else { - emptySequence() - } - }, - // 群相关, ModFriendRemark, DelFriend, ModGroupProfile - 0x27L to lambda528 { bot -> - fun ModFriendRemark.transform(bot: QQAndroidBot): Sequence<Packet> { - return this.msgFrdRmk.asSequence().mapNotNull { - val friend = bot.getFriend(it.fuin) ?: return@mapNotNull null - val old: String - friend.checkIsFriendImpl().friendInfo - .also { info -> old = info.remark } - .remark = it.rmkName - // TODO: 2020/4/10 ADD REMARK QUERY - FriendRemarkChangeEvent(friend, old, it.rmkName) - } - } - - fun DelFriend.transform(bot: QQAndroidBot): Sequence<Packet> { - return this.uint64Uins.asSequence().mapNotNull { - - val friend = bot.getFriend(it) ?: return@mapNotNull null - if (bot.friends.delegate.remove(friend)) { - FriendDeleteEvent(friend) - } else null - } - } - - fun ModGroupProfile.transform(bot: QQAndroidBot): Sequence<Packet> { - return this.msgGroupProfileInfos.asSequence().mapNotNull { info -> - when (info.field) { - 1 -> { - // 群名 - val new = info.value.encodeToString() - - val group = bot.getGroup(this.groupCode) ?: return@mapNotNull null - group.checkIsGroupImpl() - val old = group.name - - if (new == old) return@mapNotNull null - - val operator = if (this.cmdUin == bot.id) null - else group[this.cmdUin] ?: return@mapNotNull null - - group.settings.nameField = new - - return@mapNotNull GroupNameChangeEvent(old, new, group, operator) - } - 2 -> { - // 头像 - // top_package/akkz.java:3446 - /* - var4 = var82.byteAt(0); - short var3 = (short) (var82.byteAt(1) | var4 << 8); - var85 = var18.method_77927(var7 + ""); - var85.troopface = var3; - var85.hasSetNewTroopHead = true; - */ -// bot.logger.debug( -// contextualBugReportException( -// "解析 Transformers528 0x27L ModGroupProfile 群头像修改", -// forDebug = "this=${this._miraiContentToString()}" -// ) -// ) - null - } - 3 -> { // troop.credit.data - // top_package/akkz.java:3475 - // top_package/akkz.java:3498 -// bot.logger.debug( -// contextualBugReportException( -// "解析 Transformers528 0x27L ModGroupProfile 群 troop.credit.data", -// forDebug = "this=${this._miraiContentToString()}" -// ) -// ) - null - } - - else -> null - } - } - } - - fun ModGroupMemberProfile.transform(bot: QQAndroidBot): Sequence<Packet> { - return this.msgGroupMemberProfileInfos.asSequence().mapNotNull { info -> - when (info.field) { - 1 -> { // name card - val new = info.value - val group = bot.getGroup(this.groupCode) ?: return@mapNotNull null - group.checkIsGroupImpl() - val member = group[this.uin] ?: return@mapNotNull null - member.checkIsMemberImpl() - - val old = member.nameCard - - if (new == old) return@mapNotNull null - member._nameCard = new - - return@mapNotNull MemberCardChangeEvent(old, new, member) - } - 2 -> { - if (info.value.singleOrNull()?.code != 0) { - bot.logger.debug { - "Unknown Transformers528 0x27L ModGroupMemberProfile, field=${info.field}, value=${info.value}" - } - } - return@mapNotNull null - } - else -> { - bot.logger.debug { - "Unknown Transformers528 0x27L ModGroupMemberProfile, field=${info.field}, value=${info.value}" - } - return@mapNotNull null - } - } - } - } - - fun ModCustomFace.transform(bot: QQAndroidBot): Sequence<Packet> { - if (uin == bot.id) { - return sequenceOf(BotAvatarChangedEvent(bot)) - } - val friend = bot.getFriend(uin) ?: return emptySequence() - return sequenceOf(FriendAvatarChangedEvent(friend)) - } - - fun ModProfile.transform(bot: QQAndroidBot): Sequence<Packet> = buildList<Packet> { - var containsUnknown = false - msgProfileInfos.forEach { modified -> - when (modified.field) { - 20002 -> { // 昵称修改 - val value = modified.value - val to = value.encodeToString() - if (uin == bot.id) { - val from = bot.nick - if (from != to) { - bot.nick = to - bot.asFriend.checkIsFriendImpl().nick = to - add(BotNickChangedEvent(bot, from, to)) - } - } else { - val friend = (bot.getFriend(uin) ?: return@forEach) as FriendImpl - val info = friend.friendInfo - val from = info.nick - when (info) { - is FriendInfoImpl -> info.nick = to - else -> { - bot.network.logger.debug { - "Unknown how to update nick for $info" - } - } - } - add(FriendNickChangedEvent(friend, from, to)) - } - } - else -> { - containsUnknown = true - } - } - } - if (msgProfileInfos.isEmpty() || containsUnknown) { - bot.network.logger.debug { - "Transformers528 0x27L: new data: ${_miraiContentToString()}" - } - } - }.asSequence() - - return@lambda528 vProtobuf.loadAs(SubMsgType0x27MsgBody.serializer()).msgModInfos.asSequence() - .flatMap { - when { - it.msgModFriendRemark != null -> it.msgModFriendRemark.transform(bot) - it.msgDelFriend != null -> it.msgDelFriend.transform(bot) - it.msgModGroupProfile != null -> it.msgModGroupProfile.transform(bot) - it.msgModGroupMemberProfile != null -> it.msgModGroupMemberProfile.transform(bot) - it.msgModCustomFace != null -> it.msgModCustomFace.transform(bot) - it.msgModProfile != null -> it.msgModProfile.transform(bot) - else -> { - bot.network.logger.debug { - "Transformers528 0x27L: new data: ${it._miraiContentToString()}" - } - emptySequence() - } - } - } - // 0A 1C 10 28 4A 18 0A 16 08 00 10 A2 FF 8C F0 03 1A 0C E6 BD 9C E6 B1 9F E7 BE A4 E5 8F 8B - } -) +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.SidExpired.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.SidExpired.kt index 7d8f181efd5..d13a3bd9268 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.SidExpired.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.SidExpired.kt @@ -22,14 +22,9 @@ import net.mamoe.mirai.internal.network.protocol.packet.sendAndExpect internal object OnlinePushSidExpired : IncomingPacketFactory<Packet?>("OnlinePush.SidTicketExpired") { override suspend fun QQAndroidBot.handle(packet: Packet?, sequenceId: Int): OutgoingPacket { - return buildResponseUniPacket( - client, - sequenceId = sequenceId, - key = client.wLoginSigInfo.d2Key - ) {}.also { - WtLogin10(client, mainSigMap = 3554528).sendAndExpect(bot) - StatSvc.Register.online(client).sendAndExpect(bot) - } + WtLogin10(client, mainSigMap = 3554528).sendAndExpect(bot) + StatSvc.Register.online(client).sendAndExpect(bot) + return buildResponseUniPacket(client, sequenceId = sequenceId) } override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): Packet? { diff --git a/mirai-core/src/commonMain/kotlin/utils/AtomicIntSeq.kt b/mirai-core/src/commonMain/kotlin/utils/AtomicIntSeq.kt new file mode 100644 index 00000000000..e29aff6b540 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/utils/AtomicIntSeq.kt @@ -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.internal.utils + +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.update +import net.mamoe.mirai.utils.getRandomUnsignedInt +import net.mamoe.mirai.utils.toLongUnsigned + +// We probably can reduce duplicates by using value classes, but atomicFU compiler might not be able to compile it. + +// TODO: 2021/6/27 tests +internal class AtomicIntSeq private constructor( + initial: Int, + private val maxExclusive: Int, +) { + private val value = atomic(initial) + + /** + * Increment [value] within the range from 0 (inclusive) to [maxExclusive] (exclusive). + */ + fun next(): Int = value.incrementAndGet().mod(maxExclusive) // positive + + /** + * Atomically update [value] if it is smaller than [new]. + */ + fun updateIfSmallerThan(new: Int): Boolean { + value.update { instant -> + if (instant < new) new else return false + } + return true + } + + fun updateIfDifferentWith(new: Int): Boolean { + value.update { instant -> + if (instant == new) return false + new + } + return true + } + + companion object { + @JvmStatic + fun forMessageSeq() = AtomicIntSeq(0, Int.MAX_VALUE) + + @JvmStatic + fun forPrivateSync() = AtomicIntSeq(getRandomUnsignedInt(), 65535) + } +} + +// TODO: 2021/6/27 tests +internal class AtomicLongSeq( + initial: Long = getRandomUnsignedInt().toLongUnsigned(), + private val maxExclusive: Long = 65535, +) { + private val value = atomic(initial) + + /** + * Increment [value] within the range from 0 (inclusive) to [maxExclusive] (exclusive). + */ + fun next(): Long = value.incrementAndGet().mod(maxExclusive) // positive + + /** + * Atomically update [value] if it is smaller than [new]. + */ + fun updateIfSmallerThan(new: Long) { + value.update { instant -> + if (instant < new) new else return + } + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/utils/io/ProtoBuf.kt b/mirai-core/src/commonMain/kotlin/utils/io/ProtoBuf.kt deleted file mode 100644 index 1c80e510e64..00000000000 --- a/mirai-core/src/commonMain/kotlin/utils/io/ProtoBuf.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * 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/dev/LICENSE - */ - -package net.mamoe.mirai.internal.utils.io - -/** - * 仅有标示作用 - */ -internal interface ProtoBuf diff --git a/mirai-core/src/commonMain/kotlin/utils/io/ProtocolStruct.kt b/mirai-core/src/commonMain/kotlin/utils/io/ProtocolStruct.kt new file mode 100644 index 00000000000..c3692f32854 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/utils/io/ProtocolStruct.kt @@ -0,0 +1,26 @@ +/* + * 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.utils.io + +import kotlin.reflect.KClass + +internal interface ProtocolStruct +internal interface ProtoBuf : ProtocolStruct +internal interface JceStruct : ProtocolStruct + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.VALUE_PARAMETER) +internal annotation class NestedStructure( + val serializer: KClass<out NestedStructureDesensitizer<*, *>> +) + +internal interface NestedStructureDesensitizer<in C : ProtocolStruct, T : ProtocolStruct> { + fun deserialize(context: C, byteArray: ByteArray): T? +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/utils/io/serialization/utils.kt b/mirai-core/src/commonMain/kotlin/utils/io/serialization/utils.kt index 925fabdee71..dfc9f094aec 100644 --- a/mirai-core/src/commonMain/kotlin/utils/io/serialization/utils.kt +++ b/mirai-core/src/commonMain/kotlin/utils/io/serialization/utils.kt @@ -71,8 +71,8 @@ internal fun <T : JceStruct> BytePacketBuilder.writeJceRequestPacket( version = version.toShort(), servantName = servantName, funcName = funcName, - sBuffer = jceRequestSBuffer(name, serializer, body) - ) + sBuffer = jceRequestSBuffer(name, serializer, body), + ), ) /** @@ -110,16 +110,18 @@ private fun <K, V> Map<K, V>.singleValue(): V = this.entries.single().value internal fun <R> ByteReadPacket.decodeUniRequestPacketAndDeserialize(name: String? = null, block: (ByteArray) -> R): R { val request = this.readJceStruct(RequestPacket.serializer()) - return block(if (name == null) when (request.version?.toInt() ?: 3) { - 2 -> request.sBuffer.loadAs(RequestDataVersion2.serializer()).map.singleValue().singleValue() - 3 -> request.sBuffer.loadAs(RequestDataVersion3.serializer()).map.singleValue() - else -> error("unsupported version ${request.version}") - } else when (request.version?.toInt() ?: 3) { - 2 -> request.sBuffer.loadAs(RequestDataVersion2.serializer()).map.getOrElse(name) { error("cannot find $name") } - .singleValue() - 3 -> request.sBuffer.loadAs(RequestDataVersion3.serializer()).map.getOrElse(name) { error("cannot find $name") } - else -> error("unsupported version ${request.version}") - }) + return block( + if (name == null) when (request.version?.toInt() ?: 3) { + 2 -> request.sBuffer.loadAs(RequestDataVersion2.serializer()).map.singleValue().singleValue() + 3 -> request.sBuffer.loadAs(RequestDataVersion3.serializer()).map.singleValue() + else -> error("unsupported version ${request.version}") + } else when (request.version?.toInt() ?: 3) { + 2 -> request.sBuffer.loadAs(RequestDataVersion2.serializer()).map.getOrElse(name) { error("cannot find $name") } + .singleValue() + 3 -> request.sBuffer.loadAs(RequestDataVersion3.serializer()).map.getOrElse(name) { error("cannot find $name") } + else -> error("unsupported version ${request.version}") + }, + ) } internal fun <T : JceStruct> T.toByteArray( @@ -143,8 +145,8 @@ internal fun <T : ProtoBuf> BytePacketBuilder.writeOidb( command = command, serviceType = serviceType, clientVersion = clientVersion, - bodybuffer = v.toByteArray(serializer) - ) + bodybuffer = v.toByteArray(serializer), + ), ) } @@ -158,7 +160,11 @@ internal fun <T : ProtoBuf> T.toByteArray(serializer: SerializationStrategy<T>): /** * load */ -internal fun <T : ProtoBuf> ByteArray.loadAs(deserializer: DeserializationStrategy<T>): T { +internal fun <T : ProtoBuf> ByteArray.loadAs(deserializer: DeserializationStrategy<T>, offset: Int = 0): T { + if (offset != 0) { + require(offset in offset..this.lastIndex) { "invalid offset: $offset" } + return this.copyOfRange(offset, this.size).loadAs(deserializer) + } return KtProtoBuf.decodeFromByteArray(deserializer, this) } @@ -235,8 +241,8 @@ internal fun <T : JceStruct> jceRequestSBuffer( ): ByteArray { return RequestDataVersion3( mapOf( - name to JCE_STRUCT_HEAD_OF_TAG_0 + jceStruct.toByteArray(serializer) + JCE_STRUCT_TAIL_OF_TAG_0 - ) + name to JCE_STRUCT_HEAD_OF_TAG_0 + jceStruct.toByteArray(serializer) + JCE_STRUCT_TAIL_OF_TAG_0, + ), ).toByteArray(RequestDataVersion3.serializer()) } diff --git a/mirai-core/src/commonMain/kotlin/utils/string.kt b/mirai-core/src/commonMain/kotlin/utils/string.kt index a29b4d1e1a1..e36aa5f1ebf 100644 --- a/mirai-core/src/commonMain/kotlin/utils/string.kt +++ b/mirai-core/src/commonMain/kotlin/utils/string.kt @@ -11,6 +11,8 @@ package net.mamoe.mirai.internal.utils import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json +import net.mamoe.mirai.contact.MemberPermission +import net.mamoe.mirai.internal.contact.info.MemberInfoImpl import net.mamoe.mirai.utils.MiraiInternalApi @Serializable @@ -20,6 +22,18 @@ internal data class MessageData( val text: String, ) +internal fun MessageData.toMemberInfo() = MemberInfoImpl( + uin = data.toLong(), + nick = text, + permission = MemberPermission.MEMBER, + remark = "", + nameCard = "", + specialTitle = "", + muteTimestamp = 0, + anonymousId = null, + isOfficialBot = true +) + @Suppress("RegExpRedundantEscape") internal val extraJsonPattern = Regex("<(\\{.*?\\})>") diff --git a/mirai-core/src/commonTest/kotlin/MockBot.kt b/mirai-core/src/commonTest/kotlin/MockBot.kt index 4dfccad9583..2ab45a2367d 100644 --- a/mirai-core/src/commonTest/kotlin/MockBot.kt +++ b/mirai-core/src/commonTest/kotlin/MockBot.kt @@ -45,9 +45,9 @@ internal class MockBotBuilder( } @Suppress("TestFunctionName") -internal fun MockBot(conf: MockBotBuilder.() -> Unit = {}): QQAndroidBot { +internal fun MockBot(account: BotAccount = MockAccount, conf: MockBotBuilder.() -> Unit = {}): QQAndroidBot { return MockBotBuilder(MockConfiguration.copy()).apply(conf).run { - object : QQAndroidBot(MockAccount, this.conf) { + object : QQAndroidBot(account, this.conf) { override fun createBotLevelComponents(): ConcurrentComponentStorage { return super.createBotLevelComponents().apply { val componentsProvider = additionalComponentsProvider diff --git a/mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt b/mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt index 5495a4992c9..2efe8f03624 100644 --- a/mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt +++ b/mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt @@ -9,6 +9,7 @@ package net.mamoe.mirai.internal.network.framework +import net.mamoe.mirai.internal.BotAccount import net.mamoe.mirai.internal.MockAccount import net.mamoe.mirai.internal.MockConfiguration import net.mamoe.mirai.internal.QQAndroidBot @@ -43,8 +44,10 @@ internal sealed class AbstractRealNetworkHandlerTest<H : NetworkHandler> : Abstr abstract val factory: NetworkHandlerFactory<H> abstract val network: H - var bot: QQAndroidBot by lateinitMutableProperty { - object : QQAndroidBot(MockAccount, MockConfiguration.copy()) { + var bot: QQAndroidBot by lateinitMutableProperty { createBot() } + + protected open fun createBot(account: BotAccount = MockAccount): QQAndroidBot { + return object : QQAndroidBot(account, MockConfiguration.copy()) { override fun createBotLevelComponents(): ConcurrentComponentStorage = super.createBotLevelComponents().apply { setAll(overrideComponents) } diff --git a/mirai-core/src/commonTest/kotlin/notice/Desensitizer.kt b/mirai-core/src/commonTest/kotlin/notice/Desensitizer.kt new file mode 100644 index 00000000000..179ab97218a --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/notice/Desensitizer.kt @@ -0,0 +1,204 @@ +/* + * 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.notice + +import kotlinx.serialization.decodeFromString +import net.mamoe.mirai.Mirai +import net.mamoe.mirai.internal.notice.Desensitizer.Companion.generateAndDesensitize +import net.mamoe.mirai.internal.utils.codegen.* +import net.mamoe.mirai.internal.utils.io.NestedStructure +import net.mamoe.mirai.internal.utils.io.NestedStructureDesensitizer +import net.mamoe.mirai.internal.utils.io.ProtocolStruct +import net.mamoe.mirai.utils.* +import net.mamoe.yamlkt.Yaml +import net.mamoe.yamlkt.YamlBuilder +import kotlin.reflect.KType +import kotlin.reflect.full.createInstance +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.typeOf + +private val logger: MiraiLogger by lazy { MiraiLogger.Factory.create(Desensitizer::class) } + +internal class Desensitizer private constructor( + val rules: Map<String, String>, +) { + fun desensitize(value: String): String { + return rules.entries.fold(value) { acc, entry -> + acc.replace(entry.key, entry.value) + } + } + + fun desensitize(value: ByteArray): ByteArray { + return desensitize(value.toUHexString()).hexToBytes() + } + + fun desensitize(value: Array<Byte>): Array<Byte> { + return desensitize(value.toUHexString()).hexToBytes().toTypedArray() + } + + + companion object { + private val instance by lateinitMutableProperty { + create( + run<Map<String, String>> { + + val filename = + systemProp("mirai.network.recording.desensitization.filepath", "local.desensitization.yml") + + val file = + Thread.currentThread().contextClassLoader.getResource(filename) + ?: Thread.currentThread().contextClassLoader.getResource("recording/configs/$filename") + ?: error("Could not find desensitization configuration!") + + format.decodeFromString(file.readText()) + }.also { + logger.info { "Loaded ${it.size} desensitization rules." } + } + ) + } + + /** + * Loaded from local.desensitization.yml + */ + val local get() = instance + + fun desensitize(string: String): String = instance.desensitize(string) + + + fun ConstructorCallCodegenFacade.generateAndDesensitize( + value: Any?, + type: KType, + desensitizer: Desensitizer = instance, + ): String { + val a = analyze(value, type).apply { + accept(DesensitizationVisitor(desensitizer)) + } + return generate(a) + } + + @OptIn(ExperimentalStdlibApi::class) + inline fun <reified T> ConstructorCallCodegenFacade.generateAndDesensitize( + value: T, + desensitizer: Desensitizer = instance, + ): String = generateAndDesensitize(value, typeOf<T>(), desensitizer) + + + fun create(rules: Map<String, String>): Desensitizer { + val map = HashMap<String, String>() + map.putAll(rules) + + fun addExtraRulesForString(value: String, replacement: String) { + // in proto, strings have lengths field, we must ensure that their lengths are intact. + + when { + value.length > replacement.length -> { + map[value.toByteArray().toUHexString()] = + (replacement + "0".repeat(value.length - replacement.length)).toByteArray() + .toUHexString() // fix it to the same length + } + value.length < replacement.length -> { + error("Replacement '$replacement' must not be longer than '$value'") + } + else -> { + map.putIfAbsent(value.toByteArray().toUHexString(), replacement.toByteArray().toUHexString()) + } + } + } + + fun addExtraRulesForNumber(value: Long, replacement: Long) { + map.putIfAbsent(value.toString(), replacement.toString()) + + // 某些地方会 readLong, readInt, desensitizer visit 不到这些目标 + map.putIfAbsent(value.toByteArray().toUHexString(), replacement.toByteArray().toUHexString()) + + if (value in Int.MIN_VALUE.toLong()..UInt.MAX_VALUE.toLong() + && replacement in Int.MIN_VALUE.toLong()..UInt.MAX_VALUE.toLong() + ) { + map.putIfAbsent( + value.toInt().toByteArray().toUHexString(), + replacement.toInt().toByteArray().toUHexString() + ) + } + // 不需要处理 proto, 所有 proto 都会被反序列化为结构类型由 desensitizer 处理 + } + + rules.forEach { (t, u) -> + if (t.toLongOrNull() != null && u.toLongOrNull() != null) { + addExtraRulesForNumber(t.toLong(), u.toLong()) + addExtraRulesForNumber( + Mirai.calculateGroupUinByGroupCode(t.toLong()), + Mirai.calculateGroupUinByGroupCode(u.toLong()) + ) // putIfAbsent, code prevails + } + + addExtraRulesForString(t, u) + } + + return Desensitizer(map) + } + } +} + +private val format = Yaml { + // one-line + classSerialization = YamlBuilder.MapSerialization.FLOW_MAP + mapSerialization = YamlBuilder.MapSerialization.FLOW_MAP + listSerialization = YamlBuilder.ListSerialization.FLOW_SEQUENCE + stringSerialization = YamlBuilder.StringSerialization.DOUBLE_QUOTATION + encodeDefaultValues = false +} + + +private class DesensitizationVisitor( + private val desensitizer: Desensitizer, +) : ValueDescVisitor { + override fun visitPlain(desc: PlainValueDesc) { + desc.value = desensitizer.desensitize(desc.value) + } + + override fun visitObjectArray(desc: ObjectArrayValueDesc) { + if (desc.arrayType.arguments.first().type?.classifier == Byte::class) { // variance is ignored + @Suppress("UNCHECKED_CAST") + desc.value = desensitizer.desensitize(desc.value as Array<Byte>) + } else { + for (element in desc.elements) { + element.accept(this) + } + } + } + + override fun visitCollection(desc: CollectionValueDesc) { + for (element in desc.elements) { + element.accept(this) + } + } + + override fun visitPrimitiveArray(desc: PrimitiveArrayValueDesc) { + if (desc.value is ByteArray) { + desc.value = desensitizer.desensitize(desc.value as ByteArray) + } + } + + override fun <T : Any> visitClass(desc: ClassValueDesc<T>) { + super.visitClass(desc) + desc.properties.replaceAll() { key, value -> + val annotation = key.findAnnotation<NestedStructure>() + if (annotation != null && value.origin is ByteArray) { + val instance = annotation.serializer.objectInstance ?: annotation.serializer.createInstance() + + val result = instance.cast<NestedStructureDesensitizer<ProtocolStruct, ProtocolStruct>>() + .deserialize(desc.origin as ProtocolStruct, value.origin as ByteArray) + + val generate = ConstructorCallCodegenFacade.generateAndDesensitize(result) + PlainValueDesc(desc, "$generate.toByteArray()", value.origin) + } else value + } + } +} \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/notice/RecordingNoticeHandler.kt b/mirai-core/src/commonTest/kotlin/notice/RecordingNoticeHandler.kt new file mode 100644 index 00000000000..b3e5299443d --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/notice/RecordingNoticeHandler.kt @@ -0,0 +1,50 @@ +/* + * 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.notice + +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import net.mamoe.mirai.internal.network.components.NoticePipelineContext +import net.mamoe.mirai.internal.network.components.SimpleNoticeProcessor +import net.mamoe.mirai.internal.notice.Desensitizer.Companion.generateAndDesensitize +import net.mamoe.mirai.internal.utils.codegen.ConstructorCallCodegenFacade +import net.mamoe.mirai.internal.utils.io.ProtocolStruct +import net.mamoe.mirai.utils.MiraiLogger +import net.mamoe.mirai.utils.info + + +/** + * ### How to use recorder? + * + * 0. Configure desensitization. See mirai-core/src/commonTest/recording/configs/desensitization.yml + * 1. Inject the recorder as follows: + * + * ``` + * bot.components[NoticeProcessorPipeline].registerProcessor(recorder) + * ``` + * + * 2. Do something + * 3. Recorded values are shown in logs. Check 'decoded' to ensure that all sensitive values are replaced. + */ +internal class RecordingNoticeProcessor : SimpleNoticeProcessor<ProtocolStruct>(type()) { + private val id = atomic(0) + private val lock = Mutex() + + override suspend fun NoticePipelineContext.processImpl(data: ProtocolStruct) { + lock.withLock { + id.getAndDecrement() + logger.info { "Recorded #${id.value} ${data::class.simpleName}" } + logger.info { "Desensitized: \n\n\u001B[0m" + ConstructorCallCodegenFacade.generateAndDesensitize(data) + "\n\n" } + } + } +} + +private val logger: MiraiLogger by lazy { MiraiLogger.Factory.create(RecordingNoticeProcessor::class) } \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/notice/processors/AbstractNoticeProcessorTest.kt b/mirai-core/src/commonTest/kotlin/notice/processors/AbstractNoticeProcessorTest.kt new file mode 100644 index 00000000000..a5c22ef2e25 --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/notice/processors/AbstractNoticeProcessorTest.kt @@ -0,0 +1,210 @@ +/* + * 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.notice.processors + +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.serializer +import net.mamoe.mirai.Bot +import net.mamoe.mirai.Mirai +import net.mamoe.mirai.contact.* +import net.mamoe.mirai.internal.BotAccount +import net.mamoe.mirai.internal.QQAndroidBot +import net.mamoe.mirai.internal.contact.FriendImpl +import net.mamoe.mirai.internal.contact.GroupImpl +import net.mamoe.mirai.internal.contact.NormalMemberImpl +import net.mamoe.mirai.internal.contact.StrangerImpl +import net.mamoe.mirai.internal.contact.info.FriendInfoImpl +import net.mamoe.mirai.internal.contact.info.GroupInfoImpl +import net.mamoe.mirai.internal.contact.info.MemberInfoImpl +import net.mamoe.mirai.internal.contact.info.StrangerInfoImpl +import net.mamoe.mirai.internal.network.components.* +import net.mamoe.mirai.internal.network.components.NoticeProcessorPipeline.Companion.noticeProcessorPipeline +import net.mamoe.mirai.internal.network.framework.AbstractNettyNHTest +import net.mamoe.mirai.internal.network.protocol.packet.IncomingPacket +import net.mamoe.mirai.internal.utils.io.JceStruct +import net.mamoe.mirai.internal.utils.io.ProtocolStruct +import net.mamoe.mirai.internal.utils.io.serialization.tars.Tars +import net.mamoe.mirai.utils.TypeSafeMap +import net.mamoe.mirai.utils.cast +import net.mamoe.mirai.utils.currentTimeSeconds +import net.mamoe.mirai.utils.hexToUBytes + + +/** + * To add breakpoint, see [NoticeProcessorPipelineImpl.process] + */ +internal abstract class AbstractNoticeProcessorTest : AbstractNettyNHTest(), GroupExtensions { + init { + System.setProperty("mirai.network.notice.pipeline.log.full", "true") + } + + protected object UseTestContext { + val EMPTY_BYTE_ARRAY get() = net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY + fun String.hexToBytes() = hexToUBytes().toByteArray() + + internal inline fun <reified T : JceStruct> T.toByteArray( + serializer: SerializationStrategy<T> = serializer(), + ): ByteArray = Tars.UTF_8.encodeToByteArray(serializer, this) + + } + + protected suspend inline fun use( + attributes: TypeSafeMap = TypeSafeMap(), + pipeline: NoticeProcessorPipeline = bot.components.noticeProcessorPipeline, + block: UseTestContext.() -> ProtocolStruct + ): ProcessResult { + val handler = LoggingPacketHandlerAdapter(PacketLoggingStrategyImpl(bot), bot.logger) + return pipeline.process(bot, block(UseTestContext), attributes).also { list -> + for (packet in list) { + handler.handlePacket(IncomingPacket("test", packet)) + } + } + } + + protected suspend inline fun use( + attributes: TypeSafeMap = TypeSafeMap(), + crossinline createContext: NoticeProcessorPipelineImpl.(bot: QQAndroidBot, attributes: TypeSafeMap) -> NoticeProcessorPipelineImpl.ContextImpl, + block: UseTestContext.() -> ProtocolStruct + ): ProcessResult = use(attributes, pipeline = object : NoticeProcessorPipelineImpl() { + init { + bot.components.noticeProcessorPipeline.processors.forEach { registerProcessor(it) } + } + + override fun createContext(bot: QQAndroidBot, attributes: TypeSafeMap): NoticePipelineContext = + createContext(this, bot, attributes) + }, block) + + fun setBot(id: Long): QQAndroidBot { + bot = createBot(BotAccount(id, "a")) + return bot + } +} + +internal interface GroupExtensions { + + @Suppress("TestFunctionName") + fun GroupInfo( + uin: Long, + owner: Long, + groupCode: Long, + memo: String = "", + name: String, + allowMemberInvite: Boolean = false, + allowAnonymousChat: Boolean = false, + autoApprove: Boolean = false, + confessTalk: Boolean = false, + muteAll: Boolean = false, + botMuteTimestamp: Int = 0, + ): GroupInfoImpl = + GroupInfoImpl( + uin, owner, groupCode, memo, name, + allowMemberInvite, allowAnonymousChat, autoApprove, confessTalk, muteAll, + botMuteTimestamp + ) + + fun Bot.addGroup(group: Group) { + groups.delegate.add(group) + } + + fun Bot.addFriend(friend: Friend) { + friends.delegate.add(friend) + } + + fun Bot.addFriend(id: Long, nick: String = "friend$id", remark: String = ""): FriendImpl { + return FriendImpl(bot.cast(), bot.coroutineContext, FriendInfoImpl(id, nick, remark)).also { + friends.delegate.add(it) + } + } + + fun Bot.addStranger(id: Long, nick: String = "stranger$id", fromGroupId: Long = 0): StrangerImpl { + return StrangerImpl(bot.cast(), bot.coroutineContext, StrangerInfoImpl(id, nick, fromGroupId)).also { + strangers.delegate.add(it) + } + } + + fun Group.addMember(member: NormalMember) { + members.delegate.add(member) + } + + + fun Bot.addGroup( + id: Long, + owner: Long, + botPermission: MemberPermission = MemberPermission.MEMBER, + memo: String = "", + name: String = "Test Group", + allowMemberInvite: Boolean = false, + allowAnonymousChat: Boolean = false, + autoApprove: Boolean = false, + confessTalk: Boolean = false, + muteAll: Boolean = false, + botMuteTimestamp: Int = 0, + ): GroupImpl { + val impl = GroupImpl( + bot.cast(), coroutineContext, id, + GroupInfo( + Mirai.calculateGroupUinByGroupCode(id), owner, id, memo, name, allowMemberInvite, + allowAnonymousChat, autoApprove, confessTalk, muteAll, botMuteTimestamp + ), + ContactList(), + ) + addGroup(impl) + impl.botAsMember = impl.addMember(bot.id, nick = bot.nick, permission = botPermission) + return impl + } + + fun Bot.addGroup( + id: Long, + info: GroupInfoImpl, + botPermission: MemberPermission = MemberPermission.MEMBER, + ): Group { + val impl = GroupImpl( + bot.cast(), coroutineContext, id, info, + ContactList(), + ) + addGroup(impl) + impl.botAsMember = impl.addMember(bot.id, nick = bot.nick, permission = botPermission) + return impl + } + + fun Group.addMember( + id: Long, + nick: String = "user$id", + permission: MemberPermission, + remark: String = "", + nameCard: String = "", + specialTitle: String = "", + muteTimestamp: Int = 0, + anonymousId: String? = null, + joinTimestamp: Int = currentTimeSeconds().toInt(), + lastSpeakTimestamp: Int = 0, + isOfficialBot: Boolean = false, + ): NormalMemberImpl { + val member = NormalMemberImpl( + this.cast(), this.coroutineContext, + MemberInfoImpl( + id, nick, permission, remark, nameCard, + specialTitle, muteTimestamp, anonymousId, joinTimestamp, lastSpeakTimestamp, isOfficialBot + ) + ) + members.delegate.add( + member + ) + return member + } + + fun Group.addMember( + id: Long, + info: MemberInfoImpl, + ): Group { + members.delegate.add(NormalMemberImpl(this.cast(), this.coroutineContext, info)) + return this + } +} \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/notice/processors/BotInvitedJoinTest.kt b/mirai-core/src/commonTest/kotlin/notice/processors/BotInvitedJoinTest.kt new file mode 100644 index 00000000000..e5f145f9ba4 --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/notice/processors/BotInvitedJoinTest.kt @@ -0,0 +1,152 @@ +/* + * 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/dev/LICENSE + */ + +@file:JvmBlockingBridge + +package net.mamoe.mirai.internal.notice.processors + +import net.mamoe.kjbb.JvmBlockingBridge +import net.mamoe.mirai.contact.MemberPermission +import net.mamoe.mirai.event.events.BotInvitedJoinGroupRequestEvent +import net.mamoe.mirai.event.events.BotJoinGroupEvent +import net.mamoe.mirai.internal.QQAndroidBot +import net.mamoe.mirai.internal.contact.GroupImpl +import net.mamoe.mirai.internal.network.components.NoticeProcessorPipelineImpl +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +internal class BotInvitedJoinTest : AbstractNoticeProcessorTest() { + @Test + suspend fun `invited join`() { + suspend fun runTest() = use { + + net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg.StructMsg( + version = 1, + msgType = 2, + msgSeq = 1630, + msgTime = 1630, + reqUin = 1230, + msg = net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg.SystemMsg( + subType = 1, + msgTitle = "邀请加群", + msgDescribe = "邀请你加入 %group_name%", + actions = mutableListOf( + net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg.SystemMsgAction( + name = "拒绝", + result = "已拒绝", + actionInfo = net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg.SystemMsgActionInfo( + type = 12, + groupCode = 2230203, + ), + detailName = "拒绝", + ), net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg.SystemMsgAction( + name = "同意", + result = "已同意", + actionInfo = net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg.SystemMsgActionInfo( + type = 11, + groupCode = 2230203, + ), + detailName = "同意", + ), net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg.SystemMsgAction( + name = "忽略", + result = "已忽略", + actionInfo = net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg.SystemMsgActionInfo( + type = 14, + groupCode = 2230203, + ), + detailName = "忽略", + ) + ), + groupCode = 2230203, + actionUin = 1230001, + groupMsgType = 2, + groupInviterRole = 1, + groupInfo = net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg.GroupInfo( + appPrivilegeFlag = 67698880, + ), + msgInviteExtinfo = net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg.MsgInviteExt( + ), + reqUinNick = "user3", + groupName = "testtest", + actionUinNick = "user1", + groupExtFlag = 1075905600, + actionUinQqNick = "user1", + reqUinGender = 255, + c2cInviteJoinGroupFlag = 1, + ), + ) + } + + setBot(1230003) + + runTest().toList().run { + assertEquals(1, size, toString()) + get(0).run { + assertIs<BotInvitedJoinGroupRequestEvent>(this) + assertEquals(1230001, invitorId) + assertEquals("user1", invitorNick) + assertEquals(2230203, groupId) + assertEquals("testtest", groupName) + assertEquals(1630, eventId) + } + } + + } + + @Test + suspend fun `invitation accepted`() { + suspend fun runTest() = + use(createContext = { bot, attributes -> + object : NoticeProcessorPipelineImpl.ContextImpl(bot, attributes) { + override suspend fun QQAndroidBot.addNewGroupByUin(groupUin: Long): GroupImpl { + assertEquals(204230203, groupUin) // uin of 2230203 + return bot.addGroup(2230203, 1230002, name = "testtest").apply { + addMember(1230001, permission = MemberPermission.MEMBER) + } + } + } + }) { + net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.Msg( + msgHead = net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.MsgHead( + fromUin = 2230203, + toUin = 1230003, + msgType = 33, + msgSeq = 61485, + msgTime = 1630, + msgUid = 1441, + authUin = 1230003, + authNick = "user3", + extGroupKeyInfo = net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.ExtGroupKeyInfo( + curMaxSeq = 1631, + curTime = 1630, + ), + ), + contentHead = net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.ContentHead( + ), + msgBody = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.MsgBody( + richText = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.RichText( + ), + msgContent = "00 22 07 BB 01 00 12 C4 B3 03 00 12 C4 B1 00 00 30 34 32 42 39 44 46 43 34 39 45 42 34 30 46 41 42 45 45 32 33 36 34 37 45 46 39 35 31 44 44 42 31 31 32 36 31 31 38 44 43 46 44 32 37 42 30 42 45".hexToBytes(), + ), + ) + } + + setBot(1230003) + + runTest().toList().run { + assertEquals(1, size, toString()) + get(0).run { + assertIs<BotJoinGroupEvent.Invite>(this) + assertEquals(1230001, invitor.id) + assertEquals(2230203, group.id) + } + } + } +} \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/notice/processors/GroupRetrieveTest.kt b/mirai-core/src/commonTest/kotlin/notice/processors/GroupRetrieveTest.kt new file mode 100644 index 00000000000..3ea8a47771c --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/notice/processors/GroupRetrieveTest.kt @@ -0,0 +1,104 @@ +/* + * 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/dev/LICENSE + */ +@file:JvmBlockingBridge + +package net.mamoe.mirai.internal.notice.processors + +import net.mamoe.kjbb.JvmBlockingBridge +import net.mamoe.mirai.contact.MemberPermission +import net.mamoe.mirai.event.events.BotGroupPermissionChangeEvent +import net.mamoe.mirai.event.events.MemberPermissionChangeEvent +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +internal class GroupRetrieveTest : AbstractNoticeProcessorTest() { + + @Test + suspend fun `other member retrieves group from another member when they are in the group`() { + suspend fun runTest() = use { + net.mamoe.mirai.internal.network.protocol.data.proto.OnlinePushTrans.PbMsgInfo( + fromUin = 2230203, + toUin = 1230003, + msgType = 44, + msgSeq = 27, + msgUid = 14411, + msgTime = 1629, + realMsgTime = 164, + msgData = "00 22 07 BB 01 FF 00 12 C4 B1 00 12 C4 B2".hexToBytes(), + svrIp = -9623, + ) + } + + setBot(1230003) + .addGroup(2230203, 1230002, name = "testtest").apply { + addMember(1230001, permission = MemberPermission.OWNER) + addMember(1230002, permission = MemberPermission.MEMBER) + } + + runTest().toList().run { + assertEquals(2, size, toString()) + get(0).run { + assertIs<MemberPermissionChangeEvent>(this) + assertEquals(1230001, member.id) + assertEquals(MemberPermission.OWNER, origin) + assertEquals(MemberPermission.MEMBER, new) + assertEquals(MemberPermission.MEMBER, group.members[1230001]!!.permission) + } + get(1).run { + assertIs<MemberPermissionChangeEvent>(this) + assertEquals(1230002, member.id) + assertEquals(MemberPermission.MEMBER, origin) + assertEquals(MemberPermission.OWNER, new) + assertEquals(MemberPermission.OWNER, group.members[1230002]!!.permission) + } + } + } + + @Test + suspend fun `other member retrieves group from bot when they are in the group`() { + suspend fun runTest() = use { + net.mamoe.mirai.internal.network.protocol.data.proto.OnlinePushTrans.PbMsgInfo( + fromUin = 2230203, + toUin = 1230003, + msgType = 44, + msgSeq = 459, + msgUid = 14411518, + msgTime = 1629, + realMsgTime = 1629, + msgData = "00 22 07 BB 01 FF 00 12 C4 B3 00 12 C4 B2".hexToBytes(), + svrIp = -164, + ) + } + + setBot(1230003) + .addGroup(2230203, 1230002, name = "testtest").apply { + addMember(1230001, permission = MemberPermission.MEMBER) + addMember(1230002, permission = MemberPermission.MEMBER) + botAsMember.permission = MemberPermission.OWNER + } + + runTest().toList().run { + assertEquals(2, size, toString()) + get(0).run { + assertIs<BotGroupPermissionChangeEvent>(this) + assertEquals(MemberPermission.OWNER, origin) + assertEquals(MemberPermission.MEMBER, new) + assertEquals(MemberPermission.MEMBER, group.botPermission) + } + get(1).run { + assertIs<MemberPermissionChangeEvent>(this) + assertEquals(1230002, member.id) + assertEquals(MemberPermission.MEMBER, origin) + assertEquals(MemberPermission.OWNER, new) + assertEquals(MemberPermission.OWNER, group.members[1230002]!!.permission) + } + } + } +} \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/notice/processors/GroupTransferTest.kt b/mirai-core/src/commonTest/kotlin/notice/processors/GroupTransferTest.kt new file mode 100644 index 00000000000..095b5693a78 --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/notice/processors/GroupTransferTest.kt @@ -0,0 +1,104 @@ +/* + * 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/dev/LICENSE + */ +@file:JvmBlockingBridge + +package net.mamoe.mirai.internal.notice.processors + +import net.mamoe.kjbb.JvmBlockingBridge +import net.mamoe.mirai.contact.MemberPermission +import net.mamoe.mirai.event.events.BotGroupPermissionChangeEvent +import net.mamoe.mirai.event.events.MemberPermissionChangeEvent +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + + +internal class GroupTransferTest : AbstractNoticeProcessorTest() { + + @Test + suspend fun `owner transfers group to other member`() { + suspend fun runTest() = use { + net.mamoe.mirai.internal.network.protocol.data.proto.OnlinePushTrans.PbMsgInfo( + fromUin = 2230203, + toUin = 1230003, + msgType = 44, + msgSeq = 439, + msgUid = 14411520, + msgTime = 162974, + realMsgTime = 163874, + msgData = "00 22 07 BB 01 FF 00 12 C4 B2 00 12 C4 B1".hexToBytes(), + svrIp = 194, + ) + } + + val group = setBot(1230003) + .addGroup(2230203, 1230002, name = "testtest").apply { + addMember(1230001, permission = MemberPermission.MEMBER) + addMember(1230002, permission = MemberPermission.OWNER) + } + + runTest().toList().run { + assertEquals(2, size) + get(0).run { + assertIs<MemberPermissionChangeEvent>(this) + assertEquals(1230002, member.id) + assertEquals(MemberPermission.OWNER, origin) + assertEquals(MemberPermission.MEMBER, new) + assertEquals(MemberPermission.MEMBER, group.members[1230002]!!.permission) + } + get(1).run { + assertIs<MemberPermissionChangeEvent>(this) + assertEquals(1230001, member.id) + assertEquals(MemberPermission.MEMBER, origin) + assertEquals(MemberPermission.OWNER, new) + assertEquals(MemberPermission.OWNER, group.members[1230001]!!.permission) + } + } + } + + @Test + suspend fun `owner transfers group to bot`() { + suspend fun runTest() = use { + net.mamoe.mirai.internal.network.protocol.data.proto.OnlinePushTrans.PbMsgInfo( + fromUin = 2230203, + toUin = 1230003, + msgType = 44, + msgSeq = 291, + msgUid = 144115188, + msgTime = 16298, + realMsgTime = 16298, + msgData = "00 22 07 BB 01 FF 00 12 C4 B2 00 12 C4 B3".hexToBytes(), + svrIp = -14676, + ) + } + + setBot(1230003) + .addGroup(2230203, 1230002, name = "testtest").apply { + addMember(1230001, permission = MemberPermission.MEMBER) + addMember(1230002, permission = MemberPermission.OWNER) + } + + runTest().toList().run { + assertEquals(2, size) + get(0).run { + assertIs<MemberPermissionChangeEvent>(this) + assertEquals(1230002, member.id) + assertEquals(MemberPermission.OWNER, origin) + assertEquals(MemberPermission.MEMBER, new) + assertEquals(MemberPermission.MEMBER, group.members[1230002]!!.permission) + } + get(1).run { + assertIs<BotGroupPermissionChangeEvent>(this) + assertEquals(MemberPermission.MEMBER, origin) + assertEquals(MemberPermission.OWNER, new) + assertEquals(MemberPermission.OWNER, group.botPermission) + } + } + } +} \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/notice/processors/MemberAdminChangeTest.kt b/mirai-core/src/commonTest/kotlin/notice/processors/MemberAdminChangeTest.kt new file mode 100644 index 00000000000..7b49d37474e --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/notice/processors/MemberAdminChangeTest.kt @@ -0,0 +1,217 @@ +/* + * 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/dev/LICENSE + */ + +@file:JvmBlockingBridge + +package net.mamoe.mirai.internal.notice.processors + +import net.mamoe.kjbb.JvmBlockingBridge +import net.mamoe.mirai.contact.MemberPermission +import net.mamoe.mirai.event.events.BotGroupPermissionChangeEvent +import net.mamoe.mirai.event.events.MemberPermissionChangeEvent +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + + +/** + * Set/cancel admin permission for bot/member + */ +internal class MemberAdminChangeTest : AbstractNoticeProcessorTest() { + + @Test + suspend fun `bot member to admin`() { + suspend fun runTest() = use { + net.mamoe.mirai.internal.network.protocol.data.proto.OnlinePushTrans.PbMsgInfo( + fromUin = 2230203, + toUin = 1230003, + msgType = 44, + msgSeq = 4827, + msgUid = 144, + msgTime = 162, + realMsgTime = 1629, + msgData = "00 22 07 BB 01 01 00 12 C4 B3 01".hexToBytes(), + svrIp = 12165, + ) + } + + // bot was MEMBER + + val group = setBot(1230003) + .addGroup(2230203, 1230002, name = "testtest").apply { + addMember(1230001, permission = MemberPermission.MEMBER) + addMember(1230002, permission = MemberPermission.OWNER) + } + + runTest().run { + assertEquals(1, size) + val event = single() + assertIs<BotGroupPermissionChangeEvent>(event) + assertEquals(MemberPermission.MEMBER, event.origin) + assertEquals(MemberPermission.ADMINISTRATOR, event.new) + assertEquals(MemberPermission.ADMINISTRATOR, group.botPermission) + } + + + // bot was ADMINISTRATOR + + setBot(1230003) + .addGroup(2230203, 1230002, name = "testtest").apply { + addMember(1230001, permission = MemberPermission.MEMBER) + addMember(1230002, permission = MemberPermission.OWNER) + botAsMember.permission = MemberPermission.ADMINISTRATOR + } + + runTest().run { + assertEquals(0, size) + } + } + + @Test + suspend fun `bot admin to member`() { + suspend fun runTest() = use { + net.mamoe.mirai.internal.network.protocol.data.proto.OnlinePushTrans.PbMsgInfo( + fromUin = 2230203, + toUin = 1230003, + msgType = 44, + msgSeq = 483, + msgUid = 14411512, + msgTime = 1629863, + realMsgTime = 1623063, + msgData = "00 22 07 BB 01 00 00 12 C4 B3 00".hexToBytes(), + svrIp = 2039273, + ) + } + + val group = setBot(1230003) + .addGroup(2230203, 1230002, name = "testtest").apply { + addMember(1230001, permission = MemberPermission.MEMBER) + addMember(1230002, permission = MemberPermission.OWNER) + botAsMember.permission = MemberPermission.ADMINISTRATOR + } + + runTest().run { + assertEquals(1, size) + val event = single() + assertIs<BotGroupPermissionChangeEvent>(event) + assertEquals(MemberPermission.ADMINISTRATOR, event.origin) + assertEquals(MemberPermission.MEMBER, event.new) + assertEquals(MemberPermission.MEMBER, group.botPermission) + } + + + setBot(1230003) + .addGroup(2230203, 1230002, name = "testtest").apply { + addMember(1230001, permission = MemberPermission.MEMBER) + addMember(1230002, permission = MemberPermission.OWNER) + botAsMember.permission = MemberPermission.MEMBER // already member + } + + runTest().run { + assertEquals(0, size) + } + } + + @Test + suspend fun `member member to admin`() { + suspend fun runTest() = use { + net.mamoe.mirai.internal.network.protocol.data.proto.OnlinePushTrans.PbMsgInfo( + fromUin = 2230203, + toUin = 1230003, + msgType = 44, + msgSeq = 5639, + msgUid = 1441812, + msgTime = 1623204, + realMsgTime = 1623204, + msgData = "00 22 07 BB 01 01 00 12 C4 B1 01".hexToBytes(), + svrIp = -20900855, + ) + } + + // member was MEMBER + + val group = setBot(1230003) + .addGroup(2230203, 1230002, name = "testtest").apply { + addMember(1230001, permission = MemberPermission.MEMBER) + addMember(1230002, permission = MemberPermission.OWNER) + } + + + runTest().run { + assertEquals(1, size) + val event = single() + assertIs<MemberPermissionChangeEvent>(event) + assertEquals(1230001, event.member.id) + assertEquals(MemberPermission.MEMBER, event.origin) + assertEquals(MemberPermission.ADMINISTRATOR, event.new) + assertEquals(MemberPermission.ADMINISTRATOR, group.members[1230001]!!.permission) + } + + + // member was already ADMINISTRATOR + + setBot(1230003) + .addGroup(2230203, 1230002, name = "testtest").apply { + addMember(1230001, permission = MemberPermission.ADMINISTRATOR) + addMember(1230002, permission = MemberPermission.OWNER) + } + + runTest().run { + assertEquals(0, size) + } + } + + @Test + suspend fun `member admin to member`() { + suspend fun runTest() = use { + net.mamoe.mirai.internal.network.protocol.data.proto.OnlinePushTrans.PbMsgInfo( + fromUin = 2230203, + toUin = 1230003, + msgType = 44, + msgSeq = 745, + msgUid = 144115576812, + msgTime = 162250, + realMsgTime = 16290, + msgData = "00 22 07 BB 01 00 00 12 C4 B1 00".hexToBytes(), + svrIp = 277969, + ) + } + + val group = setBot(1230003) + .addGroup(2230203, 1230002, name = "testtest").apply { + addMember(1230001, permission = MemberPermission.MEMBER) + .permission = MemberPermission.ADMINISTRATOR + addMember(1230002, permission = MemberPermission.OWNER) + } + + runTest().run { + assertEquals(1, size) + val event = single() + assertIs<MemberPermissionChangeEvent>(event) + assertEquals(1230001, event.member.id) + assertEquals(MemberPermission.ADMINISTRATOR, event.origin) + assertEquals(MemberPermission.MEMBER, event.new) + assertEquals(MemberPermission.MEMBER, group.members[1230001]!!.permission) + } + + + setBot(1230003) + .addGroup(2230203, 1230002, name = "testtest").apply { + addMember(1230001, permission = MemberPermission.MEMBER) + .permission = MemberPermission.MEMBER // already member + addMember(1230002, permission = MemberPermission.OWNER) + } + + runTest().run { + assertEquals(0, size) + } + } + + +} \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/notice/processors/MemberJoinTest.kt b/mirai-core/src/commonTest/kotlin/notice/processors/MemberJoinTest.kt new file mode 100644 index 00000000000..cc60d7d465a --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/notice/processors/MemberJoinTest.kt @@ -0,0 +1,203 @@ +/* + * 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/dev/LICENSE + */ + +@file:JvmBlockingBridge + +package net.mamoe.mirai.internal.notice.processors + +import net.mamoe.kjbb.JvmBlockingBridge +import net.mamoe.mirai.contact.MemberPermission +import net.mamoe.mirai.event.events.MemberJoinEvent +import net.mamoe.mirai.event.events.MemberJoinRequestEvent +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +internal class MemberJoinTest : AbstractNoticeProcessorTest() { + + @Test + suspend fun `member actively request join`() { + suspend fun runTest() = use { + net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg.StructMsg( + version = 1, + msgType = 2, + msgSeq = 16300, + msgTime = 1630, + reqUin = 1230001, + msg = net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg.SystemMsg( + subType = 1, + msgTitle = "加群申请", + msgDescribe = "申请加入 %group_name%", + msgAdditional = "verification message", + srcId = 1, + subSrcId = 5, + actions = mutableListOf( + net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg.SystemMsgAction( + name = "拒绝", + result = "已拒绝", + actionInfo = net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg.SystemMsgActionInfo( + type = 12, + groupCode = 2230203, + ), + detailName = "拒绝", + ), net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg.SystemMsgAction( + name = "同意", + result = "已同意", + actionInfo = net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg.SystemMsgActionInfo( + type = 11, + groupCode = 2230203, + ), + detailName = "同意", + ), net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg.SystemMsgAction( + name = "忽略", + result = "已忽略", + actionInfo = net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg.SystemMsgActionInfo( + type = 14, + groupCode = 2230203, + ), + detailName = "忽略", + ) + ), + groupCode = 2230203, + groupMsgType = 1, + groupInfo = net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg.GroupInfo( + appPrivilegeFlag = 67698880, + ), + groupFlagext3 = 128, + reqUinFaceid = 7425, + reqUinNick = "user1", + groupName = "testtest", + groupExtFlag = 1075905600, + actionUinQqNick = "user1", + reqUinGender = 1, + reqUinAge = 19, + ), + ) + } + + setBot(1230003) + .addGroup(2230203, 1230002, name = "testtest", botPermission = MemberPermission.ADMINISTRATOR).apply { + addMember(1230002, "user2", MemberPermission.OWNER) + } + + runTest().run { + assertEquals(1, size) + val event = single() + assertIs<MemberJoinRequestEvent>(event) + assertEquals(1230001, event.fromId) + assertEquals(2230203, event.groupId) + assertEquals("verification message", event.message) + assertEquals("testtest", event.groupName) + } + } + + @Test + suspend fun `member request accepted by other admin`() { + suspend fun runTest() = use { + net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.Msg( + msgHead = net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.MsgHead( + fromUin = 2230203, + toUin = 1230003, + msgType = 33, + msgSeq = 45, + msgTime = 16, + msgUid = 1441, + authUin = 1230001, + authNick = "user1", + extGroupKeyInfo = net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.ExtGroupKeyInfo( + curMaxSeq = 1628, + curTime = 1630, + ), + authSex = 2, + ), + contentHead = net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.ContentHead( + ), + msgBody = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.MsgBody( + richText = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.RichText( + ), + msgContent = "00 22 07 BB 01 00 12 C4 B1 02 00 12 C4 B3 06 B9 DC C0 ED D4 B1 00 30 44 38 32 41 43 32 46 33 30 36 46 44 34 35 30 30 36 38 32 46 36 41 38 32 30 31 38 34 41 42 30 43 43 30 32 43 41 33 33 37 41 31 30 38 43 32 36 36".hexToBytes(), + ), + ) + + } + + val group = setBot(1230003) + .addGroup(2230203, 1230002, name = "testtest", botPermission = MemberPermission.ADMINISTRATOR).apply { + addMember(1230002, "user2", MemberPermission.OWNER) + } + + assertNull(group.members[1230001]) + + runTest().run { + assertEquals(1, size) + val event = single() + assertIs<MemberJoinEvent.Active>(event) + assertEquals(2230203, event.groupId) + assertEquals(1230001, event.member.id) + assertNotNull(group.members[1230001]) + } + } + + @Test + fun `member request rejected by other admin`() { + // There is no corresponding event + } + + + @Test + suspend fun `member joins directly when group allows anyone`() { + suspend fun runTest() = use { + net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.Msg( + msgHead = net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.MsgHead( + fromUin = 2230203, + toUin = 1230003, + msgType = 33, + msgSeq = 45, + msgTime = 16, + msgUid = 1441, + authUin = 1230001, + authNick = "user1", + extGroupKeyInfo = net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.ExtGroupKeyInfo( + curMaxSeq = 1628, + curTime = 1630, + ), + authSex = 2, + ), + contentHead = net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.ContentHead( + ), + msgBody = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.MsgBody( + richText = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.RichText( + ), + msgContent = "00 22 07 BB 01 00 12 C4 B1 02 00 12 C4 B3 06 B9 DC C0 ED D4 B1 00 30 44 38 32 41 43 32 46 33 30 36 46 44 34 35 30 30 36 38 32 46 36 41 38 32 30 31 38 34 41 42 30 43 43 30 32 43 41 33 33 37 41 31 30 38 43 32 36 36".hexToBytes(), + ), + ) + + } + + val group = setBot(1230003) + .addGroup(2230203, 1230002, name = "testtest", botPermission = MemberPermission.ADMINISTRATOR).apply { + addMember(1230002, "user2", MemberPermission.OWNER) + } + + assertNull(group.members[1230001]) + + runTest().run { + assertEquals(1, size) + val event = single() + assertIs<MemberJoinEvent.Active>(event) + assertEquals(2230203, event.groupId) + assertEquals(1230001, event.member.id) + assertNotNull(group.members[1230001]) + } + } + + +} \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/notice/processors/MemberQuitTest.kt b/mirai-core/src/commonTest/kotlin/notice/processors/MemberQuitTest.kt new file mode 100644 index 00000000000..a12609d8714 --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/notice/processors/MemberQuitTest.kt @@ -0,0 +1,97 @@ +/* + * 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/dev/LICENSE + */ + +@file:JvmBlockingBridge + +package net.mamoe.mirai.internal.notice.processors + +import net.mamoe.kjbb.JvmBlockingBridge +import net.mamoe.mirai.contact.MemberPermission +import net.mamoe.mirai.event.events.MemberLeaveEvent +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +internal class MemberQuitTest : AbstractNoticeProcessorTest() { + + @Test + suspend fun `member active quit`() { + suspend fun runTest() = use { + net.mamoe.mirai.internal.network.protocol.data.proto.OnlinePushTrans.PbMsgInfo( + fromUin = 2230203, + toUin = 1230003, + msgType = 34, + msgSeq = 266, + msgUid = 1441151, + msgTime = 16298, + realMsgTime = 1629, + msgData = "00 22 07 BB 01 00 12 C4 B1 02 00 30 39 41 36 36 41 32 31 32 33 35 37 32 43 39 35 38 42 42 36 38 45 32 36 44 34 34 32 38 45 32 32 37 32 36 44 39 44 45 41 31 34 41 44 37 30 31 46 31".hexToBytes(), + svrIp = 618, + extGroupKeyInfo = net.mamoe.mirai.internal.network.protocol.data.proto.OnlinePushTrans.ExtGroupKeyInfo( + curMaxSeq = 1626, + curTime = 16298, + ), + generalFlag = 1, + ) + } + + setBot(1230003) + .addGroup(2230203, 1230002, name = "testtest").apply { + addMember(1230001, "user1", MemberPermission.MEMBER) + addMember(1230002, "user2", MemberPermission.OWNER) + } + + runTest().run { + assertEquals(1, size) + single().run { + assertIs<MemberLeaveEvent.Quit>(this) + assertEquals(1230001, member.id) + assertEquals(null, group.members[1230001]) + } + } + } + + @Test + suspend fun `member kick`() { + suspend fun runTest() = use { + net.mamoe.mirai.internal.network.protocol.data.proto.OnlinePushTrans.PbMsgInfo( + fromUin = 2230203, + toUin = 1230002, + msgType = 34, + msgSeq = 430, + msgUid = 1441, + msgTime = 16298, + realMsgTime = 1629, + msgData = "00 22 07 BB 01 00 12 C4 B1 03 00 12 C4 B3 06 B4 B4 BD A8 D5 DF 00 30 45 31 39 41 35 43 41 37 34 36 44 37 38 31 36 45 34 46 36 37 41 39 35 36 46 32 34 46 46 38 33 41 32 30 34 44 41 33 38 30 35 41 38 34 39 45 44 32".hexToBytes(), + svrIp = 54562, + extGroupKeyInfo = net.mamoe.mirai.internal.network.protocol.data.proto.OnlinePushTrans.ExtGroupKeyInfo( + curMaxSeq = 1627, + curTime = 1629, + ), + generalFlag = 1, + ) + } + + setBot(1230003) + .addGroup(2230203, 1230002, name = "testtest").apply { + addMember(1230001, "user1", MemberPermission.MEMBER) + addMember(1230002, "user2", MemberPermission.OWNER) + } + + runTest().run { + assertEquals(1, size) + single().run { + assertIs<MemberLeaveEvent.Kick>(this) + assertEquals(1230001, member.id) + assertEquals(null, group.members[1230001]) + } + } + } + +} \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/notice/processors/MessageTest.kt b/mirai-core/src/commonTest/kotlin/notice/processors/MessageTest.kt new file mode 100644 index 00000000000..857cc577b9d --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/notice/processors/MessageTest.kt @@ -0,0 +1,411 @@ +/* + * 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/dev/LICENSE + */ + +@file:JvmBlockingBridge + +package net.mamoe.mirai.internal.notice.processors + +import net.mamoe.kjbb.JvmBlockingBridge +import net.mamoe.mirai.contact.MemberPermission +import net.mamoe.mirai.event.events.FriendMessageEvent +import net.mamoe.mirai.event.events.GroupMessageEvent +import net.mamoe.mirai.event.events.GroupTempMessageEvent +import net.mamoe.mirai.internal.network.components.NoticePipelineContext.Companion.KEY_FROM_SYNC +import net.mamoe.mirai.internal.network.components.SsoProcessor +import net.mamoe.mirai.message.data.MessageSource +import net.mamoe.mirai.message.data.OnlineMessageSource +import net.mamoe.mirai.message.data.PlainText +import net.mamoe.mirai.message.data.content +import org.junit.jupiter.api.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertIs + +internal class MessageTest : AbstractNoticeProcessorTest() { + + @Test + suspend fun `group message test`() { + suspend fun runTest() = use { + net.mamoe.mirai.internal.network.protocol.data.proto.MsgOnlinePush.PbPushMsg( + msg = net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.Msg( + msgHead = net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.MsgHead( + fromUin = 1230001, + toUin = 1230003, + msgType = 82, + msgSeq = 1629, // id + msgTime = 1630, + msgUid = 14411, // neither id nor internalId + groupInfo = net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.GroupInfo( + groupCode = 2230203, + groupType = 1, + groupInfoSeq = 626, + groupCard = "user1", + groupLevel = 1, + groupCardType = 2, + groupName = "testtest".toByteArray(), /* 74 65 73 74 74 65 73 74 */ + ), + fromAppid = 1, + fromInstid = 1, + userActive = 1, + ), + contentHead = net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.ContentHead( + pkgNum = 1, + ), + msgBody = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.MsgBody( + richText = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.RichText( + attr = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Attr( + codePage = 0, + time = 1630, + random = -1469, // internal id + size = 12, + effect = 0, + charSet = 134, + pitchAndFamily = 34, + fontName = "微软雅黑", + ), + elems = mutableListOf( + net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem( + text = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Text( + str = "hello", + ), + ), net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem( + elemFlags2 = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.ElemFlags2( + msgRptCnt = 1, + ), + ), net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem( + generalFlags = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.GeneralFlags( + glamourLevel = 3, + pbReserve = "08 0A 20 CB 50 80 01 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 D1 02 A0 03 40 B0 03 00 C0 03 00 D0 03 00 E8 03 00 90 04 80 01 B8 04 02 C0 04 00 CA 04 00 F8 04 00 88 05 00".hexToBytes(), + ), + ), net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem( + extraInfo = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.ExtraInfo( + nick = "user1", + level = 1, + groupMask = 3, + ), + ) + ), + ), + ), + ), + svrip = 2057, + generalFlag = 1, + ) + } + + setBot(1230003) + .addGroup(2230203, 1230002, name = "testtest").apply { + addMember(1230001, permission = MemberPermission.MEMBER, nick = "user1") + } + + runTest().run { + assertEquals(1, size) + val event = single() + assertIs<GroupMessageEvent>(event) + assertEquals(1630, event.time) + assertEquals(1230001, event.sender.id) + assertEquals("user1", event.senderName) + + assertEquals("hello", event.message.content) + + event.message.run { + assertEquals(2, size) + get(0).run { + assertIs<OnlineMessageSource.Incoming.FromGroup>(this) + assertContentEquals(intArrayOf(1629), ids) + assertContentEquals(intArrayOf(-1469), internalIds) + assertEquals(1630, time) + assertEquals(1230001, fromId) + assertEquals(2230203, targetId) + assertEquals(event.message.filterNot { it is MessageSource }, originalMessage) + } + assertIs<PlainText>(get(1)) + assertEquals("hello", get(1).content) + } + } + } + + + @Test + suspend fun `friend message test`() { + suspend fun runTest() = use(KEY_FROM_SYNC to false) { + bot.components[SsoProcessor].firstLoginSucceed = true + net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.Msg( + msgHead = net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.MsgHead( + fromUin = 1230001, + toUin = 1230003, + msgType = 166, + c2cCmd = 11, + msgSeq = 13985, + msgTime = 1630, + msgUid = 72057, + wseqInC2cMsghead = 25159, + ), + contentHead = net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.ContentHead( + pkgNum = 1, + ), + msgBody = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.MsgBody( + richText = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.RichText( + attr = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Attr( + codePage = 0, + time = 1630, + random = -5872, + size = 12, + effect = 0, + charSet = 134, + pitchAndFamily = 34, + fontName = "微软雅黑", + ), + elems = mutableListOf( + net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem( + text = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Text( + str = "123", + ), + ), net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem( + elemFlags2 = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.ElemFlags2( + ), + ), net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem( + generalFlags = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.GeneralFlags( + pbReserve = "80 01 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 CA 04 00".hexToBytes(), + ), + ) + ), + ), + ), + ) + } + + setBot(1230003) + .addFriend(1230001, "user1") + + + + runTest().run { + assertEquals(1, size) + val event = single() + assertIs<FriendMessageEvent>(event) + assertEquals(1630, event.time) + assertEquals(1230001, event.sender.id) + assertEquals("user1", event.senderName) + + assertEquals("123", event.message.content) + + event.message.run { + assertEquals(2, size) + get(0).run { + assertIs<OnlineMessageSource.Incoming.FromFriend>(this) + assertContentEquals(intArrayOf(13985), ids) + assertContentEquals(intArrayOf(-5872), internalIds) + assertEquals(1630, time) + assertEquals(1230001, fromId) + assertEquals(1230003, targetId) + assertEquals(event.message.filterNot { it is MessageSource }, originalMessage) + } + assertIs<PlainText>(get(1)) + assertEquals("123", get(1).content) + } + } + } + + @Test + suspend fun `group temp message test`() { + suspend fun runTest() = use(KEY_FROM_SYNC to false) { + bot.components[SsoProcessor].firstLoginSucceed = true + + net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.Msg( + msgHead = net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.MsgHead( + fromUin = 1230001, + toUin = 1230003, + msgType = 141, + c2cCmd = 11, + msgSeq = 11080, + msgTime = 1630, + msgUid = 720, + c2cTmpMsgHead = net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.C2CTmpMsgHead( + c2cType = 2, + groupUin = 2230203, + groupCode = 2230203, + sig = "38 59 CD 1E 22 9A 0A BA 28 59 46 BE FA 51 36 D0 F1 7A 5D 54 F5 04 05 7E 66 C7 36 4F 73 BF 45 96 00 39 7C 8F F5 43 57 74 B0 EB D9 5E 0F 1F 9B CF".hexToBytes(), + ), + wseqInC2cMsghead = 25160, + ), + contentHead = net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.ContentHead( + pkgNum = 1, + ), + msgBody = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.MsgBody( + richText = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.RichText( + attr = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Attr( + codePage = 0, + time = 1630, + random = 1854, + size = 12, + effect = 0, + charSet = 134, + pitchAndFamily = 34, + fontName = "微软雅黑", + ), + elems = mutableListOf( + net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem( + text = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Text( + str = "hello", + ), + ), net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem( + extraInfo = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.ExtraInfo( + nick = "user1", + ), + ), net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem( + elemFlags2 = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.ElemFlags2( + ), + ), net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem( + generalFlags = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.GeneralFlags( + pbReserve = "80 01 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 CA 04 00".hexToBytes(), + ), + ) + ), + ), + ), + ) + } + + setBot(1230003).apply { + addGroup(2230203, 1230002, name = "testtest").apply { + addMember(1230001, permission = MemberPermission.MEMBER, nick = "user1") + } + addStranger(1230001, "user1", fromGroupId = 2230203) + } + + + runTest().run { + assertEquals(1, size) + val event = single() + assertIs<GroupTempMessageEvent>(event) + assertEquals(1630, event.time) + assertEquals(1230001, event.sender.id) + assertEquals("user1", event.senderName) + + assertEquals("hello", event.message.content) + + event.message.run { + assertEquals(2, size) + get(0).run { + assertIs<OnlineMessageSource.Incoming.FromTemp>(this) + assertContentEquals(intArrayOf(11080), ids) + assertContentEquals(intArrayOf(1854), internalIds) + assertEquals(1630, time) + assertEquals(1230001, fromId) + assertEquals(1230003, targetId) + assertEquals(event.message.filterNot { it is MessageSource }, originalMessage) + } + assertIs<PlainText>(get(1)) + assertEquals("hello", get(1).content) + } + } + } + + // for #1410 + @Test + suspend fun `group temp message test for issue 1410`() { + suspend fun runTest() = use(KEY_FROM_SYNC to false) { + bot.components[SsoProcessor].firstLoginSucceed = true + + net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.Msg( + msgHead = net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.MsgHead( + fromUin = 1230001, + toUin = 1230003, + msgType = 141, + c2cCmd = 11, + msgSeq = 11080, + msgTime = 1630, + msgUid = 720, + c2cTmpMsgHead = net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.C2CTmpMsgHead( + c2cType = 2, + groupUin = 2055561833, + groupCode = 112561833, + sig = "38 59 CD 1E 22 9A 0A BA 28 59 46 BE FA 51 36 D0 F1 7A 5D 54 F5 04 05 7E 66 C7 36 4F 73 BF 45 96 00 39 7C 8F F5 43 57 74 B0 EB D9 5E 0F 1F 9B CF".hexToBytes(), + ), + wseqInC2cMsghead = 25160, + ), + contentHead = net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm.ContentHead( + pkgNum = 1, + ), + msgBody = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.MsgBody( + richText = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.RichText( + attr = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Attr( + codePage = 0, + time = 1630, + random = 1854, + size = 12, + effect = 0, + charSet = 134, + pitchAndFamily = 34, + fontName = "微软雅黑", + ), + elems = mutableListOf( + net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem( + text = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Text( + str = "hello", + ), + ), net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem( + extraInfo = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.ExtraInfo( + nick = "user1", + ), + ), net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem( + elemFlags2 = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.ElemFlags2( + ), + ), net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem( + generalFlags = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.GeneralFlags( + pbReserve = "80 01 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 CA 04 00".hexToBytes(), + ), + ) + ), + ), + ), + ) + } + + setBot(1230003).apply { + addGroup(112561833, 1230002, name = "testtest").apply { + addMember(1230001, permission = MemberPermission.MEMBER, nick = "user1") + } + addStranger(1230001, "user1", fromGroupId = 2230203) + } + + + runTest().run { + assertEquals(1, size) + val event = single() + assertIs<GroupTempMessageEvent>(event) + assertEquals(1630, event.time) + assertEquals(1230001, event.sender.id) + assertEquals("user1", event.senderName) + + assertEquals("hello", event.message.content) + + event.message.run { + assertEquals(2, size) + get(0).run { + assertIs<OnlineMessageSource.Incoming.FromTemp>(this) + assertContentEquals(intArrayOf(11080), ids) + assertContentEquals(intArrayOf(1854), internalIds) + assertEquals(1630, time) + assertEquals(1230001, fromId) + assertEquals(1230003, targetId) + assertEquals(event.message.filterNot { it is MessageSource }, originalMessage) + } + assertIs<PlainText>(get(1)) + assertEquals("hello", get(1).content) + } + } + } + + @Test + fun `stranger message test`() { // TODO: 2021/8/27 cannot start a such conversation + + } +} \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/notice/processors/MuteTest.kt b/mirai-core/src/commonTest/kotlin/notice/processors/MuteTest.kt new file mode 100644 index 00000000000..618cf545200 --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/notice/processors/MuteTest.kt @@ -0,0 +1,265 @@ +/* + * 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/dev/LICENSE + */ + +@file:JvmBlockingBridge + +package net.mamoe.mirai.internal.notice.processors + +import net.mamoe.kjbb.JvmBlockingBridge +import net.mamoe.mirai.contact.MemberPermission +import net.mamoe.mirai.event.events.BotMuteEvent +import net.mamoe.mirai.event.events.BotUnmuteEvent +import net.mamoe.mirai.event.events.MemberMuteEvent +import net.mamoe.mirai.event.events.MemberUnmuteEvent +import net.mamoe.mirai.internal.network.protocol.data.jce.MsgInfo +import net.mamoe.mirai.internal.network.protocol.data.jce.OnlinePushPack +import net.mamoe.mirai.internal.network.protocol.data.jce.ShareData +import net.mamoe.mirai.utils.currentTimeSeconds +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + + +internal class MuteTest : AbstractNoticeProcessorTest() { + @Test + suspend fun `bot mute`() { + suspend fun MuteTest.runTest() = use { + OnlinePushPack.SvcReqPushMsg( + uin = 1230001, + uMsgTime = 1629868940, + vMsgInfos = mutableListOf( + MsgInfo( + lFromUin = 2230203, + shMsgType = 732, + shMsgSeq = 8352, + strMsg = "", + uRealMsgTime = 16298, + vMsg = "00 22 07 BB 0C 01 00 12 C4 B2 61 25 D3 8D 00 01 00 12 C4 B1 00 00 02 58".hexToBytes(), + uAppShareID = 0, + vMsgCookies = "08 DC 05 10 DC 85 E0 80 80 80 80 80 02 18 03 20 DE 86 03".hexToBytes(), + lMsgUid = 1441151, + lLastChangeTime = 1, + vCPicInfo = mutableListOf(), + stShareData = ShareData( + ), + lFromInstId = 0, + vRemarkOfSender = net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY, + strFromMobile = "", + strFromName = "", + vNickName = mutableListOf(), + ) + ), + svrip = -1467, + vSyncCookie = net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY, + vUinPairMsg = mutableListOf(), + mPreviews = mutableMapOf( + ), + ) + + + } + setBot(1230001) + .addGroup(2230203, 1230002, name = "testtest").apply { + addMember(1230002, "user2", MemberPermission.OWNER) + } + + runTest().run { + assertEquals(1, size) + val event = single() + assertIs<BotMuteEvent>(event) + assertEquals(600, event.durationSeconds) + assertEquals(1230002, event.operator.id) + } + } + + @Test + suspend fun `bot unmute`() { + suspend fun MuteTest.runTest() = use { + OnlinePushPack.SvcReqPushMsg( + uin = 1230001, + uMsgTime = 1629869459, + vMsgInfos = mutableListOf( + MsgInfo( + lFromUin = 2230203, + shMsgType = 732, + shMsgSeq = -26716, + strMsg = "", + uRealMsgTime = 1629, + vMsg = "00 22 07 BB 0C 01 00 12 C4 B2 61 25 D5 93 00 01 00 12 C4 B1 00 00 00 00".hexToBytes(), + uAppShareID = 0, + vMsgCookies = "08 DC 05 10 DC 85 E0 80 80 80 80 80 02 18 03 20 DE 86 03".hexToBytes(), + lMsgUid = 1441151, + lLastChangeTime = 1, + vCPicInfo = mutableListOf(), + stShareData = ShareData( + ), + lFromInstId = 0, + vRemarkOfSender = net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY, + strFromMobile = "", + strFromName = "", + vNickName = mutableListOf(), + ) + ), + svrip = 1554, + vSyncCookie = net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY, + vUinPairMsg = mutableListOf(), + mPreviews = mutableMapOf( + ), + ) + } + + setBot(1230001) + .addGroup(2230203, 1230002, name = "testtest").apply { + addMember(1230002, "user2", MemberPermission.OWNER) + addMember(1230003, "user3", MemberPermission.MEMBER) + botAsMember.apply { + _muteTimestamp = currentTimeSeconds().toInt() + 600 + } + } + + runTest().run { + assertEquals(1, size) + val event = single() + assertIs<BotUnmuteEvent>(event) + assertEquals(1230002, event.operator.id) + } + + + setBot(1230001) + .addGroup(2230203, 1230002, name = "testtest").apply { + addMember(1230002, "user2", MemberPermission.OWNER) + addMember(1230003, "user3", MemberPermission.MEMBER) + botAsMember.apply { + _muteTimestamp = 0 + } + } + + runTest().run { + assertEquals(0, size) + } + } + + @Test + suspend fun `member mute`() { + suspend fun MuteTest.runTest() = use { + OnlinePushPack.SvcReqPushMsg( + uin = 1230001, + uMsgTime = 1629870209, + vMsgInfos = mutableListOf( + MsgInfo( + lFromUin = 2230203, + shMsgType = 732, + shMsgSeq = 8159, + strMsg = "", + uRealMsgTime = 16298, + vMsg = "00 22 07 BB 0C 01 00 12 C4 B2 61 25 D8 81 00 01 00 12 C4 B3 00 00 02 58".hexToBytes(), + uAppShareID = 0, + vMsgCookies = "08 DC 05 10 DC 85 E0 80 80 80 80 80 02 18 03 20 DE 86 03".hexToBytes(), + lMsgUid = 1441151, + lLastChangeTime = 1, + vCPicInfo = mutableListOf(), + stShareData = ShareData( + ), + lFromInstId = 0, + vRemarkOfSender = net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY, + strFromMobile = "", + strFromName = "", + vNickName = mutableListOf(), + ) + ), + svrip = -176, + vSyncCookie = net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY, + vUinPairMsg = mutableListOf(), + mPreviews = mutableMapOf( + ), + ) + } + + setBot(1230001) + .addGroup(2230203, 1230002, name = "testtest").apply { + addMember(1230002, "user2", MemberPermission.OWNER) + addMember(1230003, "user3", MemberPermission.MEMBER) + } + + runTest().run { + assertEquals(1, size) + val event = single() + assertIs<MemberMuteEvent>(event) + assertEquals(600, event.durationSeconds) + assertEquals(1230002, event.operator?.id) + } + } + + + @Test + suspend fun `member unmute`() { + suspend fun MuteTest.runTest() = use { + OnlinePushPack.SvcReqPushMsg( + uin = 1230001, + uMsgTime = 16298, + vMsgInfos = mutableListOf( + MsgInfo( + lFromUin = 2230203, + shMsgType = 732, + shMsgSeq = 16929, + strMsg = "", + uRealMsgTime = 16298, + vMsg = "00 22 07 BB 0C 01 00 12 C4 B2 61 25 D7 02 00 01 00 12 C4 B3 00 00 00 00".hexToBytes(), + uAppShareID = 0, + vMsgCookies = "08 DC 05 10 DC 85 E0 80 80 80 80 80 02 18 03 20 DE 86 03".hexToBytes(), + lMsgUid = 1441151, + lLastChangeTime = 1, + vCPicInfo = mutableListOf(), + stShareData = ShareData( + ), + lFromInstId = 0, + vRemarkOfSender = net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY, + strFromMobile = "", + strFromName = "", + vNickName = mutableListOf(), + ) + ), + svrip = 20406, + vSyncCookie = net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY, + vUinPairMsg = mutableListOf(), + mPreviews = mutableMapOf( + ), + ) + } + + setBot(1230001) + .addGroup(2230203, 1230002, name = "testtest").apply { + addMember(1230002, "user2", MemberPermission.OWNER) + addMember(1230003, "user3", MemberPermission.MEMBER).apply { + _muteTimestamp = currentTimeSeconds().toInt() + 600 + } + } + + runTest().run { + assertEquals(1, size) + val event = single() + assertIs<MemberUnmuteEvent>(event) + assertEquals(1230002, event.operator?.id) + } + + + setBot(1230001) + .addGroup(2230203, 1230002, name = "testtest").apply { + addMember(1230002, "user2", MemberPermission.OWNER) + addMember(1230003, "user3", MemberPermission.MEMBER).apply { + _muteTimestamp = 0 + } + } + + runTest().run { + assertEquals(0, size) + } + } + +} \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/notice/test/RecordingNoticeProcessorTest.kt b/mirai-core/src/commonTest/kotlin/notice/test/RecordingNoticeProcessorTest.kt new file mode 100644 index 00000000000..1fe94c5d398 --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/notice/test/RecordingNoticeProcessorTest.kt @@ -0,0 +1,93 @@ +/* + * 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.notice.test + +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.protobuf.ProtoNumber +import net.mamoe.mirai.internal.notice.Desensitizer +import net.mamoe.mirai.internal.test.AbstractTest +import net.mamoe.mirai.internal.utils.io.ProtocolStruct +import net.mamoe.yamlkt.Yaml +import net.mamoe.yamlkt.YamlBuilder +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class RecordingNoticeProcessorTest : AbstractTest() { + + @Serializable + data class MyProtocolStruct( + val value: String + ) : ProtocolStruct + + @Test + fun `test plain desensitization`() { + val text = Thread.currentThread().contextClassLoader.getResource("recording/configs/test.desensitization.yml")!! + .readText() + val desensitizer = Desensitizer.create(Yaml.decodeFromString(text)) + + + assertEquals( + """ + "111": s1av12sad3 + "222": s1av12sad3 + """.trim(), + desensitizer.desensitize( + """ + "123456789": s1av12sad3 + "987654321": s1av12sad3 + """.trim() + ) + ) + } + + + @Serializable + data class TestProto( + @ProtoNumber(1) val proto: Proto + ) : ProtocolStruct { + @Serializable + data class Proto( + @ProtoNumber(1) val int: Int + ) + } + + @Serializable + data class ByteArrayWrapper( + val value: ByteArray + ) + + val format = Yaml { + // one-line + classSerialization = YamlBuilder.MapSerialization.FLOW_MAP + mapSerialization = YamlBuilder.MapSerialization.FLOW_MAP + listSerialization = YamlBuilder.ListSerialization.FLOW_SEQUENCE + stringSerialization = YamlBuilder.StringSerialization.DOUBLE_QUOTATION + encodeDefaultValues = false + } + + + @Test + fun `test long as byte array desensitization`() { + val text = Thread.currentThread().contextClassLoader.getResource("recording/configs/test.desensitization.yml")!! + .readText() + val desensitizer = Desensitizer.create(Yaml.decodeFromString(text)) + + val proto = TestProto(TestProto.Proto(123456789)) + + assertEquals( + TestProto(TestProto.Proto(111)), + format.decodeFromString( + TestProto.serializer(), + desensitizer.desensitize(format.encodeToString(TestProto.serializer(), proto)) + ) + ) + } +} \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/utils/codegen/ConstructorCallCodegenFacade.kt b/mirai-core/src/commonTest/kotlin/utils/codegen/ConstructorCallCodegenFacade.kt new file mode 100644 index 00000000000..60bdbdd6da3 --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/utils/codegen/ConstructorCallCodegenFacade.kt @@ -0,0 +1,105 @@ +/* + * 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.utils.codegen + +import kotlinx.serialization.Serializable +import net.mamoe.mirai.utils.cast +import kotlin.reflect.KParameter +import kotlin.reflect.KProperty1 +import kotlin.reflect.KType +import kotlin.reflect.full.declaredMemberProperties +import kotlin.reflect.full.hasAnnotation +import kotlin.reflect.full.primaryConstructor +import kotlin.reflect.full.valueParameters +import kotlin.reflect.typeOf + +object ConstructorCallCodegenFacade { + /** + * Analyze [value] and give its correspondent [ValueDesc]. + */ + fun analyze(value: Any?, type: KType): ValueDesc { + if (value == null) return PlainValueDesc(null, "null", null) + + val clazz = value::class + + if (clazz.isData || clazz.hasAnnotation<Serializable>()) { + val primaryConstructor = + clazz.primaryConstructor ?: error("$value does not have a primary constructor.") + val properties = clazz.declaredMemberProperties + + val map = mutableMapOf<KParameter, ValueDesc>() + + for (valueParameter in primaryConstructor.valueParameters) { + val prop = properties.find { it.name == valueParameter.name } + ?: error("Could not find corresponding property for parameter ${clazz.qualifiedName}.${valueParameter.name}") + + prop.cast<KProperty1<Any, Any?>>() + map[valueParameter] = analyze(prop.get(value), prop.returnType) + } + return ClassValueDesc(null, value, map) + } + + ArrayValueDesc.createOrNull(value, type, null)?.let { return it } + if (value is Collection<*>) { + return CollectionValueDesc(null, value, arrayType = type, elementType = type.arguments.first().type!!) + } else if (value is Map<*, *>) { + return MapValueDesc( + null, + value.cast(), + value.cast(), + type, + type.arguments.first().type!!, + type.arguments[1].type!! + ) + } + + return when (value) { + is CharSequence -> { + PlainValueDesc(null, '"' + value.toString() + '"', value) + } + is Char -> { + PlainValueDesc(null, "'$value'", value) + } + else -> PlainValueDesc(null, value.toString(), value) + } + } + + /** + * Generate source code to construct the value represented by [desc]. + */ + fun generate(desc: ValueDesc, context: CodegenContext = CodegenContext()): String { + if (context.configuration.removeDefaultValues) { + val def = AnalyzeDefaultValuesMappingVisitor() + desc.accept(def) + desc.accept(RemoveDefaultValuesVisitor(def.mappings)) + } + + ValueCodegen(context).generate(desc) + return context.getResult() + } + + fun analyzeAndGenerate(value: Any?, type: KType, context: CodegenContext = CodegenContext()): String { + return generate(analyze(value, type), context) + } +} + +@OptIn(ExperimentalStdlibApi::class) +inline fun <reified T> ConstructorCallCodegenFacade.analyze(value: T): ValueDesc { + return analyze(value, typeOf<T>()) +} + +@OptIn(ExperimentalStdlibApi::class) +inline fun <reified T> ConstructorCallCodegenFacade.analyzeAndGenerate( + value: T, + context: CodegenContext = CodegenContext() +): String { + return analyzeAndGenerate(value, typeOf<T>(), context) +} + diff --git a/mirai-core/src/commonTest/kotlin/utils/codegen/ValueCodegen.kt b/mirai-core/src/commonTest/kotlin/utils/codegen/ValueCodegen.kt new file mode 100644 index 00000000000..f8365ae9adb --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/utils/codegen/ValueCodegen.kt @@ -0,0 +1,137 @@ +/* + * 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.utils.codegen + +import net.mamoe.mirai.utils.encodeToString +import net.mamoe.mirai.utils.toUHexString + +class ValueCodegen( + val context: CodegenContext +) { + fun generate(desc: ValueDesc) { + when (desc) { + is PlainValueDesc -> generate(desc) + is ObjectArrayValueDesc -> generate(desc) + is PrimitiveArrayValueDesc -> generate(desc) + is CollectionValueDesc -> generate(desc) + is ClassValueDesc<*> -> generate(desc) + is MapValueDesc -> generate(desc) + } + } + + fun generate(desc: PlainValueDesc) { + context.append(desc.value) + } + + fun generate(desc: MapValueDesc) { + context.run { + appendLine("mutableMapOf(") + for ((key, value) in desc.elements) { + generate(key) + append(" to ") + generate(value) + appendLine(",") + } + append(")") + } + } + + fun <T : Any> generate(desc: ClassValueDesc<T>) { + context.run { + appendLine("${desc.type.qualifiedName}(") + for ((param, valueDesc) in desc.properties) { + append(param.name) + append("=") + generate(valueDesc) + appendLine(",") + } + append(")") + } + } + + fun generate(desc: ArrayValueDesc) { + val array = desc.value + + fun impl(funcName: String, elements: List<ValueDesc>) { + context.run { + append(funcName) + append('(') + val list = elements.toList() + list.forEachIndexed { index, desc -> + generate(desc) + if (index != list.lastIndex) append(", ") + } + append(')') + } + } + + return when (array) { + is Array<*> -> impl("arrayOf", desc.elements) + is IntArray -> impl("intArrayOf", desc.elements) + is ByteArray -> { + if (array.size == 0) { + context.append("net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY") // let IDE to shorten references. + return + } else { + if (array.encodeToString().all { Character.isUnicodeIdentifierPart(it) || it.isWhitespace() }) { + // prefers to show readable string + context.append( + "\"${ + array.encodeToString().escapeQuotation() + }\".toByteArray() /* ${array.toUHexString()} */" + ) + } else { + context.append("\"${array.toUHexString()}\".hexToBytes()") + } + return + } + } + is ShortArray -> impl("shortArrayOf", desc.elements) + is CharArray -> impl("charArrayOf", desc.elements) + is LongArray -> impl("longArrayOf", desc.elements) + is FloatArray -> impl("floatArrayOf", desc.elements) + is DoubleArray -> impl("doubleArrayOf", desc.elements) + is BooleanArray -> impl("booleanArrayOf", desc.elements) + is List<*> -> impl("mutableListOf", desc.elements) + is Set<*> -> impl("mutableSetOf", desc.elements) + else -> error("$array is not an array.") + } + } +} + +class CodegenContext( + val sb: StringBuilder = StringBuilder(), + val configuration: CodegenConfiguration = CodegenConfiguration() +) : Appendable by sb { + fun getResult(): String { + return sb.toString() + } +} + +class CodegenConfiguration( + var removeDefaultValues: Boolean = true, +) + + +private fun String.escapeQuotation(): String = buildString { this@escapeQuotation.escapeQuotationTo(this) } + +private fun String.escapeQuotationTo(out: StringBuilder) { + for (i in 0 until length) { + when (val ch = this[i]) { + '\\' -> out.append("\\\\") + '\n' -> out.append("\\n") + '\r' -> out.append("\\r") + '\t' -> out.append("\\t") + '\"' -> out.append("\\\"") + else -> out.append(ch) + } + } +} + diff --git a/mirai-core/src/commonTest/kotlin/utils/codegen/ValueDesc.kt b/mirai-core/src/commonTest/kotlin/utils/codegen/ValueDesc.kt new file mode 100644 index 00000000000..767cf692647 --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/utils/codegen/ValueDesc.kt @@ -0,0 +1,181 @@ +/* + * 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.utils.codegen + +import kotlin.reflect.KClass +import kotlin.reflect.KParameter +import kotlin.reflect.KType +import kotlin.reflect.full.createType +import kotlin.reflect.typeOf + +sealed interface ValueDesc { + val origin: Any? + val parent: ValueDesc? + + fun accept(visitor: ValueDescVisitor) +} + +val ValueDesc.parents + get() = sequence { + var parent = parent + do { + parent ?: return@sequence + yield(parent) + parent = parent.parent + } while (true); + } + +inline fun <reified T : ValueDesc> ValueDesc.findParent(): T? = parents.filterIsInstance<T>().firstOrNull() + +sealed interface ArrayValueDesc : ValueDesc { + val value: Any + + val arrayType: KType + val elementType: KType + val elements: MutableList<ValueDesc> + + companion object { + @OptIn(ExperimentalStdlibApi::class) + fun createOrNull(array: Any, type: KType, parent: ValueDesc?): ArrayValueDesc? { + if (array is Array<*>) return ObjectArrayValueDesc(parent, array, arrayType = type) + return when (array) { + is IntArray -> PrimitiveArrayValueDesc(parent, array, arrayType = type, elementType = typeOf<Int>()) + is ByteArray -> PrimitiveArrayValueDesc(parent, array, arrayType = type, elementType = typeOf<Byte>()) + is ShortArray -> PrimitiveArrayValueDesc(parent, array, arrayType = type, elementType = typeOf<Short>()) + is CharArray -> PrimitiveArrayValueDesc(parent, array, arrayType = type, elementType = typeOf<Char>()) + is LongArray -> PrimitiveArrayValueDesc(parent, array, arrayType = type, elementType = typeOf<Long>()) + is FloatArray -> PrimitiveArrayValueDesc(parent, array, arrayType = type, elementType = typeOf<Float>()) + is DoubleArray -> PrimitiveArrayValueDesc( + parent, + array, + arrayType = type, + elementType = typeOf<Double>() + ) + is BooleanArray -> PrimitiveArrayValueDesc( + parent, + array, + arrayType = type, + elementType = typeOf<Boolean>() + ) + else -> return null + } + } + } +} + +class ObjectArrayValueDesc( + override val parent: ValueDesc?, + override var value: Array<*>, + override val origin: Array<*> = value, + override val arrayType: KType, + override val elementType: KType = arrayType.arguments.first().type ?: Any::class.createType(), +) : ArrayValueDesc { + override val elements: MutableList<ValueDesc> by lazy { + value.mapTo(mutableListOf()) { + ConstructorCallCodegenFacade.analyze(it, elementType) + } + } + + override fun accept(visitor: ValueDescVisitor) { + visitor.visitObjectArray(this) + } +} + +class CollectionValueDesc( + override val parent: ValueDesc?, + override var value: Collection<*>, + override val origin: Collection<*> = value, + override val arrayType: KType, + override val elementType: KType = arrayType.arguments.first().type ?: Any::class.createType() +) : ArrayValueDesc { + override val elements: MutableList<ValueDesc> by lazy { + value.mapTo(mutableListOf()) { + ConstructorCallCodegenFacade.analyze(it, elementType) + } + } + + override fun accept(visitor: ValueDescVisitor) { + visitor.visitCollection(this) + } +} + +class MapValueDesc( + override val parent: ValueDesc?, + var value: Map<Any?, Any?>, + override val origin: Map<Any?, Any?> = value, + val mapType: KType, + val keyType: KType = mapType.arguments.first().type ?: Any::class.createType(), + val valueType: KType = mapType.arguments[1].type ?: Any::class.createType(), +) : ValueDesc { + val elements: MutableMap<ValueDesc, ValueDesc> by lazy { + value.map { + ConstructorCallCodegenFacade.analyze(it.key, keyType) to ConstructorCallCodegenFacade.analyze( + it.value, + valueType + ) + }.toMap(mutableMapOf()) + } + + override fun accept(visitor: ValueDescVisitor) { + visitor.visitMap(this) + } +} + +class PrimitiveArrayValueDesc( + override val parent: ValueDesc?, + override var value: Any, + override val origin: Any = value, + override val arrayType: KType, + override val elementType: KType +) : ArrayValueDesc { + override val elements: MutableList<ValueDesc> by lazy { + when (val value = value) { + is IntArray -> value.mapTo(mutableListOf()) { ConstructorCallCodegenFacade.analyze(it, elementType) } + is ByteArray -> value.mapTo(mutableListOf()) { ConstructorCallCodegenFacade.analyze(it, elementType) } + is ShortArray -> value.mapTo(mutableListOf()) { ConstructorCallCodegenFacade.analyze(it, elementType) } + is CharArray -> value.mapTo(mutableListOf()) { ConstructorCallCodegenFacade.analyze(it, elementType) } + is LongArray -> value.mapTo(mutableListOf()) { ConstructorCallCodegenFacade.analyze(it, elementType) } + is FloatArray -> value.mapTo(mutableListOf()) { ConstructorCallCodegenFacade.analyze(it, elementType) } + is DoubleArray -> value.mapTo(mutableListOf()) { ConstructorCallCodegenFacade.analyze(it, elementType) } + is BooleanArray -> value.mapTo(mutableListOf()) { ConstructorCallCodegenFacade.analyze(it, elementType) } + else -> error("$value is not an array.") + } + } + + override fun accept(visitor: ValueDescVisitor) { + visitor.visitPrimitiveArray(this) + } +} + +class PlainValueDesc( + override val parent: ValueDesc?, + var value: String, + override val origin: Any? +) : ValueDesc { + init { + require(value.isNotBlank()) + } + + override fun accept(visitor: ValueDescVisitor) { + visitor.visitPlain(this) + } +} + +class ClassValueDesc<T : Any>( + override val parent: ValueDesc?, + override val origin: T, + val properties: MutableMap<KParameter, ValueDesc>, +) : ValueDesc { + val type: KClass<out T> by lazy { origin::class } + + override fun accept(visitor: ValueDescVisitor) { + visitor.visitClass(this) + } +} \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/utils/codegen/ValueDescVisitor.kt b/mirai-core/src/commonTest/kotlin/utils/codegen/ValueDescVisitor.kt new file mode 100644 index 00000000000..7c9a0083230 --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/utils/codegen/ValueDescVisitor.kt @@ -0,0 +1,178 @@ +/* + * 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.utils.codegen + +import net.mamoe.mirai.utils.cast +import kotlin.reflect.KClass +import kotlin.reflect.KParameter +import kotlin.reflect.KProperty +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.memberProperties +import kotlin.reflect.full.primaryConstructor + +interface ValueDescVisitor { + fun visitValue(desc: ValueDesc) {} + + fun visitPlain(desc: PlainValueDesc) { + visitValue(desc) + } + + fun visitArray(desc: ArrayValueDesc) { + visitValue(desc) + for (element in desc.elements) { + element.accept(this) + } + } + + fun visitObjectArray(desc: ObjectArrayValueDesc) { + visitArray(desc) + } + + fun visitCollection(desc: CollectionValueDesc) { + visitArray(desc) + } + + fun visitMap(desc: MapValueDesc) { + visitValue(desc) + for ((key, value) in desc.elements.entries) { + key.accept(this) + value.accept(this) + } + } + + fun visitPrimitiveArray(desc: PrimitiveArrayValueDesc) { + visitArray(desc) + } + + fun <T : Any> visitClass(desc: ClassValueDesc<T>) { + visitValue(desc) + desc.properties.forEach { (_, u) -> + u.accept(this) + } + } +} + + +class DefaultValuesMapping( + val forClass: KClass<*>, + val mapping: MutableMap<String, Any?> = mutableMapOf() +) { + operator fun get(property: KProperty<*>): Any? = mapping[property.name] +} + +class AnalyzeDefaultValuesMappingVisitor : ValueDescVisitor { + val mappings: MutableList<DefaultValuesMapping> = mutableListOf() + + override fun <T : Any> visitClass(desc: ClassValueDesc<T>) { + super.visitClass(desc) + + if (mappings.any { it.forClass == desc.type }) return + + val defaultInstance = + createInstanceWithMostDefaultValues(desc.type, desc.properties.mapValues { it.value.origin }) + + val optionalParameters = desc.type.primaryConstructor!!.parameters.filter { it.isOptional } + + mappings.add( + DefaultValuesMapping( + desc.type, + optionalParameters.associateTo(mutableMapOf()) { param -> + val value = findCorrespondingProperty(desc, param).get(defaultInstance) + param.name!! to value + } + ) + ) + } + + + private fun <T : Any> findCorrespondingProperty( + desc: ClassValueDesc<T>, + param: KParameter + ) = desc.type.memberProperties.single { it.name == param.name }.cast<KProperty1<Any, Any>>() + + private fun <T : Any> createInstanceWithMostDefaultValues(clazz: KClass<T>, arguments: Map<KParameter, Any?>): T { + val primaryConstructor = clazz.primaryConstructor ?: error("Type $clazz does not have primary constructor.") + return primaryConstructor.callBy(arguments.filter { !it.key.isOptional }) + } +} + +class RemoveDefaultValuesVisitor( + private val mappings: MutableList<DefaultValuesMapping>, +) : ValueDescVisitor { + override fun <T : Any> visitClass(desc: ClassValueDesc<T>) { + super.visitClass(desc) + val mapping = mappings.find { it.forClass == desc.type }?.mapping ?: return + + // remove properties who have the same values as their default values, this would significantly reduce code size. + mapping.forEach { (name, defaultValue) -> + if (desc.properties.entries.removeIf { + it.key.name == name && isDefaultOrEmpty(it.key, it.value, defaultValue) + }) { + return@forEach // by removing one property, there will not by any other matches + } + } + } + + private fun isDefaultOrEmpty(key: KParameter, value: ValueDesc, defaultValue: Any?): Boolean { + if (!key.isOptional) return false + if (equals(value.origin, defaultValue)) return true + + if (value is ClassValueDesc<*> + && value.properties.all { it.key.isOptional && isDefaultOrEmpty(it.key, it.value, defaultValue) } + ) { + return true + } + + return false + } + + private fun Any?.isNullOrZeroOrEmpty(): Boolean { + when (this) { + null, + 0.toByte(), 0.toShort(), 0, 0L, 0.toFloat(), 0.toDouble(), 0.toChar(), + "", listOf<Any>(), setOf<Any>(), mapOf<Any, Any>(), + -> return true + } + + check(this != null) + + when { + this is Array<*> && this.isEmpty() -> return true + this is IntArray && this.isEmpty() -> return true + this is ByteArray && this.isEmpty() -> return true + this is ShortArray && this.isEmpty() -> return true + this is LongArray && this.isEmpty() -> return true + this is CharArray && this.isEmpty() -> return true + this is FloatArray && this.isEmpty() -> return true + this is DoubleArray && this.isEmpty() -> return true + this is BooleanArray && this.isEmpty() -> return true + } + + return false + } + + fun equals(a: Any?, b: Any?): Boolean { + if (a.isNullOrZeroOrEmpty() && b.isNullOrZeroOrEmpty()) return true + return when { + a === b -> true + a == b -> true + a is Array<*>? && b is Array<*>? -> a.contentEquals(b) + a is IntArray? && b is IntArray? -> a.contentEquals(b) + a is ByteArray? && b is ByteArray? -> a.contentEquals(b) + a is ShortArray? && b is ShortArray? -> a.contentEquals(b) + a is LongArray? && b is LongArray? -> a.contentEquals(b) + a is CharArray? && b is CharArray? -> a.contentEquals(b) + a is FloatArray? && b is FloatArray? -> a.contentEquals(b) + a is DoubleArray? && b is DoubleArray? -> a.contentEquals(b) + a is BooleanArray? && b is BooleanArray? -> a.contentEquals(b) + else -> false + } + } +} diff --git a/mirai-core/src/commonTest/kotlin/utils/codegen/test/ConstructorCallCodegenTest.kt b/mirai-core/src/commonTest/kotlin/utils/codegen/test/ConstructorCallCodegenTest.kt new file mode 100644 index 00000000000..fec8533cf54 --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/utils/codegen/test/ConstructorCallCodegenTest.kt @@ -0,0 +1,112 @@ +/* + * 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.utils.codegen.test + +import net.mamoe.mirai.internal.utils.codegen.ConstructorCallCodegenFacade +import net.mamoe.mirai.internal.utils.codegen.analyzeAndGenerate +import kotlin.test.Test +import kotlin.test.assertEquals + +class ConstructorCallCodegenTest { + + @Test + fun `test plain`() { + assertEquals( + "\"test\"", + ConstructorCallCodegenFacade.analyzeAndGenerate("test") + ) + assertEquals( + "1", + ConstructorCallCodegenFacade.analyzeAndGenerate(1) + ) + assertEquals( + "1.0", + ConstructorCallCodegenFacade.analyzeAndGenerate(1.0) + ) + } + + @Test + fun `test array`() { + assertEquals( + "arrayOf(1, 2)", + ConstructorCallCodegenFacade.analyzeAndGenerate(arrayOf(1, 2)) + ) + assertEquals( + "arrayOf(5.0)", + ConstructorCallCodegenFacade.analyzeAndGenerate(arrayOf(5.0)) + ) + assertEquals( + "arrayOf(\"1\")", + ConstructorCallCodegenFacade.analyzeAndGenerate(arrayOf("1")) + ) + assertEquals( + "arrayOf(arrayOf(1))", + ConstructorCallCodegenFacade.analyzeAndGenerate(arrayOf(arrayOf(1))) + ) + } + + data class TestClass( + val value: String + ) + + data class TestClass2( + val value: Any + ) + + @Test + fun `test class`() { + assertEquals( + """ + ${TestClass::class.qualifiedName!!}( + value="test", + ) + """.trimIndent(), + ConstructorCallCodegenFacade.analyzeAndGenerate(TestClass("test")) + ) + assertEquals( + """ + ${TestClass2::class.qualifiedName!!}( + value="test", + ) + """.trimIndent(), + ConstructorCallCodegenFacade.analyzeAndGenerate(TestClass2("test")) + ) + assertEquals( + """ + ${TestClass2::class.qualifiedName!!}( + value=1, + ) + """.trimIndent(), + ConstructorCallCodegenFacade.analyzeAndGenerate(TestClass2(1)) + ) + } + + data class TestNesting( + val nested: Nested + ) { + data class Nested( + val value: String + ) + } + + @Test + fun `test nesting`() { + assertEquals( + """ + net.mamoe.mirai.internal.utils.codegen.test.ConstructorCallCodegenTest.TestNesting( + nested=net.mamoe.mirai.internal.utils.codegen.test.ConstructorCallCodegenTest.TestNesting.Nested( + value="test", + ), + ) + """.trimIndent(), + ConstructorCallCodegenFacade.analyzeAndGenerate(TestNesting(TestNesting.Nested("test"))) + ) + } +} \ No newline at end of file diff --git a/mirai-core/src/commonTest/resources/recording/configs/desensitization.yml b/mirai-core/src/commonTest/resources/recording/configs/desensitization.yml new file mode 100644 index 00000000000..1b16f027279 --- /dev/null +++ b/mirai-core/src/commonTest/resources/recording/configs/desensitization.yml @@ -0,0 +1,20 @@ +# Template for Desensitization in recordings +# +# Format: +# ``` +# <sensitive value>: <replacement> +# ``` +# +# If key is a number, its group uin counterpart will also be processed, with calculated replacer. +# WARNING: Ensure the <replacement> is not longer than <sensitive value>. +# +# For example, if your account id is 147258369, you may add: +# ``` +# 147258369: 123456 +# ``` +# Then your id will be replaced with 123456. +# +# +# To use desensitization, duplicate this file into name "local.desensitization.yml". + +123456789: 111 \ No newline at end of file diff --git a/mirai-core/src/commonTest/resources/recording/configs/test.desensitization.yml b/mirai-core/src/commonTest/resources/recording/configs/test.desensitization.yml new file mode 100644 index 00000000000..43ca7ec12e6 --- /dev/null +++ b/mirai-core/src/commonTest/resources/recording/configs/test.desensitization.yml @@ -0,0 +1,2 @@ +123456789: 111 +987654321: 222 \ No newline at end of file diff --git a/mirai-core/src/jvmTest/kotlin/bootstrap/RunRecorder.kt b/mirai-core/src/jvmTest/kotlin/bootstrap/RunRecorder.kt new file mode 100644 index 00000000000..abd154fca75 --- /dev/null +++ b/mirai-core/src/jvmTest/kotlin/bootstrap/RunRecorder.kt @@ -0,0 +1,52 @@ +/* + * 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/dev/LICENSE + */ + +package net.mamoe.mirai.internal.bootstrap + +import kotlinx.serialization.Serializable +import net.mamoe.mirai.Bot +import net.mamoe.mirai.BotFactory +import net.mamoe.mirai.internal.asQQAndroidBot +import net.mamoe.mirai.internal.network.components.NoticeProcessorPipeline +import net.mamoe.mirai.internal.notice.Desensitizer +import net.mamoe.mirai.internal.notice.RecordingNoticeProcessor +import net.mamoe.mirai.utils.BotConfiguration +import net.mamoe.mirai.utils.readResource +import net.mamoe.yamlkt.Yaml +import kotlin.concurrent.thread + +@Serializable +data class LocalAccount( + val id: Long, + val password: String +) + +suspend fun main() { + Runtime.getRuntime().addShutdownHook(thread(start = false) { + Bot.instances.forEach { + it.close() + } + }) + + + Desensitizer.local.desensitize("") // verify rules + + val account = Yaml.decodeFromString(LocalAccount.serializer(), readResource("local.account.yml")) + val bot = BotFactory.newBot(account.id, account.password) { + enableContactCache() + fileBasedDeviceInfo("local.device.json") + protocol = BotConfiguration.MiraiProtocol.ANDROID_PHONE + }.asQQAndroidBot() + + bot.components[NoticeProcessorPipeline].registerProcessor(RecordingNoticeProcessor()) + + bot.login() + + bot.join() +} \ No newline at end of file diff --git a/mirai-core/src/jvmTest/resources/account.yml b/mirai-core/src/jvmTest/resources/account.yml new file mode 100644 index 00000000000..4269245870e --- /dev/null +++ b/mirai-core/src/jvmTest/resources/account.yml @@ -0,0 +1,2 @@ +id: 123 +password: "" \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 5f7b234d978..cea7b73aee8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,6 +9,9 @@ pluginManagement { repositories { + if (System.getProperty("use.maven.local") == "true") { + mavenLocal() + } // mavenLocal() gradlePluginPortal() mavenCentral()