mirror of
https://github.com/mamoe/mirai.git
synced 2025-04-25 04:50:26 +08:00
Rewrite
This commit is contained in:
parent
da18aafa2f
commit
2f67f8363b
README.md
mirai-api-http
mirai-core-timpc
build.gradle.kts
src
androidMain/kotlin/net/mamoe/mirai/timpc
commonMain/kotlin/net.mamoe.mirai.timpc
TIMPC.ktTIMPCBot.kt
internal
network
ContactImpl.ktTIMBotNetworkHandler.ktTIMProtocol.kt
handler
packet
Annotations.ktDecrypters.ktOutgoingPacket.ktPacket.ktPacketFactory.ktPacketId.kt
action
AddContact.ktFriendImage.ktFriendList.ktGradeInfo.ktGroupImage.ktGroupPacket.ktHttpAPIAccessor.ktImage.ktProfile.ktRemark.ktRequestFriendListPacket.ktSendFriendMessagePacket.kt
event
AndroidOnlineStatusChange.ktConnectionOccupiedEvent.ktEventPacketFactory.ktFriendAddRequestEventPacket.ktFriendConversationIniliaze.ktFriendOnlineStatusChanged.ktGroupFileUpload.ktIgnored.ktMemberJoin.ktMemberKickEvent.ktMemberMute.ktMemberPermission.ktMessageEvent.ktUnknown.kt
login
utils
jvmMain/kotlin/net/mamoe/mirai/timpc
jvmTest/kotlin
main
mirai-core/src
androidMain/kotlin/net/mamoe/mirai
message
network
utils
androidTest/java
commonMain/kotlin/net.mamoe.mirai
Bot.ktBotAccount.ktBotAddFriend.ktBotFactory.ktBotHelper.ktBotImpl.kt
contact
event
MessageSubscribers.ktSubscribersWithBot.kt
events
ConnectionOccupiedEvent.ktFriendStatusChanged.ktMuteEvent.ktPacketEvents.ktReceiveFriendAddRequestEvent.kt
internal
message
network
@ -31,17 +31,18 @@ repositories{
|
||||
您需要将 `VERSION` 替换为最新的版本(如 `0.5.1`): [](https://bintray.com/him188moe/mirai/mirai-core/)
|
||||
Mirai 目前还处于实验性阶段, 建议您时刻保持最新版本.
|
||||
|
||||
现在 Mirai 只支持 TIM PC 协议.
|
||||
**common**
|
||||
```kotlin
|
||||
implementation("net.mamoe:mirai-core-common:VERSION")
|
||||
implementation("net.mamoe:mirai-core-timpc-common:VERSION")
|
||||
```
|
||||
**jvm**
|
||||
```kotlin
|
||||
implementation("net.mamoe:mirai-core-jvm:VERSION")
|
||||
implementation("net.mamoe:mirai-core-timpc-jvm:VERSION")
|
||||
```
|
||||
**android**
|
||||
```kotlin
|
||||
implementation("net.mamoe:mirai-core-android:VERSION")
|
||||
implementation("net.mamoe:mirai-core-timpc-android:VERSION")
|
||||
```
|
||||
|
||||
## Try
|
||||
|
@ -33,7 +33,7 @@ kotlin {
|
||||
|
||||
sourceSets["main"].apply {
|
||||
dependencies {
|
||||
implementation(project(":mirai-core"))
|
||||
implementation(project(":mirai-core-timpc"))
|
||||
|
||||
implementation(kotlin("stdlib-jdk8", kotlinVersion))
|
||||
implementation(kotlin("stdlib-jdk7", kotlinVersion))
|
||||
|
@ -17,9 +17,7 @@ import io.ktor.util.pipeline.ContextDsl
|
||||
import io.ktor.util.pipeline.PipelineContext
|
||||
import io.ktor.util.pipeline.PipelineInterceptor
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.addFriend
|
||||
import net.mamoe.mirai.contact.sendMessage
|
||||
import net.mamoe.mirai.getGroup
|
||||
import net.mamoe.mirai.utils.io.hexToBytes
|
||||
import net.mamoe.mirai.utils.io.hexToUBytes
|
||||
|
||||
@ -40,7 +38,7 @@ fun Application.mirai() {
|
||||
}
|
||||
|
||||
mirai("/sendGroupMessage") {
|
||||
Bot.instanceWhose(qq = param("bot")).getGroup(param<UInt>("group")).sendMessage(param<String>("message"))
|
||||
Bot.instanceWhose(qq = param("bot")).getGroup(param<Long>("group")).sendMessage(param<String>("message"))
|
||||
call.ok()
|
||||
}
|
||||
|
||||
|
110
mirai-core-timpc/build.gradle.kts
Normal file
110
mirai-core-timpc/build.gradle.kts
Normal file
@ -0,0 +1,110 @@
|
||||
@file:Suppress("UNUSED_VARIABLE")
|
||||
|
||||
plugins {
|
||||
kotlin("multiplatform")
|
||||
id("kotlinx-atomicfu")
|
||||
id("com.android.library")
|
||||
id("kotlinx-serialization")
|
||||
`maven-publish`
|
||||
id("com.jfrog.bintray") version "1.8.4-jetbrains-3" // DO NOT CHANGE THIS VERSION UNLESS YOU WANT TO WASTE YOUR TIME
|
||||
}
|
||||
|
||||
apply(from = rootProject.file("gradle/publish.gradle"))
|
||||
|
||||
val kotlinVersion: String by rootProject.ext
|
||||
val atomicFuVersion: String by rootProject.ext
|
||||
val coroutinesVersion: String by rootProject.ext
|
||||
val kotlinXIoVersion: String by rootProject.ext
|
||||
val coroutinesIoVersion: String by rootProject.ext
|
||||
|
||||
val klockVersion: String by rootProject.ext
|
||||
val ktorVersion: String by rootProject.ext
|
||||
|
||||
val serializationVersion: String by rootProject.ext
|
||||
|
||||
fun kotlinx(id: String, version: String) = "org.jetbrains.kotlinx:kotlinx-$id:$version"
|
||||
|
||||
fun ktor(id: String, version: String) = "io.ktor:ktor-$id:$version"
|
||||
|
||||
|
||||
description = "QQ protocol library"
|
||||
|
||||
kotlin {
|
||||
android("android") {
|
||||
publishAllLibraryVariants()
|
||||
project.android {
|
||||
compileSdkVersion(29)
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion(15)
|
||||
}
|
||||
|
||||
// sourceSets.filterIsInstance(com.android.build.gradle.api.AndroidSourceSet::class.java).forEach {
|
||||
// it.manifest.srcFile("src/androidMain/res/AndroidManifest.xml")
|
||||
// it.res.srcDirs(file("src/androidMain/res"))
|
||||
// }
|
||||
//(sourceSets["main"] as AndroidSourceSet).java.srcDirs(file("src/androidMain/kotlin"))
|
||||
}
|
||||
}
|
||||
|
||||
jvm("jvm") {
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
all {
|
||||
languageSettings.enableLanguageFeature("InlineClasses")
|
||||
languageSettings.useExperimentalAnnotation("kotlin.Experimental")
|
||||
|
||||
dependencies {
|
||||
api(project(":mirai-core"))
|
||||
|
||||
api(kotlin("stdlib", kotlinVersion))
|
||||
api(kotlin("serialization", kotlinVersion))
|
||||
|
||||
api("org.jetbrains.kotlinx:atomicfu:$atomicFuVersion")
|
||||
api(kotlinx("io", kotlinXIoVersion))
|
||||
api(kotlinx("coroutines-io", coroutinesIoVersion))
|
||||
api(kotlinx("coroutines-core", coroutinesVersion))
|
||||
}
|
||||
}
|
||||
commonMain {
|
||||
dependencies {
|
||||
}
|
||||
}
|
||||
commonTest {
|
||||
dependencies {
|
||||
api(kotlin("test-annotations-common"))
|
||||
api(kotlin("test-common"))
|
||||
}
|
||||
}
|
||||
|
||||
val androidMain by getting {
|
||||
dependencies {
|
||||
}
|
||||
}
|
||||
|
||||
val androidTest by getting {
|
||||
dependencies {
|
||||
api(kotlin("test", kotlinVersion))
|
||||
api(kotlin("test-junit", kotlinVersion))
|
||||
api(kotlin("test-annotations-common"))
|
||||
api(kotlin("test-common"))
|
||||
}
|
||||
}
|
||||
|
||||
val jvmMain by getting {
|
||||
dependencies {
|
||||
}
|
||||
}
|
||||
|
||||
val jvmTest by getting {
|
||||
dependencies {
|
||||
api(kotlin("test", kotlinVersion))
|
||||
api(kotlin("test-junit", kotlinVersion))
|
||||
implementation("org.pcap4j:pcap4j-distribution:1.8.2")
|
||||
|
||||
//runtimeOnly(files("build/classes/kotlin/jvm/test")) // classpath is not properly set by IDE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package net.mamoe.mirai.timpc
|
||||
|
||||
import kotlinx.io.InputStream
|
||||
import kotlinx.io.streams.inputStream
|
||||
import net.mamoe.mirai.BotAccount
|
||||
import net.mamoe.mirai.message.data.Image
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
internal actual class TIMPCBot actual constructor(
|
||||
account: BotAccount,
|
||||
logger: MiraiLogger?,
|
||||
context: CoroutineContext
|
||||
) : TIMPCBotBase(account, logger, context) {
|
||||
suspend inline fun Image.downloadAsStream(): InputStream = download().inputStream()
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package net.mamoe.mirai.network.protocol.timpc
|
||||
package net.mamoe.mirai.timpc.network
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
@ -9,4 +9,5 @@ import java.util.concurrent.Executors
|
||||
*
|
||||
* JVM: 独立的 4 thread 调度器
|
||||
*/
|
||||
internal actual val NetworkDispatcher: CoroutineDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher()
|
||||
internal actual val NetworkDispatcher: CoroutineDispatcher
|
||||
get() = Executors.newFixedThreadPool(4).asCoroutineDispatcher()
|
@ -0,0 +1,76 @@
|
||||
@file:Suppress("FunctionName", "unused", "SpellCheckingInspection")
|
||||
|
||||
package net.mamoe.mirai.timpc
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.BotAccount
|
||||
import net.mamoe.mirai.BotFactory
|
||||
import net.mamoe.mirai.timpc.TIMPC.Bot
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
/**
|
||||
* TIM PC 协议的 [Bot] 构造器.
|
||||
*/
|
||||
object TIMPC : BotFactory {
|
||||
/**
|
||||
* 在当前 CoroutineScope 下构造 Bot 实例
|
||||
* 该 Bot 实例的生命周期将会跟随这个 CoroutineScope.
|
||||
* 这个 CoroutineScope 也会等待 Bot 的结束才会结束.
|
||||
*
|
||||
* ```kotlin
|
||||
* suspend fun myProcess(){
|
||||
* TIMPC.Bot(account, logger)
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
override suspend fun Bot(account: BotAccount, logger: MiraiLogger?): Bot =
|
||||
TIMPCBot(account, logger, coroutineContext)
|
||||
|
||||
/**
|
||||
* 在当前 CoroutineScope 下构造 Bot 实例
|
||||
* 该 Bot 实例的生命周期将会跟随这个 CoroutineScope.
|
||||
* 这个 CoroutineScope 也会等待 Bot 的结束才会结束.
|
||||
*
|
||||
* ```kotlin
|
||||
* suspend fun myProcess(){
|
||||
* TIMPC.Bot(account, logger)
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
override suspend fun Bot(qq: Long, password: String, logger: MiraiLogger?): Bot =
|
||||
TIMPCBot(BotAccount(qq, password), logger, coroutineContext)
|
||||
|
||||
/**
|
||||
* 在特定的 CoroutineScope 下构造 Bot 实例
|
||||
* 该 Bot 实例的生命周期将会跟随这个 CoroutineScope.
|
||||
* 这个 CoroutineScope 也会等待 Bot 的结束才会结束.
|
||||
*
|
||||
* ```kotlin
|
||||
* fun myProcess(){
|
||||
* TIMPC.run {
|
||||
* GlobalScope.Bot(account, logger)
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
override fun CoroutineScope.Bot(qq: Long, password: String, logger: MiraiLogger?): Bot =
|
||||
TIMPCBot(BotAccount(qq, password), logger, coroutineContext)
|
||||
|
||||
/**
|
||||
* 在特定的 CoroutineScope 下构造 Bot 实例
|
||||
* 该 Bot 实例的生命周期将会跟随这个 CoroutineScope.
|
||||
* 这个 CoroutineScope 也会等待 Bot 的结束才会结束.
|
||||
*
|
||||
* ```kotlin
|
||||
* fun myProcess(){
|
||||
* TIMPC.run {
|
||||
* GlobalScope.Bot(account, logger)
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
override fun CoroutineScope.Bot(account: BotAccount, logger: MiraiLogger?): Bot =
|
||||
TIMPCBot(account, logger, coroutineContext)
|
||||
}
|
@ -0,0 +1,243 @@
|
||||
package net.mamoe.mirai.timpc
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.BotAccount
|
||||
import net.mamoe.mirai.BotImpl
|
||||
import net.mamoe.mirai.contact.*
|
||||
import net.mamoe.mirai.message.data.Image
|
||||
import net.mamoe.mirai.message.data.ImageId0x03
|
||||
import net.mamoe.mirai.message.data.ImageId0x06
|
||||
import net.mamoe.mirai.network.data.*
|
||||
import net.mamoe.mirai.qqAccount
|
||||
import net.mamoe.mirai.timpc.internal.RawGroupInfo
|
||||
import net.mamoe.mirai.timpc.network.GroupImpl
|
||||
import net.mamoe.mirai.timpc.network.MemberImpl
|
||||
import net.mamoe.mirai.timpc.network.QQImpl
|
||||
import net.mamoe.mirai.timpc.network.TIMBotNetworkHandler
|
||||
import net.mamoe.mirai.timpc.network.handler.TemporaryPacketHandler
|
||||
import net.mamoe.mirai.timpc.network.packet.KnownPacketId
|
||||
import net.mamoe.mirai.timpc.network.packet.OutgoingPacket
|
||||
import net.mamoe.mirai.timpc.network.packet.SessionKey
|
||||
import net.mamoe.mirai.timpc.network.packet.action.*
|
||||
import net.mamoe.mirai.timpc.utils.assertUnreachable
|
||||
import net.mamoe.mirai.utils.*
|
||||
import net.mamoe.mirai.utils.internal.coerceAtLeastOrFail
|
||||
import net.mamoe.mirai.utils.io.logStacktrace
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.jvm.JvmSynthetic
|
||||
|
||||
internal expect class TIMPCBot constructor(
|
||||
account: BotAccount,
|
||||
logger: MiraiLogger?,
|
||||
context: CoroutineContext
|
||||
) : TIMPCBotBase
|
||||
|
||||
@UseExperimental(MiraiInternalAPI::class)
|
||||
internal abstract class TIMPCBotBase constructor(
|
||||
account: BotAccount,
|
||||
logger: MiraiLogger?,
|
||||
context: CoroutineContext
|
||||
) : BotImpl<TIMBotNetworkHandler>(account, logger ?: DefaultLogger("Bot(" + account.id + ")"), context) {
|
||||
companion object {
|
||||
init {
|
||||
KnownPacketId.values() /* load id classes */
|
||||
}
|
||||
}
|
||||
|
||||
final override val network: TIMBotNetworkHandler get() = _network
|
||||
|
||||
inline val sessionKey: SessionKey get() = network.sessionKey
|
||||
|
||||
private lateinit var _network: TIMBotNetworkHandler
|
||||
|
||||
override suspend fun login(configuration: BotConfiguration) =
|
||||
reinitializeNetworkHandler(configuration, null)
|
||||
|
||||
// shouldn't be suspend!! This function MUST NOT inherit the context from the caller because the caller(NetworkHandler) is going to close
|
||||
internal fun tryReinitializeNetworkHandler(
|
||||
configuration: BotConfiguration,
|
||||
cause: Throwable?
|
||||
): Job = launch {
|
||||
repeat(configuration.reconnectionRetryTimes) {
|
||||
try {
|
||||
reinitializeNetworkHandler(configuration, cause)
|
||||
logger.info("Reconnected successfully")
|
||||
return@launch
|
||||
} catch (e: LoginFailedException){
|
||||
delay(configuration.reconnectPeriodMillis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun reinitializeNetworkHandler(
|
||||
configuration: BotConfiguration,
|
||||
cause: Throwable?
|
||||
) {
|
||||
logger.info("BotAccount: $qqAccount")
|
||||
logger.info("Initializing BotNetworkHandler")
|
||||
try {
|
||||
if (::_network.isInitialized) {
|
||||
_network.close(cause)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("Cannot close network handler", e)
|
||||
}
|
||||
_network = TIMBotNetworkHandler(this.coroutineContext + configuration, this as TIMPCBot)
|
||||
|
||||
_network.login()
|
||||
}
|
||||
|
||||
final override suspend fun addFriend(id: Long, message: String?, remark: String?): AddFriendResult {
|
||||
return when (CanAddFriendPacket(qqAccount, id, sessionKey).sendAndExpect<CanAddFriendResponse>()) {
|
||||
is CanAddFriendResponse.AlreadyAdded -> AddFriendResult.ALREADY_ADDED
|
||||
is CanAddFriendResponse.Rejected -> AddFriendResult.REJECTED
|
||||
|
||||
is CanAddFriendResponse.ReadyToAdd,
|
||||
is CanAddFriendResponse.RequireVerification -> {
|
||||
val key = RequestFriendAdditionKeyPacket(qqAccount, id, sessionKey).sendAndExpect<RequestFriendAdditionKeyPacket.Response>().key
|
||||
AddFriendPacket.RequestAdd(qqAccount, id, sessionKey, message, remark, key).sendAndExpect<AddFriendPacket.Response>()
|
||||
AddFriendResult.WAITING_FOR_APPROVE
|
||||
} //这个做的是需要验证消息的情况, 不确定 ReadyToAdd 的是啥
|
||||
|
||||
// 似乎 RequireVerification 和 ReadyToAdd 判断错了. 需要重新检查一下
|
||||
|
||||
// TODO: 2019/11/11 需要验证问题的情况
|
||||
|
||||
/*is CanAddFriendResponse.ReadyToAdd -> {
|
||||
// TODO: 2019/11/11 这不需要验证信息的情况
|
||||
|
||||
//AddFriendPacket(qqAccount, id, sessionKey, ).sendAndExpectAsync<AddFriendPacket.Response>().await()
|
||||
TODO()
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
final override suspend fun approveFriendAddRequest(id: Long, remark: String?) {
|
||||
AddFriendPacket.Approve(qqAccount, sessionKey, 0, id, remark).sendAndExpect<AddFriendPacket.Response>()
|
||||
}
|
||||
|
||||
|
||||
// region contacts
|
||||
|
||||
final override val groups: ContactList<Group> = ContactList(LockFreeLinkedList())
|
||||
final override val qqs: ContactList<QQ> = ContactList(LockFreeLinkedList())
|
||||
|
||||
/**
|
||||
* 线程安全地获取缓存的 QQ 对象. 若没有对应的缓存, 则会创建一个.
|
||||
*/
|
||||
@UseExperimental(MiraiInternalAPI::class)
|
||||
@JvmSynthetic
|
||||
final override fun getQQ(id: Long): QQ = qqs.delegate.getOrAdd(id) { QQ(id) }
|
||||
|
||||
@UseExperimental(MiraiInternalAPI::class, ExperimentalUnsignedTypes::class)
|
||||
final override suspend fun getGroup(id: GroupId): Group = groups.delegate.getOrNull(id.value) ?: inline {
|
||||
val info: RawGroupInfo = try {
|
||||
when (val response =
|
||||
GroupPacket.QueryGroupInfo(qqAccount, id.toInternalId(), sessionKey).sendAndExpect<GroupPacket.InfoResponse>()) {
|
||||
is RawGroupInfo -> response
|
||||
is GroupNotFound -> throw GroupNotFoundException("id=${id.value}")
|
||||
else -> assertUnreachable()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw IllegalStateException("Cannot obtain group info for id ${id.value}", e)
|
||||
}
|
||||
|
||||
return groups.delegate.getOrAdd(id.value) { Group(id, info) }
|
||||
}
|
||||
|
||||
final override suspend fun getGroup(internalId: GroupInternalId): Group =
|
||||
getGroup0(internalId.toId().value)
|
||||
|
||||
private suspend inline fun getGroup0(id: Long): Group =
|
||||
groups.delegate.getOrNull(id) ?: inline {
|
||||
val info: RawGroupInfo = try {
|
||||
GroupPacket.QueryGroupInfo(qqAccount, GroupId(id).toInternalId(), sessionKey).sendAndExpect()
|
||||
} catch (e: Exception) {
|
||||
e.logStacktrace()
|
||||
error("Cannot obtain group info for id $id")
|
||||
}
|
||||
|
||||
return groups.delegate.getOrAdd(id) { Group(GroupId(id), info) }
|
||||
}
|
||||
|
||||
@UseExperimental(MiraiInternalAPI::class)
|
||||
final override suspend fun getGroup(id: Long): Group = getGroup0(id.coerceAtLeastOrFail(0))
|
||||
|
||||
|
||||
internal suspend inline fun <reified P : Packet, R> OutgoingPacket.sendAndExpectAsync(
|
||||
checkSequence: Boolean = true,
|
||||
noinline handler: suspend (P) -> R
|
||||
): Deferred<R> {
|
||||
val deferred: CompletableDeferred<R> = CompletableDeferred(coroutineContext[Job])
|
||||
network.temporaryPacketHandlers.addLast(
|
||||
TemporaryPacketHandler(
|
||||
expectationClass = P::class,
|
||||
deferred = deferred,
|
||||
checkSequence = if (checkSequence) this.sequenceId else null,
|
||||
callerContext = coroutineContext + deferred,
|
||||
handler = handler
|
||||
)
|
||||
)
|
||||
network.socket.sendPacket(this)
|
||||
return deferred
|
||||
}
|
||||
|
||||
internal suspend inline fun <reified P : Packet> OutgoingPacket.sendAndExpectAsync(checkSequence: Boolean = true): Deferred<P> =
|
||||
sendAndExpectAsync<P, P>(checkSequence) { it }
|
||||
|
||||
internal suspend inline fun <reified P : Packet, R> OutgoingPacket.sendAndExpect(
|
||||
checkSequence: Boolean = true,
|
||||
timeoutMillis: Long = 5.secondsToMillis,
|
||||
crossinline mapper: (P) -> R
|
||||
): R = withTimeout(timeoutMillis) { sendAndExpectAsync<P, R>(checkSequence) { mapper(it) }.await() }
|
||||
|
||||
internal suspend inline fun <reified P : Packet> OutgoingPacket.sendAndExpect(
|
||||
checkSequence: Boolean = true,
|
||||
timeoutMillist: Long = 5.secondsToMillis
|
||||
): P = withTimeout(timeoutMillist) { sendAndExpectAsync<P, P>(checkSequence) { it }.await() }
|
||||
|
||||
internal suspend inline fun OutgoingPacket.send() = network.socket.sendPacket(this)
|
||||
|
||||
|
||||
final override suspend fun Image.getLink(): ImageLink = when (val id = this.id) {
|
||||
is ImageId0x03 -> GroupImagePacket.RequestImageLink(qqAccount, sessionKey, id).sendAndExpect<GroupImageLink>().requireSuccess()
|
||||
is ImageId0x06 -> FriendImagePacket.RequestImageLink(qqAccount, sessionKey, id).sendAndExpect<FriendImageLink>()
|
||||
else -> assertUnreachable()
|
||||
}
|
||||
|
||||
@Suppress("FunctionName")
|
||||
@PublishedApi
|
||||
internal fun CoroutineScope.Group(groupId: GroupId, info: RawGroupInfo): Group {
|
||||
return GroupImpl(this@TIMPCBotBase as TIMPCBot, groupId, coroutineContext).apply { this.info = info.parseBy(this); launch { startUpdater() } }
|
||||
}
|
||||
|
||||
@Suppress("FunctionName")
|
||||
@PublishedApi
|
||||
internal fun Group.Member(qq: QQ, permission: MemberPermission): Member {
|
||||
return MemberImpl(qq, this, permission, coroutineContext).apply { launch { startUpdater() } }
|
||||
}
|
||||
|
||||
@Suppress("FunctionName")
|
||||
internal fun CoroutineScope.QQ(id: Long): QQ =
|
||||
QQImpl(this@TIMPCBotBase as TIMPCBot, id, coroutineContext).apply { launch { startUpdater() } }
|
||||
}
|
||||
|
||||
internal inline fun <R> inline(block: () -> R): R = block()
|
||||
|
||||
internal suspend fun TIMPCBot.sendPacket(toSend: OutgoingPacket) = this.network.socket.sendPacket(toSend)
|
||||
|
||||
/**
|
||||
* 以 [Bot] 作为接收器 (receiver) 并调用 [block], 返回 [block] 的返回值.
|
||||
* 这个方法将能帮助使用在 [Bot] 中定义的一些扩展方法
|
||||
*/
|
||||
@UseExperimental(ExperimentalContracts::class)
|
||||
internal inline fun <R> Contact.withTIMPCBot(block: TIMPCBot.() -> R): R {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
return (bot as TIMPCBot).run(block)
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package net.mamoe.mirai.timpc.internal
|
||||
|
||||
import net.mamoe.mirai.contact.*
|
||||
import net.mamoe.mirai.network.data.GroupInfo
|
||||
import net.mamoe.mirai.timpc.TIMPCBot
|
||||
import net.mamoe.mirai.timpc.network.packet.action.GroupPacket
|
||||
import net.mamoe.mirai.utils.LockFreeLinkedList
|
||||
import net.mamoe.mirai.utils.MiraiInternalAPI
|
||||
|
||||
data class RawGroupInfo(
|
||||
val group: Long,
|
||||
val owner: Long,
|
||||
val name: String,
|
||||
val announcement: String,
|
||||
/**
|
||||
* 含群主
|
||||
*/
|
||||
val members: Map<Long, MemberPermission>
|
||||
) : GroupPacket.InfoResponse {
|
||||
|
||||
@UseExperimental(MiraiInternalAPI::class)
|
||||
fun parseBy(group: Group): GroupInfo = group.withBot {
|
||||
this as? TIMPCBot ?: error("internal error: wrong Bot instance passed")
|
||||
|
||||
val memberList = LockFreeLinkedList<Member>()
|
||||
members.forEach { entry: Map.Entry<Long, MemberPermission> ->
|
||||
memberList.addLast(entry.key.qq().let { group.Member(it, entry.value) })
|
||||
}
|
||||
return GroupInfo(
|
||||
group,
|
||||
this@RawGroupInfo.owner.qq().let { group.Member(it, MemberPermission.OWNER) },
|
||||
this@RawGroupInfo.name,
|
||||
this@RawGroupInfo.announcement,
|
||||
ContactList(memberList)
|
||||
)
|
||||
}
|
||||
}
|
@ -1,24 +1,29 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "unused", "MemberVisibilityCanBePrivate", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.contact.internal
|
||||
package net.mamoe.mirai.timpc.network
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import net.mamoe.mirai.Bot
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.mamoe.mirai.contact.*
|
||||
import net.mamoe.mirai.contact.data.Profile
|
||||
import net.mamoe.mirai.event.subscribeAlways
|
||||
import net.mamoe.mirai.message.MessageChain
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.action.*
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.event.MemberJoinEventPacket
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.event.MemberQuitEvent
|
||||
import net.mamoe.mirai.network.qqAccount
|
||||
import net.mamoe.mirai.network.sessionKey
|
||||
import net.mamoe.mirai.message.data.ImageId
|
||||
import net.mamoe.mirai.message.data.MessageChain
|
||||
import net.mamoe.mirai.network.data.FriendNameRemark
|
||||
import net.mamoe.mirai.network.data.GroupInfo
|
||||
import net.mamoe.mirai.network.data.PreviousNameList
|
||||
import net.mamoe.mirai.network.data.Profile
|
||||
import net.mamoe.mirai.qqAccount
|
||||
import net.mamoe.mirai.sendPacket
|
||||
import net.mamoe.mirai.utils.MiraiInternalAPI
|
||||
import net.mamoe.mirai.withSession
|
||||
import net.mamoe.mirai.timpc.TIMPCBot
|
||||
import net.mamoe.mirai.timpc.internal.RawGroupInfo
|
||||
import net.mamoe.mirai.timpc.network.packet.action.*
|
||||
import net.mamoe.mirai.timpc.network.packet.event.MemberJoinEventPacket
|
||||
import net.mamoe.mirai.timpc.network.packet.event.MemberQuitEvent
|
||||
import net.mamoe.mirai.timpc.sendPacket
|
||||
import net.mamoe.mirai.timpc.utils.assertUnreachable
|
||||
import net.mamoe.mirai.timpc.withTIMPCBot
|
||||
import net.mamoe.mirai.utils.*
|
||||
import net.mamoe.mirai.utils.io.toUHexString
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
internal sealed class ContactImpl : Contact {
|
||||
@ -30,22 +35,12 @@ internal sealed class ContactImpl : Contact {
|
||||
internal abstract suspend fun startUpdater()
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造 [Group]
|
||||
*/
|
||||
@Suppress("FunctionName")
|
||||
@PublishedApi
|
||||
internal fun CoroutineScope.Group(bot: Bot, groupId: GroupId, info: RawGroupInfo, context: CoroutineContext): Group =
|
||||
GroupImpl(bot, groupId, context).apply {
|
||||
this@apply.info = info.parseBy(this@apply)
|
||||
launch { startUpdater() }
|
||||
}
|
||||
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate", "CanBeParameter")
|
||||
internal data class GroupImpl internal constructor(override val bot: Bot, val groupId: GroupId, override val coroutineContext: CoroutineContext) :
|
||||
internal class GroupImpl internal constructor(bot: TIMPCBot, val groupId: GroupId, override val coroutineContext: CoroutineContext) :
|
||||
ContactImpl(), Group, CoroutineScope {
|
||||
override val id: UInt get() = groupId.value
|
||||
override val bot: TIMPCBot by bot.unsafeWeakRef()
|
||||
|
||||
override val id: Long get() = groupId.value
|
||||
override val internalId = GroupId(id).toInternalId()
|
||||
|
||||
internal lateinit var info: GroupInfo
|
||||
@ -56,19 +51,45 @@ internal data class GroupImpl internal constructor(override val bot: Bot, val gr
|
||||
override val announcement: String get() = info.announcement
|
||||
override val members: ContactList<Member> get() = info.members
|
||||
|
||||
override fun getMember(id: UInt): Member =
|
||||
members.getOrNull(id) ?: throw NoSuchElementException("No such member whose id is ${id.toLong()} in group ${groupId.value.toLong()}")
|
||||
override fun getMember(id: Long): Member =
|
||||
members.getOrNull(id) ?: throw NoSuchElementException("No such member whose id is $id in group ${groupId.value}")
|
||||
|
||||
override suspend fun sendMessage(message: MessageChain) {
|
||||
bot.sendPacket(GroupPacket.Message(bot.qqAccount, internalId, bot.sessionKey, message))
|
||||
}
|
||||
|
||||
override suspend fun updateGroupInfo(): GroupInfo = bot.withSession {
|
||||
override suspend fun uploadImage(image: ExternalImage): ImageId = withTIMPCBot {
|
||||
val userContext = coroutineContext
|
||||
val response = GroupImagePacket.RequestImageId(bot.qqAccount, internalId, image, sessionKey).sendAndExpect<GroupImageResponse>()
|
||||
|
||||
withContext(userContext) {
|
||||
when (response) {
|
||||
is ImageUploadInfo -> response.uKey?.let {
|
||||
Http.postImage(
|
||||
htcmd = "0x6ff0071",
|
||||
uin = bot.qqAccount,
|
||||
groupId = GroupId(id),
|
||||
imageInput = image.input,
|
||||
inputSize = image.inputSize,
|
||||
uKeyHex = it.toUHexString("")
|
||||
)
|
||||
} // if null: image already exists
|
||||
|
||||
// TODO: 2019/11/17 超过大小的情况
|
||||
//is Overfile -> throw OverFileSizeMaxException()
|
||||
else -> assertUnreachable()
|
||||
}
|
||||
}
|
||||
|
||||
return image.groupImageId
|
||||
}
|
||||
|
||||
override suspend fun updateGroupInfo(): GroupInfo = withTIMPCBot {
|
||||
GroupPacket.QueryGroupInfo(qqAccount, internalId, sessionKey).sendAndExpect<RawGroupInfo>().parseBy(this@GroupImpl).also { info = it }
|
||||
}
|
||||
|
||||
override suspend fun quit(): QuitGroupResponse = bot.withSession {
|
||||
GroupPacket.QuitGroup(qqAccount, sessionKey, internalId).sendAndExpect()
|
||||
override suspend fun quit(): Boolean = withTIMPCBot {
|
||||
GroupPacket.QuitGroup(qqAccount, sessionKey, internalId).sendAndExpect<GroupPacket.QuitGroupResponse>().isSuccess
|
||||
}
|
||||
|
||||
@UseExperimental(MiraiInternalAPI::class)
|
||||
@ -84,25 +105,45 @@ internal data class GroupImpl internal constructor(override val bot: Bot, val gr
|
||||
override fun toString(): String = "Group(${this.id})"
|
||||
}
|
||||
|
||||
@Suppress("FunctionName", "NOTHING_TO_INLINE")
|
||||
internal inline fun CoroutineScope.QQ(bot: Bot, id: UInt, coroutineContext: CoroutineContext): QQ = QQImpl(bot, id, coroutineContext).apply { launch { startUpdater() } }
|
||||
|
||||
@PublishedApi
|
||||
internal data class QQImpl @PublishedApi internal constructor(override val bot: Bot, override val id: UInt, override val coroutineContext: CoroutineContext) :
|
||||
internal class QQImpl @PublishedApi internal constructor(bot: TIMPCBot, override val id: Long, override val coroutineContext: CoroutineContext) :
|
||||
ContactImpl(),
|
||||
QQ, CoroutineScope {
|
||||
override val bot: TIMPCBot by bot.unsafeWeakRef()
|
||||
|
||||
override suspend fun sendMessage(message: MessageChain) =
|
||||
bot.sendPacket(SendFriendMessagePacket(bot.qqAccount, id, bot.sessionKey, message))
|
||||
|
||||
override suspend fun queryProfile(): Profile = bot.withSession {
|
||||
override suspend fun uploadImage(image: ExternalImage): ImageId = withTIMPCBot {
|
||||
FriendImagePacket.RequestImageId(qqAccount, sessionKey, id, image).sendAndExpect<FriendImageResponse>().let {
|
||||
when (it) {
|
||||
is FriendImageUKey -> {
|
||||
Http.postImage(
|
||||
htcmd = "0x6ff0070",
|
||||
uin = bot.qqAccount,
|
||||
groupId = null,
|
||||
uKeyHex = it.uKey.toUHexString(""),
|
||||
imageInput = image.input,
|
||||
inputSize = image.inputSize
|
||||
)
|
||||
it.imageId
|
||||
}
|
||||
is FriendImageAlreadyExists -> it.imageId
|
||||
is FriendImageOverFileSizeMax -> throw OverFileSizeMaxException()
|
||||
else -> error("This shouldn't happen")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun queryProfile(): Profile = withTIMPCBot {
|
||||
RequestProfileDetailsPacket(bot.qqAccount, id, sessionKey).sendAndExpect<RequestProfileDetailsResponse>().profile
|
||||
}
|
||||
|
||||
override suspend fun queryPreviousNameList(): PreviousNameList = bot.withSession {
|
||||
override suspend fun queryPreviousNameList(): PreviousNameList = withTIMPCBot {
|
||||
QueryPreviousNamePacket(bot.qqAccount, sessionKey, id).sendAndExpect()
|
||||
}
|
||||
|
||||
override suspend fun queryRemark(): FriendNameRemark = bot.withSession {
|
||||
override suspend fun queryRemark(): FriendNameRemark = withTIMPCBot {
|
||||
QueryFriendRemarkPacket(bot.qqAccount, sessionKey, id).sendAndExpect()
|
||||
}
|
||||
|
||||
@ -114,10 +155,6 @@ internal data class QQImpl @PublishedApi internal constructor(override val bot:
|
||||
override fun toString(): String = "QQ(${this.id})"
|
||||
}
|
||||
|
||||
@Suppress("FunctionName", "NOTHING_TO_INLINE")
|
||||
internal inline fun Group.Member(delegate: QQ, permission: MemberPermission, coroutineContext: CoroutineContext): Member =
|
||||
MemberImpl(delegate, this, permission, coroutineContext).apply { launch { startUpdater() } }
|
||||
|
||||
/**
|
||||
* 群成员
|
||||
*/
|
||||
@ -130,13 +167,13 @@ internal data class MemberImpl(
|
||||
) : QQ by delegate, CoroutineScope, Member, ContactImpl() {
|
||||
override fun toString(): String = "Member(id=${this.id}, group=${group.id}, permission=$permission)"
|
||||
|
||||
override suspend fun mute(durationSeconds: Int): Boolean = bot.withSession {
|
||||
override suspend fun mute(durationSeconds: Int): Boolean = withTIMPCBot {
|
||||
require(durationSeconds > 0) { "duration must be greater than 0 second" }
|
||||
require(durationSeconds <= 30 * 24 * 3600) { "duration must be no more than 30 days" }
|
||||
|
||||
if (permission == MemberPermission.OWNER) return false
|
||||
val operator = group.getMember(bot.qqAccount)
|
||||
check(operator.id != id) { "The bot is the owner of group ${group.id.toLong()}, it cannot mute itself!" }
|
||||
check(operator.id != id) { "The bot is the owner of group ${group.id}, it cannot mute itself!" }
|
||||
when (operator.permission) {
|
||||
MemberPermission.MEMBER -> return false
|
||||
MemberPermission.ADMINISTRATOR -> if (permission == MemberPermission.ADMINISTRATOR) return false
|
||||
@ -153,7 +190,7 @@ internal data class MemberImpl(
|
||||
// TODO: 2019/12/6 更新群成员信息
|
||||
}
|
||||
|
||||
override suspend fun unmute(): Unit = bot.withSession {
|
||||
override suspend fun unmute(): Unit = withTIMPCBot {
|
||||
GroupPacket.Mute(qqAccount, group.internalId, sessionKey, id, 0u).sendAndExpect<GroupPacket.MuteResponse>()
|
||||
}
|
||||
}
|
@ -1,30 +1,28 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc
|
||||
package net.mamoe.mirai.timpc.network
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.io.core.*
|
||||
import kotlinx.io.pool.useInstance
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.event.*
|
||||
import net.mamoe.mirai.event.events.BeforePacketSendEvent
|
||||
import net.mamoe.mirai.event.BroadcastControllable
|
||||
import net.mamoe.mirai.event.Cancellable
|
||||
import net.mamoe.mirai.event.Subscribable
|
||||
import net.mamoe.mirai.event.broadcast
|
||||
import net.mamoe.mirai.event.events.BotLoginSucceedEvent
|
||||
import net.mamoe.mirai.event.events.PacketSentEvent
|
||||
import net.mamoe.mirai.event.events.ServerPacketReceivedEvent
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.BotSession
|
||||
import net.mamoe.mirai.network.protocol.timpc.handler.DataPacketSocketAdapter
|
||||
import net.mamoe.mirai.network.protocol.timpc.handler.TemporaryPacketHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.*
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.login.*
|
||||
import net.mamoe.mirai.network.data.LoginResult
|
||||
import net.mamoe.mirai.network.data.Packet
|
||||
import net.mamoe.mirai.qqAccount
|
||||
import net.mamoe.mirai.utils.OnlineStatus
|
||||
import net.mamoe.mirai.utils.currentBotConfiguration
|
||||
import net.mamoe.mirai.timpc.TIMPCBot
|
||||
import net.mamoe.mirai.timpc.network.handler.DataPacketSocketAdapter
|
||||
import net.mamoe.mirai.timpc.network.handler.TemporaryPacketHandler
|
||||
import net.mamoe.mirai.timpc.network.packet.*
|
||||
import net.mamoe.mirai.timpc.network.packet.login.*
|
||||
import net.mamoe.mirai.utils.*
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.properties.Delegates
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
@ -39,9 +37,9 @@ internal expect val NetworkDispatcher: CoroutineDispatcher
|
||||
*
|
||||
* @see BotNetworkHandler
|
||||
*/
|
||||
internal class TIMBotNetworkHandler internal constructor(coroutineContext: CoroutineContext, override val bot: Bot) :
|
||||
BotNetworkHandler<TIMBotNetworkHandler.BotSocketAdapter>, CoroutineScope {
|
||||
|
||||
internal class TIMBotNetworkHandler internal constructor(coroutineContext: CoroutineContext, bot: TIMPCBot) :
|
||||
BotNetworkHandler(), CoroutineScope {
|
||||
override val bot: TIMPCBot by bot.unsafeWeakRef()
|
||||
override val supervisor: CompletableJob = SupervisorJob(coroutineContext[Job])
|
||||
|
||||
override val coroutineContext: CoroutineContext =
|
||||
@ -50,52 +48,43 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
|
||||
?: "an unnamed coroutine"} under TIMBotNetworkHandler", e)
|
||||
} + supervisor
|
||||
|
||||
override lateinit var socket: BotSocketAdapter
|
||||
lateinit var socket: BotSocketAdapter
|
||||
private set
|
||||
|
||||
private val temporaryPacketHandlers = mutableListOf<TemporaryPacketHandler<*, *>>()
|
||||
private val handlersLock = Mutex()
|
||||
internal val temporaryPacketHandlers = LockFreeLinkedList<TemporaryPacketHandler<*, *>>()
|
||||
|
||||
private var heartbeatJob: Job? = null
|
||||
|
||||
suspend fun addHandler(temporaryPacketHandler: TemporaryPacketHandler<*, *>) {
|
||||
handlersLock.withLock {
|
||||
temporaryPacketHandlers.add(temporaryPacketHandler)
|
||||
}
|
||||
temporaryPacketHandler.send(this.session)
|
||||
}
|
||||
override suspend fun login() {
|
||||
|
||||
override suspend fun login(): LoginResult {
|
||||
return withContext(this.coroutineContext) {
|
||||
TIMProtocol.SERVER_IP.sortedBy { Random.nextInt() }.forEach { ip ->
|
||||
bot.logger.info("Connecting server $ip")
|
||||
try {
|
||||
withTimeout(3000) {
|
||||
socket = BotSocketAdapter(ip)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return@withContext LoginResult.NETWORK_UNAVAILABLE
|
||||
TIMProtocol.SERVER_IP.sortedBy { Random.nextInt() }.forEach { ip ->
|
||||
bot.logger.info("Connecting server $ip")
|
||||
try {
|
||||
withTimeout(3000) {
|
||||
socket = BotSocketAdapter(ip)
|
||||
}
|
||||
|
||||
loginResult = CompletableDeferred()
|
||||
|
||||
socket.resendTouch().takeIf { it != LoginResult.TIMEOUT }?.let { return@withContext it }
|
||||
|
||||
bot.logger.warning("Timeout. Retrying next server")
|
||||
|
||||
socket.close()
|
||||
} catch (e: Exception) {
|
||||
throw LoginFailedException(LoginResult.NETWORK_UNAVAILABLE)
|
||||
}
|
||||
return@withContext LoginResult.TIMEOUT
|
||||
|
||||
loginResult = CompletableDeferred()
|
||||
|
||||
val result = socket.resendTouch() ?: return // success
|
||||
result.takeIf { it != LoginResult.TIMEOUT }?.let { throw LoginFailedException(it) }
|
||||
|
||||
bot.logger.warning("Timeout. Retrying next server")
|
||||
|
||||
socket.close()
|
||||
}
|
||||
throw LoginFailedException(LoginResult.TIMEOUT)
|
||||
}
|
||||
|
||||
internal var loginResult: CompletableDeferred<LoginResult> = CompletableDeferred()
|
||||
|
||||
override lateinit var session: BotSession
|
||||
internal var loginResult: CompletableDeferred<LoginResult?> = CompletableDeferred()
|
||||
|
||||
//private | internal
|
||||
|
||||
private var sessionKey: SessionKey by Delegates.notNull()
|
||||
private var _sessionKey: SessionKey? = null
|
||||
internal val sessionKey: SessionKey get() = _sessionKey ?: error("sessionKey is not yet initialized")
|
||||
|
||||
override suspend fun awaitDisconnection() {
|
||||
heartbeatJob?.join()
|
||||
@ -187,43 +176,38 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun resendTouch(): LoginResult /* = coroutineScope */ {
|
||||
internal suspend fun resendTouch(): LoginResult? /* = coroutineScope */ {
|
||||
loginHandler?.close()
|
||||
|
||||
loginHandler = LoginHandler()
|
||||
|
||||
|
||||
val expect = expectPacket<TouchPacket.TouchResponse>()
|
||||
launch { processReceive() }
|
||||
launch {
|
||||
if (withTimeoutOrNull(currentBotConfiguration().touchTimeoutMillis) { expect.join() } == null) {
|
||||
loginResult.complete(LoginResult.TIMEOUT)
|
||||
expectingTouchResponse = Job(supervisor)
|
||||
try {
|
||||
launch { processReceive() }
|
||||
launch {
|
||||
if (withTimeoutOrNull(currentBotConfiguration().touchTimeoutMillis) { expectingTouchResponse!!.join() } == null) {
|
||||
loginResult.complete(LoginResult.TIMEOUT)
|
||||
}
|
||||
}
|
||||
}
|
||||
sendPacket(TouchPacket(bot.qqAccount, serverIp, false))
|
||||
sendPacket(TouchPacket(bot.qqAccount, serverIp, false))
|
||||
|
||||
return loginResult.await()
|
||||
return loginResult.await()
|
||||
} finally {
|
||||
expectingTouchResponse = null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun <reified P : Packet> expectPacket(): CompletableDeferred<P> {
|
||||
val receiving = CompletableDeferred<P>(coroutineContext[Job])
|
||||
subscribe<ServerPacketReceivedEvent<*>> {
|
||||
if (it.packet is P && it.bot === bot) {
|
||||
receiving.complete(it.packet)
|
||||
ListeningStatus.STOPPED
|
||||
} else
|
||||
ListeningStatus.LISTENING
|
||||
}
|
||||
return receiving
|
||||
}
|
||||
private var expectingTouchResponse: CompletableJob? = null
|
||||
|
||||
private suspend fun <TPacket : Packet> handlePacket0(
|
||||
sequenceId: UShort,
|
||||
packet: TPacket,
|
||||
factory: PacketFactory<TPacket, *>
|
||||
) {
|
||||
if (ServerPacketReceivedEvent(bot, packet).broadcast().cancelled)
|
||||
return
|
||||
if (packet is TouchPacket.TouchResponse) {
|
||||
expectingTouchResponse?.complete()
|
||||
}
|
||||
|
||||
if (!packet::class.annotations.filterIsInstance<NoLog>().any()) {
|
||||
if ((packet as? BroadcastControllable)?.shouldBroadcast != false) {
|
||||
@ -236,12 +220,10 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
|
||||
is Subscribable -> if ((packet as? BroadcastControllable)?.shouldBroadcast != false) packet.broadcast()
|
||||
}
|
||||
|
||||
// Remove first to release the lock
|
||||
handlersLock.withLock {
|
||||
temporaryPacketHandlers.filter { it.filter(session, packet, sequenceId) }
|
||||
.also { temporaryPacketHandlers.removeAll(it) }
|
||||
}.forEach {
|
||||
it.doReceiveCatchingExceptions(packet)
|
||||
temporaryPacketHandlers.forEach {
|
||||
if (it.filter(packet, sequenceId) && temporaryPacketHandlers.remove(it)) {
|
||||
it.doReceivePassingExceptionsToDeferred(packet)
|
||||
}
|
||||
}
|
||||
|
||||
if (factory is SessionPacketFactory<*>) {
|
||||
@ -256,10 +238,6 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
|
||||
internal suspend fun sendPacket(packet: OutgoingPacket): Unit = withContext(coroutineContext + CoroutineName("sendPacket")) {
|
||||
check(channel.isOpen) { "channel is not open" }
|
||||
|
||||
if (BeforePacketSendEvent(bot, packet).broadcast().cancelled) {
|
||||
return@withContext
|
||||
}
|
||||
|
||||
packet.delegate.use { built ->
|
||||
val buffer = IoBuffer.Pool.borrow()
|
||||
try {
|
||||
@ -291,8 +269,6 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
|
||||
bot.logger.verbose("Packet sent: ${it.name}")
|
||||
}
|
||||
|
||||
PacketSentEvent(bot, packet).broadcast()
|
||||
|
||||
Unit
|
||||
}
|
||||
|
||||
@ -466,8 +442,8 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
|
||||
}
|
||||
|
||||
is RequestSessionPacket.SessionKeyResponse -> {
|
||||
sessionKey = packet.sessionKey
|
||||
bot.logger.info("sessionKey = ${sessionKey.value.toUHexString()}")
|
||||
_sessionKey = packet.sessionKey
|
||||
bot.logger.info("sessionKey = ${packet.sessionKey.value.toUHexString()}")
|
||||
|
||||
setOnlineStatus(OnlineStatus.ONLINE)//required
|
||||
}
|
||||
@ -475,14 +451,11 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
|
||||
is ChangeOnlineStatusPacket.ChangeOnlineStatusResponse -> {
|
||||
BotLoginSucceedEvent(bot).broadcast()
|
||||
|
||||
|
||||
session = BotSession(sessionKey)
|
||||
|
||||
val configuration = currentBotConfiguration()
|
||||
heartbeatJob = this@TIMBotNetworkHandler.launch {
|
||||
while (socket.isOpen) {
|
||||
delay(configuration.heartbeatPeriodMillis)
|
||||
with(session) {
|
||||
with(bot) {
|
||||
class HeartbeatTimeoutException : CancellationException("heartbeat timeout")
|
||||
|
||||
if (withTimeoutOrNull(configuration.heartbeatTimeoutMillis) {
|
||||
@ -500,7 +473,7 @@ internal class TIMBotNetworkHandler internal constructor(coroutineContext: Corou
|
||||
}
|
||||
|
||||
bot.logger.info("Successfully logged in")
|
||||
loginResult.complete(LoginResult.SUCCESS)
|
||||
loginResult.complete(null)
|
||||
this.close()//The LoginHandler is useless since then
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc
|
||||
package net.mamoe.mirai.timpc.network
|
||||
|
||||
import net.mamoe.mirai.utils.io.hexToBytes
|
||||
import net.mamoe.mirai.utils.solveIpAddress
|
@ -1,14 +1,9 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.handler
|
||||
package net.mamoe.mirai.timpc.network.handler
|
||||
|
||||
import kotlinx.io.core.Closeable
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.event.events.ServerPacketReceivedEvent
|
||||
import net.mamoe.mirai.network.BotSession
|
||||
import net.mamoe.mirai.network.protocol.timpc.TIMBotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.OutgoingPacket
|
||||
import net.mamoe.mirai.utils.MiraiInternalAPI
|
||||
import net.mamoe.mirai.utils.io.PlatformDatagramChannel
|
||||
|
||||
/**
|
@ -0,0 +1,37 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.timpc.network.handler
|
||||
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.mamoe.mirai.network.data.Packet
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
internal class TemporaryPacketHandler<P : Packet, R>(
|
||||
private val expectationClass: KClass<P>,
|
||||
private val deferred: CompletableDeferred<R>,
|
||||
private val checkSequence: UShort? = null,
|
||||
/**
|
||||
* 调用者的 [CoroutineContext]. 包处理过程将会在这个 context 下运行
|
||||
*/
|
||||
private val callerContext: CoroutineContext,
|
||||
private val handler: suspend (P) -> R
|
||||
) {
|
||||
internal fun filter(packet: Packet, sequenceId: UShort): Boolean =
|
||||
expectationClass.isInstance(packet) && if (checkSequence != null) sequenceId == checkSequence else true
|
||||
|
||||
internal suspend inline fun doReceivePassingExceptionsToDeferred(packet: Packet) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val ret = try {
|
||||
withContext(callerContext) {
|
||||
handler(packet as P)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
deferred.completeExceptionally(e)
|
||||
return
|
||||
}
|
||||
deferred.complete(ret)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "unused")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet
|
||||
package net.mamoe.mirai.timpc.network.packet
|
||||
|
||||
/**
|
||||
* 包的最后一次修改时间, 和分析时使用的 TIM 版本
|
@ -1,8 +1,12 @@
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet
|
||||
package net.mamoe.mirai.timpc.network.packet
|
||||
|
||||
import kotlinx.io.core.BytePacketBuilder
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.IoBuffer
|
||||
import kotlinx.io.core.writeFully
|
||||
import net.mamoe.mirai.utils.decryptBy
|
||||
import net.mamoe.mirai.utils.encryptBy
|
||||
import net.mamoe.mirai.utils.io.encryptAndWrite
|
||||
|
||||
|
||||
/**
|
||||
@ -15,6 +19,7 @@ internal inline class SessionKey(override val value: ByteArray) : DecrypterByteA
|
||||
/**
|
||||
* [ByteArray] 解密器
|
||||
*/
|
||||
@PublishedApi
|
||||
internal interface DecrypterByteArray : Decrypter {
|
||||
val value: ByteArray
|
||||
override fun decrypt(input: ByteReadPacket): ByteReadPacket = input.decryptBy(value)
|
||||
@ -50,4 +55,8 @@ internal interface Decrypter {
|
||||
operator fun plus(another: Decrypter): Decrypter = LinkedDecrypter { another.decrypt(this.decrypt(it)) }
|
||||
}
|
||||
|
||||
internal interface DecrypterType<D : Decrypter>
|
||||
internal interface DecrypterType<D : Decrypter>
|
||||
|
||||
@PublishedApi
|
||||
internal inline fun BytePacketBuilder.encryptAndWrite(key: DecrypterByteArray, encoder: BytePacketBuilder.() -> Unit) =
|
||||
this.encryptAndWrite(key.value, encoder)
|
@ -1,13 +1,13 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS", "unused", "MemberVisibilityCanBePrivate")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet
|
||||
package net.mamoe.mirai.timpc.network.packet
|
||||
|
||||
import kotlinx.io.core.*
|
||||
import kotlinx.serialization.SerializationStrategy
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.TIMProtocol
|
||||
import net.mamoe.mirai.utils.io.encryptAndWrite
|
||||
import net.mamoe.mirai.network.data.Packet
|
||||
import net.mamoe.mirai.timpc.network.TIMProtocol
|
||||
import net.mamoe.mirai.utils.io.hexToBytes
|
||||
import net.mamoe.mirai.utils.io.writeQQ
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
@ -39,13 +39,11 @@ internal abstract class SessionPacketFactory<TPacket : Packet> : PacketFactory<T
|
||||
/**
|
||||
* 在 [BotNetworkHandler] 下处理这个包. 广播事件等.
|
||||
*/
|
||||
open suspend fun BotNetworkHandler<*>.handlePacket(packet: TPacket) {}
|
||||
open suspend fun BotNetworkHandler.handlePacket(packet: TPacket) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造一个待发送给服务器的数据包.
|
||||
*
|
||||
* 若不提供参数 [id], 则会通过注解 [AnnotatedId] 获取 id.
|
||||
*/
|
||||
@UseExperimental(ExperimentalContracts::class)
|
||||
@JvmOverloads
|
||||
@ -76,13 +74,11 @@ internal inline fun PacketFactory<*, *>.buildOutgoingPacket(
|
||||
|
||||
/**
|
||||
* 构造一个待发送给服务器的会话数据包.
|
||||
*
|
||||
* 若不提供参数 [id], 则会通过注解 [AnnotatedId] 获取 id.
|
||||
*/
|
||||
@UseExperimental(ExperimentalContracts::class)
|
||||
@JvmOverloads
|
||||
internal inline fun PacketFactory<*, *>.buildSessionPacket(
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
sessionKey: SessionKey,
|
||||
name: String? = null,
|
||||
id: PacketId = this.id,
|
||||
@ -105,13 +101,11 @@ internal inline fun PacketFactory<*, *>.buildSessionPacket(
|
||||
|
||||
/**
|
||||
* 构造一个待发送给服务器的会话数据包.
|
||||
*
|
||||
* 若不提供参数 [id], 则会通过注解 [AnnotatedId] 获取 id.
|
||||
*/
|
||||
@UseExperimental(ExperimentalContracts::class)
|
||||
@JvmOverloads
|
||||
internal fun <T> PacketFactory<*, *>.buildSessionProtoPacket(
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
sessionKey: SessionKey,
|
||||
name: String? = null,
|
||||
id: PacketId = this.id,
|
@ -1,14 +1,10 @@
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet
|
||||
package net.mamoe.mirai.timpc.network.packet
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.readBytes
|
||||
import net.mamoe.mirai.network.data.Packet
|
||||
import net.mamoe.mirai.utils.io.toUHexString
|
||||
|
||||
/**
|
||||
* 一个包的数据 (body)
|
||||
*/
|
||||
interface Packet
|
||||
|
||||
/**
|
||||
* 被忽略的数据包.
|
||||
*/
|
@ -1,6 +1,6 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet
|
||||
package net.mamoe.mirai.timpc.network.packet
|
||||
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
@ -10,6 +10,7 @@ import kotlinx.io.pool.useInstance
|
||||
import kotlinx.serialization.DeserializationStrategy
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.data.Packet
|
||||
import net.mamoe.mirai.utils.io.ByteArrayPool
|
||||
import net.mamoe.mirai.utils.io.debugPrint
|
||||
import net.mamoe.mirai.utils.io.read
|
||||
@ -36,7 +37,7 @@ internal abstract class PacketFactory<out TPacket : Packet, TDecrypter : Decrypt
|
||||
/**
|
||||
* **解码**服务器的回复数据包
|
||||
*/
|
||||
abstract suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): TPacket
|
||||
abstract suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): TPacket
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun <T> ByteReadPacket.decodeProtoPacket(
|
||||
@ -69,7 +70,7 @@ internal abstract class PacketFactory<out TPacket : Packet, TDecrypter : Decrypt
|
||||
}
|
||||
|
||||
internal object UnknownPacketFactory : SessionPacketFactory<UnknownPacket>() {
|
||||
override suspend fun BotNetworkHandler<*>.handlePacket(packet: UnknownPacket) {
|
||||
override suspend fun BotNetworkHandler.handlePacket(packet: UnknownPacket) {
|
||||
ByteArrayPool.useInstance {
|
||||
packet.body.readAvailable(it)
|
||||
bot.logger.debug("UnknownPacket(${packet.id.value.toUHexString()}) = " + it.toUHexString())
|
||||
@ -80,7 +81,7 @@ internal object UnknownPacketFactory : SessionPacketFactory<UnknownPacket>() {
|
||||
override suspend fun ByteReadPacket.decode(
|
||||
id: PacketId,
|
||||
sequenceId: UShort,
|
||||
handler: BotNetworkHandler<*>
|
||||
handler: BotNetworkHandler
|
||||
): UnknownPacket {
|
||||
return UnknownPacket(id, this)
|
||||
}
|
||||
@ -90,6 +91,6 @@ internal object IgnoredPacketFactory : SessionPacketFactory<IgnoredPacket>() {
|
||||
override suspend fun ByteReadPacket.decode(
|
||||
id: PacketId,
|
||||
sequenceId: UShort,
|
||||
handler: BotNetworkHandler<*>
|
||||
handler: BotNetworkHandler
|
||||
): IgnoredPacket = IgnoredPacket(id)
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet
|
||||
package net.mamoe.mirai.timpc.network.packet
|
||||
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.action.*
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.event.EventPacketFactory
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.event.FriendOnlineStatusChangedPacket
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.login.*
|
||||
import net.mamoe.mirai.timpc.network.packet.action.*
|
||||
import net.mamoe.mirai.timpc.network.packet.event.EventPacketFactory
|
||||
import net.mamoe.mirai.timpc.network.packet.event.FriendOnlineStatusChangedPacket
|
||||
import net.mamoe.mirai.timpc.network.packet.login.*
|
||||
import net.mamoe.mirai.utils.io.toUHexString
|
||||
|
||||
|
@ -1,15 +1,16 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.action
|
||||
package net.mamoe.mirai.timpc.network.packet.action
|
||||
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.contact.QQ
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.*
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.event.EventPacket
|
||||
import net.mamoe.mirai.network.data.EventPacket
|
||||
import net.mamoe.mirai.network.data.Packet
|
||||
import net.mamoe.mirai.network.data.PreviousNameList
|
||||
import net.mamoe.mirai.timpc.network.TIMProtocol
|
||||
import net.mamoe.mirai.timpc.network.packet.*
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
import net.mamoe.mirai.withSession
|
||||
|
||||
|
||||
/**
|
||||
@ -22,9 +23,9 @@ import net.mamoe.mirai.withSession
|
||||
@PacketVersion(date = "2019.11.11", timVersion = "2.3.2 (21173)")
|
||||
internal object QueryPreviousNamePacket : SessionPacketFactory<PreviousNameList>() {
|
||||
operator fun invoke(
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
sessionKey: SessionKey,
|
||||
target: UInt
|
||||
target: Long
|
||||
): OutgoingPacket = buildSessionPacket(bot, sessionKey) {
|
||||
writeZero(2)
|
||||
writeQQ(bot)
|
||||
@ -41,7 +42,7 @@ internal object QueryPreviousNamePacket : SessionPacketFactory<PreviousNameList>
|
||||
// [00 00 00 10] 68 69 6D 31 38 38 E7 9A 84 E5 B0 8F 64 69 63 6B
|
||||
// [00 00 00 0F] E4 B8 B6 E6 9A 97 E8 A3 94 E5 89 91 E9 AD 94
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): PreviousNameList {
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): PreviousNameList {
|
||||
// 00 00 00 01 00 00 00 0F E8 87 AA E5 8A A8 E9 A9 BE E9 A9 B6 31 2E 33
|
||||
|
||||
val count = readUInt().toInt()
|
||||
@ -54,19 +55,6 @@ internal object QueryPreviousNamePacket : SessionPacketFactory<PreviousNameList>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 曾用名列表
|
||||
*
|
||||
* 曾用名可能是:
|
||||
* - 昵称
|
||||
* - 共同群内的群名片
|
||||
*/
|
||||
class PreviousNameList(
|
||||
list: List<String>
|
||||
) : Packet, List<String> by list {
|
||||
override fun toString(): String = this.joinToString(prefix = "PreviousNameList(", postfix = ")", separator = ", ")
|
||||
}
|
||||
|
||||
// 需要验证消息
|
||||
// 0065 发送 03 07 57 37 E8
|
||||
// 0065 接受 03 07 57 37 E8 10 40 00 00 10 14 20 00 00 00 00 00 00 00 01 00 00 00 00 00
|
||||
@ -79,20 +67,21 @@ class PreviousNameList(
|
||||
@PacketVersion(date = "2019.11.11", timVersion = "2.3.2 (21173)")
|
||||
internal object CanAddFriendPacket : SessionPacketFactory<CanAddFriendResponse>() {
|
||||
operator fun invoke(
|
||||
bot: UInt,
|
||||
qq: UInt,
|
||||
bot: Long,
|
||||
qq: Long,
|
||||
sessionKey: SessionKey
|
||||
): OutgoingPacket = buildSessionPacket(bot, sessionKey) {
|
||||
writeQQ(qq)
|
||||
}
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): CanAddFriendResponse =
|
||||
handler.bot.withSession {
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): CanAddFriendResponse =
|
||||
with(handler.bot) {
|
||||
if (remaining > 20) {//todo check
|
||||
return CanAddFriendResponse.AlreadyAdded(readUInt().qq())
|
||||
return CanAddFriendResponse.AlreadyAdded(readQQ().qq())
|
||||
}
|
||||
val qq: QQ = readUInt().qq()
|
||||
val qq: QQ = readQQ().qq()
|
||||
|
||||
readUByteLVByteArray()
|
||||
// debugDiscardExact(1)
|
||||
|
||||
return when (val state = readUByte().toUInt()) {
|
||||
@ -148,7 +137,7 @@ internal sealed class CanAddFriendResponse : EventPacket {
|
||||
接受 03 00 00 00 00 01 30 5D 12 93 30 00 14 00 00 00 00 10 30 36 35 39 E4 B8 80 E7 BE 8E E5 A4 A9 E9 9D 99 02 0A 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 1E
|
||||
*/
|
||||
|
||||
inline class FriendAdditionKey(val value: IoBuffer)
|
||||
internal inline class FriendAdditionKey(val value: IoBuffer)
|
||||
|
||||
/**
|
||||
* 请求一个 32 位 Key, 在添加好友时发出
|
||||
@ -156,8 +145,8 @@ inline class FriendAdditionKey(val value: IoBuffer)
|
||||
@PacketVersion(date = "2019.11.11", timVersion = "2.3.2 (21173)")
|
||||
internal object RequestFriendAdditionKeyPacket : SessionPacketFactory<RequestFriendAdditionKeyPacket.Response>() {
|
||||
operator fun invoke(
|
||||
bot: UInt,
|
||||
qq: UInt,
|
||||
bot: Long,
|
||||
qq: Long,
|
||||
sessionKey: SessionKey
|
||||
) = buildSessionPacket(bot, sessionKey) {
|
||||
//01 00 01 02 B3 74 F6
|
||||
@ -165,7 +154,7 @@ internal object RequestFriendAdditionKeyPacket : SessionPacketFactory<RequestFri
|
||||
writeQQ(qq)
|
||||
}
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): Response {
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): Response {
|
||||
//01 00 01 00 00 20 01 C2 76 47 98 38 A1 FF AB 64 04 A9 81 1F CC 2B 2B A6 29 FC 97 80 A6 90 2D 26 C8 37 EE 1D 8A FA
|
||||
discardExact(4)
|
||||
return Response(FriendAdditionKey(readIoBuffer(readUShort().toInt())))
|
||||
@ -183,8 +172,8 @@ internal object AddFriendPacket : SessionPacketFactory<AddFriendPacket.Response>
|
||||
@PacketVersion(date = "2019.11.11", timVersion = "2.3.2 (21173)")
|
||||
@Suppress("FunctionName")
|
||||
fun RequestAdd(
|
||||
bot: UInt,
|
||||
qq: UInt,
|
||||
bot: Long,
|
||||
qq: Long,
|
||||
sessionKey: SessionKey,
|
||||
/**
|
||||
* 验证消息
|
||||
@ -250,13 +239,13 @@ internal object AddFriendPacket : SessionPacketFactory<AddFriendPacket.Response>
|
||||
@Suppress("FunctionName")
|
||||
@PacketVersion(date = "2019.11.20", timVersion = "2.3.2 (21173)")
|
||||
fun Approve(
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
sessionKey: SessionKey,
|
||||
/**
|
||||
* 好友列表分组的组的 ID. "我的好友" 为 0
|
||||
*/
|
||||
friendListId: Short,
|
||||
qq: UInt,
|
||||
qq: Long,
|
||||
/**
|
||||
* 备注. 不设置则需要为 `null` TODO 需要确认是否还需发送一个设置备注包. 因为测试时若有备注则会多发一个包并且包里面有所设置的备注
|
||||
*/
|
||||
@ -284,7 +273,7 @@ internal object AddFriendPacket : SessionPacketFactory<AddFriendPacket.Response>
|
||||
}
|
||||
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): Response {
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): Response {
|
||||
//02 02 B3 74 F6 00 //02 B3 74 F6 是QQ号
|
||||
return Response
|
||||
}
|
@ -1,53 +1,19 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.action
|
||||
package net.mamoe.mirai.timpc.network.packet.action
|
||||
|
||||
import kotlinx.io.charsets.Charsets
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.contact.QQ
|
||||
import net.mamoe.mirai.message.ImageId
|
||||
import net.mamoe.mirai.message.ImageId0x06
|
||||
import net.mamoe.mirai.message.requireLength
|
||||
import net.mamoe.mirai.message.data.ImageId
|
||||
import net.mamoe.mirai.message.data.ImageId0x06
|
||||
import net.mamoe.mirai.message.data.requireLength
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.*
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.event.EventPacket
|
||||
import net.mamoe.mirai.network.qqAccount
|
||||
import net.mamoe.mirai.qqAccount
|
||||
import net.mamoe.mirai.network.data.EventPacket
|
||||
import net.mamoe.mirai.network.data.ImageLink
|
||||
import net.mamoe.mirai.timpc.network.TIMProtocol
|
||||
import net.mamoe.mirai.timpc.network.packet.*
|
||||
import net.mamoe.mirai.utils.ExternalImage
|
||||
import net.mamoe.mirai.utils.Http
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
import net.mamoe.mirai.withSession
|
||||
|
||||
|
||||
/**
|
||||
* 上传图片
|
||||
* 挂起直到上传完成或失败
|
||||
*
|
||||
* 在 JVM 下, `SendImageUtilsJvm.kt` 内有多个捷径函数
|
||||
*
|
||||
* @throws OverFileSizeMaxException 如果文件过大, 服务器拒绝接收时
|
||||
*/
|
||||
suspend fun QQ.uploadImage(image: ExternalImage): ImageId = bot.withSession {
|
||||
FriendImagePacket.RequestImageId(qqAccount, sessionKey, id, image).sendAndExpect<FriendImageResponse>().let {
|
||||
when (it) {
|
||||
is FriendImageUKey -> {
|
||||
Http.postImage(
|
||||
htcmd = "0x6ff0070",
|
||||
uin = bot.qqAccount,
|
||||
groupId = null,
|
||||
uKeyHex = it.uKey.toUHexString(""),
|
||||
imageInput = image.input,
|
||||
inputSize = image.inputSize
|
||||
)
|
||||
it.imageId
|
||||
}
|
||||
is FriendImageAlreadyExists -> it.imageId
|
||||
is FriendImageOverFileSizeMax -> throw OverFileSizeMaxException()
|
||||
else -> error("This shouldn't happen")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// region FriendImageResponse
|
||||
@ -58,7 +24,7 @@ internal interface FriendImageResponse : EventPacket
|
||||
* 图片数据地址.
|
||||
*/
|
||||
// TODO: 2019/11/15 应该为 inline class, 但 kotlin 有 bug
|
||||
data class FriendImageLink(override inline val original: String) : FriendImageResponse, ImageLink {
|
||||
internal data class FriendImageLink(override inline val original: String) : FriendImageResponse, ImageLink {
|
||||
override fun toString(): String = "FriendImageLink($original)"
|
||||
}
|
||||
|
||||
@ -96,9 +62,9 @@ internal object FriendImageOverFileSizeMax : FriendImageResponse {
|
||||
internal object FriendImagePacket : SessionPacketFactory<FriendImageResponse>() {
|
||||
@Suppress("FunctionName")
|
||||
fun RequestImageId(
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
sessionKey: SessionKey,
|
||||
target: UInt,
|
||||
target: Long,
|
||||
image: ExternalImage
|
||||
): OutgoingPacket = buildSessionPacket(
|
||||
bot,
|
||||
@ -119,8 +85,8 @@ internal object FriendImagePacket : SessionPacketFactory<FriendImageResponse>()
|
||||
writeTV(0x08_01u)
|
||||
|
||||
writeUVarIntLVPacket(tag = 0x12u, lengthOffset = { it + 1 }) {
|
||||
writeTUVarint(0x08u, bot)
|
||||
writeTUVarint(0x10u, target)
|
||||
writeTUVarint(0x08u, bot.toUInt())
|
||||
writeTUVarint(0x10u, target.toUInt())
|
||||
writeTV(0x18_00u)
|
||||
writeTLV(0x22u, image.md5)
|
||||
writeTUVarint(0x28u, image.inputSize.toUInt())
|
||||
@ -150,7 +116,7 @@ internal object FriendImagePacket : SessionPacketFactory<FriendImageResponse>()
|
||||
|
||||
@Suppress("FunctionName")
|
||||
fun RequestImageLink(
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
sessionKey: SessionKey,
|
||||
imageId: ImageId
|
||||
): OutgoingPacket {
|
||||
@ -207,8 +173,8 @@ internal object FriendImagePacket : SessionPacketFactory<FriendImageResponse>()
|
||||
|
||||
writeUByte(0x1Au)
|
||||
writeUByte(0x47u)
|
||||
writeTUVarint(0x08u, bot)
|
||||
writeTUVarint(0x10u, bot) // 这里实际上应该是这张图片来自哪个 QQ 号. 但传 bot 也没事.
|
||||
writeTUVarint(0x08u, bot.toUInt())
|
||||
writeTUVarint(0x10u, bot.toUInt()) // 这里实际上应该是这张图片来自哪个 QQ 号. 但传 bot 也没事.
|
||||
writeTLV(0x1Au, imageId.value.toByteArray(Charsets.ISO_8859_1))
|
||||
writeHex("20 02 30 04 38 20 40 FF 01 50 00 6A 05 32 36 39 33 33 78 01")
|
||||
}
|
||||
@ -217,7 +183,7 @@ internal object FriendImagePacket : SessionPacketFactory<FriendImageResponse>()
|
||||
override suspend fun ByteReadPacket.decode(
|
||||
id: PacketId,
|
||||
sequenceId: UShort,
|
||||
handler: BotNetworkHandler<*>
|
||||
handler: BotNetworkHandler
|
||||
): FriendImageResponse {
|
||||
|
||||
// 上传图片, 成功获取ID
|
@ -1,12 +1,12 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.action
|
||||
package net.mamoe.mirai.timpc.network.packet.action
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.Packet
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.PacketId
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.SessionPacketFactory
|
||||
import net.mamoe.mirai.network.data.Packet
|
||||
import net.mamoe.mirai.timpc.network.packet.PacketId
|
||||
import net.mamoe.mirai.timpc.network.packet.SessionPacketFactory
|
||||
|
||||
|
||||
// 0001
|
||||
@ -26,7 +26,7 @@ import net.mamoe.mirai.network.protocol.timpc.packet.SessionPacketFactory
|
||||
internal inline class FriendListList(val delegate: List<FriendList>): Packet
|
||||
|
||||
internal object QueryFriendListListPacket : SessionPacketFactory<FriendList>() {
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): FriendList {
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): FriendList {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
}
|
@ -1,14 +1,14 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.action
|
||||
package net.mamoe.mirai.timpc.network.packet.action
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.writeFully
|
||||
import kotlinx.io.core.writeUByte
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.*
|
||||
import net.mamoe.mirai.utils.io.encryptAndWrite
|
||||
import net.mamoe.mirai.network.data.Packet
|
||||
import net.mamoe.mirai.timpc.network.TIMProtocol
|
||||
import net.mamoe.mirai.timpc.network.packet.*
|
||||
import net.mamoe.mirai.utils.io.writeQQ
|
||||
|
||||
/**
|
||||
@ -18,7 +18,7 @@ import net.mamoe.mirai.utils.io.writeQQ
|
||||
*/
|
||||
internal object RequestAccountInfoPacket : SessionPacketFactory<RequestAccountInfoPacket.Response>() {
|
||||
operator fun invoke(
|
||||
qq: UInt,
|
||||
qq: Long,
|
||||
sessionKey: SessionKey
|
||||
): OutgoingPacket = buildOutgoingPacket {
|
||||
writeQQ(qq)
|
||||
@ -35,5 +35,5 @@ internal object RequestAccountInfoPacket : SessionPacketFactory<RequestAccountIn
|
||||
override fun toString(): String = "RequestAccountInfoPacket.Response"
|
||||
}
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): Response = Response
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): Response = Response
|
||||
}
|
@ -1,62 +1,21 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS", "RUNTIME_ANNOTATION_NOT_SUPPORTED")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.action
|
||||
package net.mamoe.mirai.timpc.network.packet.action
|
||||
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.serialization.SerialId
|
||||
import kotlinx.serialization.Serializable
|
||||
import net.mamoe.mirai.contact.Group
|
||||
import net.mamoe.mirai.contact.GroupId
|
||||
import net.mamoe.mirai.contact.GroupInternalId
|
||||
import net.mamoe.mirai.contact.withSession
|
||||
import net.mamoe.mirai.message.ImageId
|
||||
import net.mamoe.mirai.message.ImageId0x03
|
||||
import net.mamoe.mirai.message.requireLength
|
||||
import net.mamoe.mirai.message.data.ImageId0x03
|
||||
import net.mamoe.mirai.message.data.requireLength
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.*
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.event.EventPacket
|
||||
import net.mamoe.mirai.qqAccount
|
||||
import net.mamoe.mirai.network.data.EventPacket
|
||||
import net.mamoe.mirai.network.data.ImageLink
|
||||
import net.mamoe.mirai.timpc.network.packet.*
|
||||
import net.mamoe.mirai.timpc.utils.assertUnreachable
|
||||
import net.mamoe.mirai.utils.ExternalImage
|
||||
import net.mamoe.mirai.utils.Http
|
||||
import net.mamoe.mirai.utils.assertUnreachable
|
||||
import net.mamoe.mirai.utils.io.toUHexString
|
||||
|
||||
|
||||
/**
|
||||
* 上传群图片
|
||||
* 挂起直到上传完成或失败
|
||||
*
|
||||
* 在 JVM 下, `SendImageUtilsJvm.kt` 内有多个捷径函数
|
||||
*
|
||||
* @throws OverFileSizeMaxException 如果文件过大, 服务器拒绝接收时
|
||||
*/
|
||||
suspend fun Group.uploadImage(image: ExternalImage): ImageId = withSession {
|
||||
val userContext = coroutineContext
|
||||
val response = GroupImagePacket.RequestImageId(bot.qqAccount, internalId, image, sessionKey).sendAndExpect<GroupImageResponse>()
|
||||
|
||||
withContext(userContext) {
|
||||
when (response) {
|
||||
is ImageUploadInfo -> response.uKey?.let {
|
||||
Http.postImage(
|
||||
htcmd = "0x6ff0071",
|
||||
uin = bot.qqAccount,
|
||||
groupId = GroupId(id),
|
||||
imageInput = image.input,
|
||||
inputSize = image.inputSize,
|
||||
uKeyHex = it.toUHexString("")
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: 2019/11/17 超过大小的情况
|
||||
//is Overfile -> throw OverFileSizeMaxException()
|
||||
else -> assertUnreachable()
|
||||
}
|
||||
}
|
||||
|
||||
return image.groupImageId
|
||||
}
|
||||
|
||||
internal interface GroupImageResponse : EventPacket
|
||||
|
||||
// endregion
|
||||
@ -193,7 +152,7 @@ internal object GroupImagePacket : SessionPacketFactory<GroupImageResponse>() {
|
||||
|
||||
@Suppress("FunctionName")
|
||||
fun RequestImageId(
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
groupInternalId: GroupInternalId,
|
||||
image: ExternalImage,
|
||||
sessionKey: SessionKey
|
||||
@ -215,7 +174,7 @@ internal object GroupImagePacket : SessionPacketFactory<GroupImageResponse>() {
|
||||
private val requestImageLinkHead = ubyteArrayOf(0x08u, 0x01u, 0x12u, 0x03u, 0x98u, 0x01u, 0x2u)
|
||||
@Suppress("FunctionName")
|
||||
fun RequestImageLink(
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
sessionKey: SessionKey,
|
||||
imageId: ImageId0x03
|
||||
): OutgoingPacket {
|
||||
@ -242,7 +201,7 @@ internal object GroupImagePacket : SessionPacketFactory<GroupImageResponse>() {
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): GroupImageResponse {
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): GroupImageResponse {
|
||||
|
||||
@Serializable
|
||||
data class GroupImageResponseProto(
|
@ -1,88 +1,34 @@
|
||||
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.action
|
||||
package net.mamoe.mirai.timpc.network.packet.action
|
||||
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.contact.*
|
||||
import net.mamoe.mirai.contact.internal.Member
|
||||
import net.mamoe.mirai.message.MessageChain
|
||||
import net.mamoe.mirai.contact.GroupInternalId
|
||||
import net.mamoe.mirai.contact.MemberPermission
|
||||
import net.mamoe.mirai.contact.groupInternalId
|
||||
import net.mamoe.mirai.message.data.MessageChain
|
||||
import net.mamoe.mirai.message.internal.toPacket
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.*
|
||||
import net.mamoe.mirai.utils.LockFreeLinkedList
|
||||
import net.mamoe.mirai.utils.MiraiInternalAPI
|
||||
import net.mamoe.mirai.network.data.Packet
|
||||
import net.mamoe.mirai.timpc.internal.RawGroupInfo
|
||||
import net.mamoe.mirai.timpc.network.TIMProtocol
|
||||
import net.mamoe.mirai.timpc.network.packet.*
|
||||
import net.mamoe.mirai.timpc.utils.unsupportedFlag
|
||||
import net.mamoe.mirai.timpc.utils.unsupportedType
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
import net.mamoe.mirai.withSession
|
||||
import kotlin.collections.mutableMapOf
|
||||
import kotlin.collections.set
|
||||
|
||||
|
||||
/**
|
||||
* 群资料
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate") // 将来使用
|
||||
class GroupInfo(
|
||||
internal var _group: Group,
|
||||
internal var _owner: Member,
|
||||
internal var _name: String,
|
||||
internal var _announcement: String,
|
||||
internal var _members: ContactList<Member>
|
||||
) {
|
||||
val group: Group get() = _group
|
||||
val owner: Member get() = _owner
|
||||
val name: String get() = _name
|
||||
val announcement: String get() = _announcement
|
||||
val members: ContactList<Member> get() = _members
|
||||
|
||||
override fun toString(): String =
|
||||
"GroupInfo(id=${group.id}, owner=$owner, name=$name, announcement=$announcement, members=${members.idContentString}"
|
||||
}
|
||||
|
||||
internal object GroupNotFound : GroupPacket.InfoResponse {
|
||||
override fun toString(): String = "GroupPacket.InfoResponse.GroupNotFound"
|
||||
}
|
||||
|
||||
internal data class RawGroupInfo(
|
||||
val group: UInt,
|
||||
val owner: UInt,
|
||||
val name: String,
|
||||
val announcement: String,
|
||||
/**
|
||||
* 含群主
|
||||
*/
|
||||
val members: Map<UInt, MemberPermission>
|
||||
) : GroupPacket.InfoResponse {
|
||||
|
||||
@UseExperimental(MiraiInternalAPI::class)
|
||||
fun parseBy(group: Group): GroupInfo = group.bot.withSession {
|
||||
val memberList = LockFreeLinkedList<Member>()
|
||||
members.forEach { entry: Map.Entry<UInt, MemberPermission> ->
|
||||
memberList.addLast(entry.key.qq().let { group.Member(it, entry.value, it.coroutineContext) })
|
||||
}
|
||||
return GroupInfo(
|
||||
group,
|
||||
this@RawGroupInfo.owner.qq().let { group.Member(it, MemberPermission.OWNER, it.coroutineContext) },
|
||||
this@RawGroupInfo.name,
|
||||
this@RawGroupInfo.announcement,
|
||||
ContactList(memberList)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出群的返回
|
||||
*/
|
||||
inline class QuitGroupResponse(private val _group: GroupInternalId?) : Packet, GroupPacket.GroupPacketResponse {
|
||||
val group: GroupInternalId get() = _group ?: error("request failed")
|
||||
val isSuccess: Boolean get() = _group != null
|
||||
|
||||
override fun toString(): String = "GroupPacket.QuitResponse"
|
||||
}
|
||||
|
||||
@Suppress("FunctionName")
|
||||
internal object GroupPacket : SessionPacketFactory<GroupPacket.GroupPacketResponse>() {
|
||||
@PacketVersion(date = "2019.10.19", timVersion = "2.3.2 (21173)")
|
||||
fun Message(
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
groupInternalId: GroupInternalId,
|
||||
sessionKey: SessionKey,
|
||||
message: MessageChain
|
||||
@ -109,7 +55,7 @@ internal object GroupPacket : SessionPacketFactory<GroupPacket.GroupPacketRespon
|
||||
*/
|
||||
@PacketVersion(date = "2019.11.28", timVersion = "2.3.2 (21173)")
|
||||
fun QuitGroup(
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
sessionKey: SessionKey,
|
||||
group: GroupInternalId
|
||||
): OutgoingPacket = buildSessionPacket(bot, sessionKey, name = "GroupPacket.QuitGroup") {
|
||||
@ -122,7 +68,7 @@ internal object GroupPacket : SessionPacketFactory<GroupPacket.GroupPacketRespon
|
||||
*/
|
||||
@PacketVersion(date = "2019.11.27", timVersion = "2.3.2 (21173)")
|
||||
fun QueryGroupInfo(
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
groupInternalId: GroupInternalId,
|
||||
sessionKey: SessionKey
|
||||
): OutgoingPacket = buildSessionPacket(bot, sessionKey, name = "GroupPacket.QueryGroupInfo", headerSizeHint = 9) {
|
||||
@ -136,10 +82,10 @@ internal object GroupPacket : SessionPacketFactory<GroupPacket.GroupPacketRespon
|
||||
*/
|
||||
@PacketVersion(date = "2019.12.2", timVersion = "2.3.2 (21173)")
|
||||
fun Mute(
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
groupInternalId: GroupInternalId,
|
||||
sessionKey: SessionKey,
|
||||
target: UInt,
|
||||
target: Long,
|
||||
/**
|
||||
* 0 为取消
|
||||
*/
|
||||
@ -168,12 +114,22 @@ internal object GroupPacket : SessionPacketFactory<GroupPacket.GroupPacketRespon
|
||||
|
||||
internal interface InfoResponse : Packet, GroupPacketResponse
|
||||
|
||||
/**
|
||||
* 退出群的返回
|
||||
*/
|
||||
class QuitGroupResponse(private val _group: GroupInternalId?) : Packet, GroupPacketResponse {
|
||||
val group: GroupInternalId get() = _group ?: error("request failed")
|
||||
val isSuccess: Boolean get() = _group != null
|
||||
|
||||
override fun toString(): String = "GroupPacket.QuitResponse"
|
||||
}
|
||||
|
||||
@PacketVersion(date = "2019.11.27", timVersion = "2.3.2 (21173)")
|
||||
@UseExperimental(ExperimentalStdlibApi::class)
|
||||
override suspend fun ByteReadPacket.decode(
|
||||
id: PacketId,
|
||||
sequenceId: UShort,
|
||||
handler: BotNetworkHandler<*>
|
||||
handler: BotNetworkHandler
|
||||
): GroupPacketResponse {
|
||||
return when (val packetType = readUByte().toUInt()) {
|
||||
0x2Au -> MessageResponse
|
||||
@ -181,7 +137,7 @@ internal object GroupPacket : SessionPacketFactory<GroupPacket.GroupPacketRespon
|
||||
|
||||
0x09u -> {
|
||||
if (readByte().toInt() == 0) {
|
||||
QuitGroupResponse(readUInt().groupInternalId())
|
||||
QuitGroupResponse(readUInt().toLong().groupInternalId())
|
||||
} else {
|
||||
QuitGroupResponse(null)
|
||||
}
|
||||
@ -218,10 +174,10 @@ internal object GroupPacket : SessionPacketFactory<GroupPacket.GroupPacketRespon
|
||||
76 E4 B8 DD 00 00
|
||||
*/
|
||||
discardExact(4) // group internal id
|
||||
val group = readUInt() // group id
|
||||
val group = readUInt().toLong() // group id
|
||||
|
||||
discardExact(13) //00 00 00 03 01 01 00 04 01 00 80 01 40
|
||||
val owner = readUInt()
|
||||
val owner = readUInt().toLong()
|
||||
discardExact(22)
|
||||
val groupName = readUByteLVString()
|
||||
|
||||
@ -234,11 +190,11 @@ internal object GroupPacket : SessionPacketFactory<GroupPacket.GroupPacketRespon
|
||||
|
||||
discardExact(50)
|
||||
|
||||
val stop = readUInt() // 标记读取群成员的结束
|
||||
val stop = readUInt().toLong() // 标记读取群成员的结束
|
||||
discardExact(1) // 00
|
||||
val members = mutableMapOf<UInt, MemberPermission>()
|
||||
val members = mutableMapOf<Long, MemberPermission>()
|
||||
do {
|
||||
val qq = readUInt()
|
||||
val qq = readUInt().toLong()
|
||||
val status = readUShort().toInt() // 这个群成员的状态, 最后一 bit 为管理员权限. 这里面还包含其他状态
|
||||
if (qq == owner) {
|
||||
continue
|
@ -1,6 +1,6 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.action
|
||||
package net.mamoe.mirai.timpc.network.packet.action
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.post
|
||||
@ -17,7 +17,7 @@ import net.mamoe.mirai.contact.GroupId
|
||||
@Suppress("SpellCheckingInspection")
|
||||
internal suspend inline fun HttpClient.postImage(
|
||||
htcmd: String,
|
||||
uin: UInt,
|
||||
uin: Long,
|
||||
groupId: GroupId?,
|
||||
imageInput: Input,
|
||||
inputSize: Long,
|
||||
@ -30,7 +30,7 @@ internal suspend inline fun HttpClient.postImage(
|
||||
path("cgi-bin/httpconn")
|
||||
|
||||
parameters["htcmd"] = htcmd
|
||||
parameters["uin"] = uin.toLong().toString()
|
||||
parameters["uin"] = uin.toString()
|
||||
|
||||
if (groupId != null) parameters["groupcode"] = groupId.value.toLong().toString()
|
||||
|
@ -1,27 +1,7 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS", "unused", "NO_REFLECTION_IN_CLASS_PATH")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.action
|
||||
package net.mamoe.mirai.timpc.network.packet.action
|
||||
|
||||
import io.ktor.client.request.get
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.readBytes
|
||||
import net.mamoe.mirai.utils.Http
|
||||
|
||||
/**
|
||||
* 图片文件过大
|
||||
*/
|
||||
class OverFileSizeMaxException : IllegalStateException()
|
||||
|
||||
interface ImageLink {
|
||||
/**
|
||||
* 原图
|
||||
*/
|
||||
val original: String
|
||||
|
||||
suspend fun downloadAsByteArray(): ByteArray = download().readBytes()
|
||||
|
||||
suspend fun download(): ByteReadPacket = Http.get(original)
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
@ -32,8 +12,8 @@ interface ImageLink {
|
||||
@PacketVersion(date = "2019.10.26", timVersion = "2.3.2 (21173)")
|
||||
object SubmitImageFilenamePacket : PacketFactory {
|
||||
operator fun invoke(
|
||||
bot: UInt,
|
||||
target: UInt,
|
||||
bot: Long,
|
||||
target: Long,
|
||||
filename: String,
|
||||
sessionKey: SessionKey
|
||||
): OutgoingPacket = buildOutgoingPacket {
|
@ -1,13 +1,14 @@
|
||||
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.action
|
||||
package net.mamoe.mirai.timpc.network.packet.action
|
||||
|
||||
import io.ktor.util.date.GMTDate
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.contact.data.Gender
|
||||
import net.mamoe.mirai.contact.data.Profile
|
||||
import net.mamoe.mirai.network.data.Gender
|
||||
import net.mamoe.mirai.network.data.Profile
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.*
|
||||
import net.mamoe.mirai.network.data.Packet
|
||||
import net.mamoe.mirai.timpc.network.packet.*
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
|
||||
inline class AvatarLink(val value: String) : Packet
|
||||
@ -23,13 +24,13 @@ internal object QueryNicknamePacket : SessionPacketFactory<NicknameMap>() {
|
||||
*/
|
||||
@PacketVersion(date = "2019.12.7", timVersion = "2.3.2 (21173)")
|
||||
operator fun invoke(
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
sessionKey: SessionKey,
|
||||
qq: UInt
|
||||
qq: Long
|
||||
): OutgoingPacket = buildSessionPacket(bot, sessionKey) {
|
||||
writeHex("03 00 00 00 00 00 00 00 00 00 00")
|
||||
writeByte(1)
|
||||
writeUInt(qq)
|
||||
writeQQ(qq)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -38,7 +39,7 @@ internal object QueryNicknamePacket : SessionPacketFactory<NicknameMap>() {
|
||||
*/
|
||||
@PacketVersion(date = "2019.12.7", timVersion = "2.3.2 (21173)")
|
||||
operator fun invoke(
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
sessionKey: SessionKey,
|
||||
qq: Array<UInt>
|
||||
): OutgoingPacket = buildSessionPacket(bot, sessionKey) {
|
||||
@ -109,7 +110,7 @@ body=03 00 00 00 00 00 00 00 00 00 00 00 03 8E 3C A6 A0 EE EF 02 07 6C 01 14 E8
|
||||
04 5D EC AF 48
|
||||
*/
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): NicknameMap {
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): NicknameMap {
|
||||
//03 00 00 00 00 00 00 00 00 00 00 12 04 14 37
|
||||
val type = readUByte().toInt()
|
||||
if (type == 15) {
|
||||
@ -137,16 +138,16 @@ body=03 00 00 00 00 00 00 00 00 00 00 00 03 8E 3C A6 A0 EE EF 02 07 6C 01 14 E8
|
||||
internal object RequestProfileAvatarPacket : SessionPacketFactory<AvatarLink>() {
|
||||
//00 01 00 17 D4 54 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 29 4E 22 4E 25 4E 26 4E 27 4E 29 4E 2A 4E 2B 4E 2D 4E 2E 4E 2F 4E 30 4E 31 4E 33 4E 35 4E 36 4E 37 4E 38 4E 3F 4E 40 4E 41 4E 42 4E 43 4E 45 4E 49 4E 4B 4E 4F 4E 54 4E 5B 52 0B 52 0F 5D C2 5D C8 65 97 69 9D 69 A9 9D A5 A4 91 A4 93 A4 94 A4 9C A4 B5
|
||||
operator fun invoke(
|
||||
bot: UInt,
|
||||
qq: UInt,
|
||||
bot: Long,
|
||||
qq: Long,
|
||||
sessionKey: SessionKey
|
||||
): OutgoingPacket = buildSessionPacket(bot, sessionKey) {
|
||||
writeUShort(0x01u)
|
||||
writeUInt(qq)
|
||||
writeQQ(qq)
|
||||
writeHex("00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 29 4E 22 4E 25 4E 26 4E 27 4E 29 4E 2A 4E 2B 4E 2D 4E 2E 4E 2F 4E 30 4E 31 4E 33 4E 35 4E 36 4E 37 4E 38 4E 3F 4E 40 4E 41 4E 42 4E 43 4E 45 4E 49 4E 4B 4E 4F 4E 54 4E 5B 52 0B 52 0F 5D C2 5D C8 65 97 69 9D 69 A9 9D A5 A4 91 A4 93 A4 94 A4 9C A4 B5")
|
||||
}
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): AvatarLink {
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): AvatarLink {
|
||||
println(" RequestProfileAvatarPacket body=${this.readBytes().toUHexString()}")
|
||||
TODO()
|
||||
}
|
||||
@ -163,19 +164,19 @@ internal object RequestProfileDetailsPacket : SessionPacketFactory<RequestProfil
|
||||
//00 01 87 73 86 9D 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 29 4E 22 4E 25 4E 26 4E 27 4E 29 4E 2A 4E 2B 4E 2D 4E 2E 4E 2F 4E 30 4E 31 4E 33 4E 35 4E 36 4E 37 4E 38 4E 3F 4E 40 4E 41 4E 42 4E 43 4E 45 4E 49 4E 4B 4E 4F 4E 54 4E 5B 52 0B 52 0F 5D C2 5D C8 65 97 69 9D 69 A9 9D A5 A4 91 A4 93 A4 94 A4 9C A4 B5
|
||||
@PacketVersion(date = "2019.11.2", timVersion = "2.3.2 (21173)")
|
||||
operator fun invoke(
|
||||
bot: UInt,
|
||||
qq: UInt,
|
||||
bot: Long,
|
||||
qq: Long,
|
||||
sessionKey: SessionKey
|
||||
): OutgoingPacket = buildSessionPacket(bot, sessionKey) {
|
||||
writeUShort(0x01u)
|
||||
writeUInt(qq)
|
||||
writeQQ(qq)
|
||||
writeHex("00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 29 4E 22 4E 25 4E 26 4E 27 4E 29 4E 2A 4E 2B 4E 2D 4E 2E 4E 2F 4E 30 4E 31 4E 33 4E 35 4E 36 4E 37 4E 38 4E 3F 4E 40 4E 41 4E 42 4E 43 4E 45 4E 49 4E 4B 4E 4F 4E 54 4E 5B 52 0B 52 0F 5D C2 5D C8 65 97 69 9D 69 A9 9D A5 A4 91 A4 93 A4 94 A4 9C A4 B5")
|
||||
}
|
||||
|
||||
@PacketVersion(date = "2019.11.2", timVersion = "2.3.2 (21173)")
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): RequestProfileDetailsResponse {
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): RequestProfileDetailsResponse {
|
||||
discardExact(3)
|
||||
val qq = readUInt()
|
||||
val qq = readUInt().toLong()
|
||||
discardExact(6)
|
||||
val map = readTLVMap(tagSize = 2, expectingEOF = true)
|
||||
//map.printTLVMap("Profile(qq=$qq) raw=")
|
||||
@ -193,7 +194,7 @@ internal object RequestProfileDetailsPacket : SessionPacketFactory<RequestProfil
|
||||
0x02u -> Gender.FEMALE
|
||||
0x01u -> Gender.MALE
|
||||
else -> Gender.SECRET // 猜的
|
||||
//else -> error("Cannot determine gender, bad value of 0x4E29u: ${map[0x4729u]!![0].toUHexString()}")
|
||||
//else -> error("Cannot determine gender, bad value of 0x4E29u: ${map[0x4729u]!![0].toHexString()}")
|
||||
},
|
||||
birthday = map[0x4E3Fu]?.let { GMTDate(it.toUInt().toLong()) },
|
||||
personalStatement = map[0x4E33u]?.encodeToString(),
|
||||
@ -209,7 +210,7 @@ internal object RequestProfileDetailsPacket : SessionPacketFactory<RequestProfil
|
||||
}
|
||||
|
||||
internal data class RequestProfileDetailsResponse(
|
||||
val qq: UInt,
|
||||
val qq: Long,
|
||||
val profile: Profile
|
||||
) : Packet {
|
||||
//00 01 00 99 6B F8 D2 00 00 00 00 00 29
|
@ -1,29 +1,25 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.action
|
||||
package net.mamoe.mirai.timpc.network.packet.action
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.discardExact
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.*
|
||||
import net.mamoe.mirai.network.data.FriendNameRemark
|
||||
import net.mamoe.mirai.timpc.network.packet.*
|
||||
import net.mamoe.mirai.utils.io.readUShortLVString
|
||||
import net.mamoe.mirai.utils.io.writeQQ
|
||||
import net.mamoe.mirai.utils.io.writeZero
|
||||
|
||||
/**
|
||||
* 给好友设置的备注
|
||||
*/
|
||||
inline class FriendNameRemark(val value: String) : Packet
|
||||
|
||||
internal object QueryFriendRemarkPacket : SessionPacketFactory<FriendNameRemark>() {
|
||||
/**
|
||||
* 查询好友的备注
|
||||
*/
|
||||
@PacketVersion(date = "2019.11.27", timVersion = "2.3.2 (21173)")
|
||||
operator fun invoke(
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
sessionKey: SessionKey,
|
||||
target: UInt
|
||||
target: Long
|
||||
): OutgoingPacket = buildSessionPacket(
|
||||
bot, sessionKey
|
||||
) {
|
||||
@ -32,7 +28,7 @@ internal object QueryFriendRemarkPacket : SessionPacketFactory<FriendNameRemark>
|
||||
writeZero(1)
|
||||
}
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): FriendNameRemark {
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): FriendNameRemark {
|
||||
//0D 00 5D DA 3D 0F 59 17 3E 05 00 00 06 E6 9F 90 E4 B9 90 00 00 00 00 00 00
|
||||
discardExact(11)
|
||||
return FriendNameRemark(readUShortLVString())
|
@ -1,19 +1,20 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.action
|
||||
package net.mamoe.mirai.timpc.network.packet.action
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.*
|
||||
import net.mamoe.mirai.network.data.Packet
|
||||
import net.mamoe.mirai.timpc.network.TIMProtocol
|
||||
import net.mamoe.mirai.timpc.network.packet.*
|
||||
import net.mamoe.mirai.utils.io.writeZero
|
||||
|
||||
class FriendList : Packet
|
||||
|
||||
@PacketVersion(date = "2019.11.24", timVersion = "2.3.2 (21173)")
|
||||
internal object RequestFriendListPacket : SessionPacketFactory<FriendList>() {
|
||||
@PacketVersion(date = "2019.11.24", timVersion = "2.3.2 (21173)")
|
||||
operator fun invoke(
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
sessionKey: SessionKey
|
||||
): OutgoingPacket = buildSessionPacket(
|
||||
bot, sessionKey, version = TIMProtocol.version0x02
|
||||
@ -22,7 +23,7 @@ internal object RequestFriendListPacket : SessionPacketFactory<FriendList>() {
|
||||
writeZero(4)
|
||||
}
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): FriendList {
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): FriendList {
|
||||
|
||||
TODO()
|
||||
}
|
@ -1,21 +1,22 @@
|
||||
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.action
|
||||
package net.mamoe.mirai.timpc.network.packet.action
|
||||
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.message.MessageChain
|
||||
import net.mamoe.mirai.message.data.MessageChain
|
||||
import net.mamoe.mirai.message.internal.toPacket
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.*
|
||||
import net.mamoe.mirai.network.data.Packet
|
||||
import net.mamoe.mirai.timpc.network.TIMProtocol
|
||||
import net.mamoe.mirai.timpc.network.packet.*
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
import net.mamoe.mirai.utils.md5
|
||||
|
||||
@PacketVersion(date = "2019.10.19", timVersion = "2.3.2 (21173)")
|
||||
internal object SendFriendMessagePacket : SessionPacketFactory<SendFriendMessagePacket.Response>() {
|
||||
operator fun invoke(
|
||||
botQQ: UInt,
|
||||
targetQQ: UInt,
|
||||
botQQ: Long,
|
||||
targetQQ: Long,
|
||||
sessionKey: SessionKey,
|
||||
message: MessageChain
|
||||
): OutgoingPacket = buildSessionPacket(botQQ, sessionKey) {
|
||||
@ -25,14 +26,11 @@ internal object SendFriendMessagePacket : SessionPacketFactory<SendFriendMessage
|
||||
writeHex("38 03")
|
||||
writeQQ(botQQ)
|
||||
writeQQ(targetQQ)
|
||||
writeFully(md5(buildPacket { writeQQ(targetQQ); writeFully(sessionKey) }.readBytes()))
|
||||
writeFully(md5(buildPacket { writeQQ(targetQQ); writeFully(sessionKey.value) }.readBytes()))
|
||||
writeHex("00 0B")
|
||||
writeRandom(2)
|
||||
writeTime()
|
||||
writeHex(
|
||||
"01 1D" +
|
||||
" 00 00 00 00"
|
||||
)
|
||||
writeHex("01 1D 00 00 00 00")
|
||||
|
||||
//消息过多要分包发送
|
||||
//如果只有一个
|
||||
@ -68,5 +66,5 @@ internal object SendFriendMessagePacket : SessionPacketFactory<SendFriendMessage
|
||||
override fun toString(): String = "SendFriendMessagePacket.Response"
|
||||
}
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): Response = Response
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): Response = Response
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.event
|
||||
package net.mamoe.mirai.timpc.network.packet.event
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.discardExact
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.Packet
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.PacketVersion
|
||||
import net.mamoe.mirai.network.data.Packet
|
||||
import net.mamoe.mirai.timpc.network.packet.PacketVersion
|
||||
import net.mamoe.mirai.utils.io.readBoolean
|
||||
|
||||
|
@ -1,20 +1,14 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.event
|
||||
package net.mamoe.mirai.timpc.network.packet.event
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.discardExact
|
||||
import kotlinx.io.core.readBytes
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.event.events.ConnectionOccupiedEvent
|
||||
import net.mamoe.mirai.utils.io.encodeToString
|
||||
|
||||
/**
|
||||
* 被挤下线. 只能获取到中文的消息
|
||||
*/
|
||||
inline class ConnectionOccupiedEvent(val message: String) : EventPacket {
|
||||
override fun toString(): String = "ConnectionOccupiedEvent(${message.replace("\n", "")})"
|
||||
}
|
||||
|
||||
internal object ConnectionOccupiedPacketHandler : KnownEventParserAndHandler<ConnectionOccupiedEvent>(0x0030u) {
|
||||
override suspend fun ByteReadPacket.parse(bot: Bot, identity: EventPacketIdentity): ConnectionOccupiedEvent {
|
||||
discardExact(6)
|
@ -1,13 +1,13 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.event
|
||||
package net.mamoe.mirai.timpc.network.packet.event
|
||||
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.TIMBotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.*
|
||||
import net.mamoe.mirai.network.sessionKey
|
||||
import net.mamoe.mirai.network.data.Packet
|
||||
import net.mamoe.mirai.timpc.network.TIMBotNetworkHandler
|
||||
import net.mamoe.mirai.timpc.network.packet.*
|
||||
import net.mamoe.mirai.qqAccount
|
||||
import net.mamoe.mirai.utils.io.readIoBuffer
|
||||
|
||||
@ -15,16 +15,16 @@ import net.mamoe.mirai.utils.io.readIoBuffer
|
||||
* 事件的识别 ID. 在 ACK 时使用
|
||||
*/
|
||||
internal class EventPacketIdentity(
|
||||
val from: UInt,//对于好友消息, 这个是发送人
|
||||
val to: UInt,//对于好友消息, 这个是bot
|
||||
val from: Long,//对于好友消息, 这个是发送人
|
||||
val to: Long,//对于好友消息, 这个是bot
|
||||
internal val uniqueId: IoBuffer//8
|
||||
) {
|
||||
override fun toString(): String = "($from->$to)"
|
||||
}
|
||||
|
||||
internal fun BytePacketBuilder.writeEventPacketIdentity(identity: EventPacketIdentity) = with(identity) {
|
||||
writeUInt(from)
|
||||
writeUInt(to)
|
||||
writeUInt(from.toUInt())
|
||||
writeUInt(to.toUInt())
|
||||
writeFully(uniqueId)
|
||||
}
|
||||
|
||||
@ -39,20 +39,16 @@ internal fun matchEventPacketFactory(value: UShort): EventParserAndHandler<*> =
|
||||
@NoLog
|
||||
@Suppress("FunctionName")
|
||||
internal object EventPacketFactory : PacketFactory<Packet, SessionKey>(SessionKey) {
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): Packet {
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): Packet {
|
||||
val eventIdentity = EventPacketIdentity(
|
||||
from = readUInt(),
|
||||
to = readUInt(),
|
||||
from = readUInt().toLong(), // clear semantic, don't readQQ() or readGroup()
|
||||
to = readUInt().toLong(), // clear semantic
|
||||
uniqueId = readIoBuffer(8)
|
||||
)
|
||||
(handler as TIMBotNetworkHandler).socket.sendPacket(EventPacketFactory(id, sequenceId, handler.bot.qqAccount, handler.sessionKey, eventIdentity))
|
||||
discardExact(2) // 1F 40
|
||||
|
||||
return with(matchEventPacketFactory(readUShort())) { parse(handler.bot, eventIdentity) }.also {
|
||||
if (it is MessagePacket<*, *>) {
|
||||
it.botVar = handler.bot
|
||||
}
|
||||
|
||||
if (it is EventParserAndHandler<*>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
with(it as EventParserAndHandler<in Packet>) {
|
||||
@ -68,7 +64,7 @@ internal object EventPacketFactory : PacketFactory<Packet, SessionKey>(SessionKe
|
||||
operator fun invoke(
|
||||
id: PacketId,
|
||||
sequenceId: UShort,
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
sessionKey: SessionKey,
|
||||
identity: EventPacketIdentity
|
||||
): OutgoingPacket = buildSessionPacket(name = "EventPacket", id = id, sequenceId = sequenceId, bot = bot, sessionKey = sessionKey) {
|
||||
@ -84,7 +80,7 @@ internal interface EventParserAndHandler<TPacket : Packet> {
|
||||
/**
|
||||
* 在 [BotNetworkHandler] 下处理这个包. 广播事件等.
|
||||
*/
|
||||
suspend fun BotNetworkHandler<*>.handlePacket(packet: TPacket) {}
|
||||
suspend fun BotNetworkHandler.handlePacket(packet: TPacket) {}
|
||||
}
|
||||
|
||||
internal abstract class KnownEventParserAndHandler<TPacket : Packet>(override val id: UShort) : EventParserAndHandler<TPacket> {
|
@ -1,44 +1,19 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.event
|
||||
package net.mamoe.mirai.timpc.network.packet.event
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.discardExact
|
||||
import kotlinx.io.core.readUInt
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.contact.QQ
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.PacketVersion
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.action.AddFriendPacket
|
||||
import net.mamoe.mirai.network.qqAccount
|
||||
import net.mamoe.mirai.event.events.ReceiveFriendAddRequestEvent
|
||||
import net.mamoe.mirai.timpc.network.packet.PacketVersion
|
||||
import net.mamoe.mirai.utils.io.readQQ
|
||||
import net.mamoe.mirai.utils.io.readUShortLVString
|
||||
import net.mamoe.mirai.withSession
|
||||
import kotlin.jvm.JvmOverloads
|
||||
|
||||
|
||||
/**
|
||||
* 陌生人请求添加机器人账号为好友
|
||||
*/
|
||||
data class ReceiveFriendAddRequestEvent(
|
||||
val qq: QQ,
|
||||
/**
|
||||
* 验证消息
|
||||
*/
|
||||
val message: String
|
||||
) : EventPacket {
|
||||
/**
|
||||
* 同意这个请求
|
||||
*
|
||||
* @param remark 备注名, 不设置则需为 `null`
|
||||
*/
|
||||
@JvmOverloads
|
||||
suspend fun approve(remark: String? = null): Unit = qq.bot.withSession {
|
||||
AddFriendPacket.Approve(qqAccount, sessionKey, 0, qq.id, remark).sendAndExpect<AddFriendPacket.Response>()
|
||||
}
|
||||
}
|
||||
|
||||
@PacketVersion(date = "2019.11.20", timVersion = "2.3.2 (21173)")
|
||||
internal object FriendAddRequestEventPacket : KnownEventParserAndHandler<ReceiveFriendAddRequestEvent>(0x02DFu) {
|
||||
override suspend fun ByteReadPacket.parse(bot: Bot, identity: EventPacketIdentity): ReceiveFriendAddRequestEvent = bot.withSession {
|
||||
override suspend fun ByteReadPacket.parse(bot: Bot, identity: EventPacketIdentity): ReceiveFriendAddRequestEvent = with(bot) {
|
||||
// 00 00 00 08 00 0A 00 04 01 00
|
||||
// 00 00 00 01
|
||||
// 76 E4 B8 DD
|
||||
@ -88,7 +63,7 @@ Mirai 20:35:23 : Packet received: UnknownEventPacket(id=00 BB, identity=(7610254
|
||||
discardExact(10 + 4) // 00 00 00 08 00 0A 00 04 01 00 00 00 00 01
|
||||
discardExact(4) // bot account uint
|
||||
discardExact(4) // 00 00 00 01
|
||||
val qq = readUInt().qq()
|
||||
val qq = readQQ().qq()
|
||||
discardExact(4) // bot account uint
|
||||
discardExact(3) // 02 00 00 恒定
|
||||
|
@ -1,23 +1,25 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.event
|
||||
package net.mamoe.mirai.timpc.network.packet.event
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.discardExact
|
||||
import kotlinx.io.core.readUInt
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.PacketVersion
|
||||
import net.mamoe.mirai.network.data.EventPacket
|
||||
import net.mamoe.mirai.timpc.network.packet.PacketVersion
|
||||
import net.mamoe.mirai.utils.io.readQQ
|
||||
|
||||
|
||||
data class FriendConversationInitialize(
|
||||
val qq: UInt
|
||||
val qq: Long
|
||||
) : EventPacket
|
||||
|
||||
@PacketVersion(date = "2019.11.2", timVersion = "2.3.2 (21173)")
|
||||
internal object FriendConversationInitializedEventParserAndHandler : KnownEventParserAndHandler<FriendConversationInitialize>(0x0079u) {
|
||||
override suspend fun ByteReadPacket.parse(bot: Bot, identity: EventPacketIdentity): FriendConversationInitialize {
|
||||
discardExact(4)// 00 00 00 00
|
||||
return FriendConversationInitialize(readUInt())
|
||||
return FriendConversationInitialize(readQQ())
|
||||
}
|
||||
|
||||
}
|
@ -1,30 +1,26 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS", "JoinDeclarationAndAssignment")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.event
|
||||
package net.mamoe.mirai.timpc.network.packet.event
|
||||
|
||||
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.QQ
|
||||
import net.mamoe.mirai.event.events.FriendStatusChanged
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.KnownPacketId
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.PacketId
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.SessionPacketFactory
|
||||
import net.mamoe.mirai.timpc.network.packet.KnownPacketId
|
||||
import net.mamoe.mirai.timpc.network.packet.PacketId
|
||||
import net.mamoe.mirai.timpc.network.packet.SessionPacketFactory
|
||||
import net.mamoe.mirai.utils.OnlineStatus
|
||||
|
||||
data class FriendStatusChanged(
|
||||
val qq: QQ,
|
||||
val status: OnlineStatus
|
||||
) : EventPacket
|
||||
import net.mamoe.mirai.utils.io.readQQ
|
||||
|
||||
/**
|
||||
* 好友在线状态改变
|
||||
*/
|
||||
internal object FriendOnlineStatusChangedPacket : SessionPacketFactory<FriendStatusChanged>() {
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): FriendStatusChanged {
|
||||
val qq = readUInt()
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): FriendStatusChanged {
|
||||
val qq = readQQ()
|
||||
discardExact(8)
|
||||
val statusId = readUByte()
|
||||
val status = OnlineStatus(statusId)
|
@ -1,10 +1,11 @@
|
||||
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.event
|
||||
package net.mamoe.mirai.timpc.network.packet.event
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.PacketVersion
|
||||
import net.mamoe.mirai.network.data.EventPacket
|
||||
import net.mamoe.mirai.timpc.network.packet.PacketVersion
|
||||
import net.mamoe.mirai.utils.io.debugPrint
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.event
|
||||
package net.mamoe.mirai.timpc.network.packet.event
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.network.data.EventPacket
|
||||
import net.mamoe.mirai.utils.io.toUHexString
|
||||
|
||||
internal inline class IgnoredEventPacket(val id: UShort) : EventPacket {
|
@ -1,21 +1,19 @@
|
||||
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE", "unused", "MemberVisibilityCanBePrivate")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.event
|
||||
package net.mamoe.mirai.timpc.network.packet.event
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.readUInt
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.contact.Group
|
||||
import net.mamoe.mirai.contact.Member
|
||||
import net.mamoe.mirai.contact.MemberPermission
|
||||
import net.mamoe.mirai.contact.internal.Member
|
||||
import net.mamoe.mirai.contact.internal.MemberImpl
|
||||
import net.mamoe.mirai.event.Subscribable
|
||||
import net.mamoe.mirai.event.broadcast
|
||||
import net.mamoe.mirai.getGroup
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.timpc.TIMPCBot
|
||||
import net.mamoe.mirai.utils.MiraiInternalAPI
|
||||
import net.mamoe.mirai.utils.io.discardExact
|
||||
import net.mamoe.mirai.utils.io.readQQ
|
||||
|
||||
/**
|
||||
* 成员加入前的事件. 群的成员列表中还没有这个人
|
||||
@ -71,20 +69,23 @@ internal object MemberJoinPacketHandler : KnownEventParserAndHandler<MemberJoinE
|
||||
discardExact(11) //00 00 00 08 00 0A 00 04 01 00 00
|
||||
|
||||
discardExact(1) // 00
|
||||
val group = bot.getGroup(readUInt())
|
||||
val group = bot.getGroup(readQQ())
|
||||
|
||||
discardExact(1) // 01
|
||||
val qq = bot.getQQ(readUInt())
|
||||
val member = group.Member(qq, MemberPermission.MEMBER, qq.coroutineContext)
|
||||
val qq = bot.getQQ(readQQ())
|
||||
val member = with(bot) {
|
||||
this as? TIMPCBot ?: error("wrong Bot type passed")
|
||||
group.Member(qq, MemberPermission.MEMBER)
|
||||
}
|
||||
|
||||
return if (readByte().toInt() == 0x03) {
|
||||
MemberJoinEventPacket(member, null)
|
||||
} else {
|
||||
MemberJoinEventPacket(member, group.getMember(readUInt()))
|
||||
MemberJoinEventPacket(member, group.getMember(readQQ()))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun BotNetworkHandler<*>.handlePacket(packet: MemberJoinEventPacket) {
|
||||
override suspend fun BotNetworkHandler.handlePacket(packet: MemberJoinEventPacket) {
|
||||
PreMemberJoinEvent(packet).broadcast()
|
||||
packet.broadcast()
|
||||
PostMemberJoinEvent(packet).broadcast()
|
@ -1,18 +1,15 @@
|
||||
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.event
|
||||
package net.mamoe.mirai.timpc.network.packet.event
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.readUByte
|
||||
import kotlinx.io.core.readUInt
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.contact.Group
|
||||
import net.mamoe.mirai.contact.Member
|
||||
import net.mamoe.mirai.getGroup
|
||||
import net.mamoe.mirai.network.data.EventPacket
|
||||
import net.mamoe.mirai.qqAccount
|
||||
import net.mamoe.mirai.utils.io.discardExact
|
||||
import net.mamoe.mirai.utils.io.toUHexString
|
||||
import net.mamoe.mirai.utils.io.unsupported
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
|
||||
/**
|
||||
* 群成员列表变动事件.
|
||||
@ -53,21 +50,21 @@ internal object MemberGoneEventPacketHandler : KnownEventParserAndHandler<Member
|
||||
discardExact(11)
|
||||
|
||||
discardExact(1)
|
||||
val group = bot.getGroup(readUInt())
|
||||
val group = bot.getGroup(readGroup())
|
||||
|
||||
discardExact(1)
|
||||
val id = readUInt()
|
||||
val id = readQQ()
|
||||
if (id == bot.qqAccount) {
|
||||
discardExact(1)
|
||||
return BeingKickEvent(group, group.getMember(readUInt()))
|
||||
return BeingKickEvent(group, group.getMember(readQQ()))
|
||||
}
|
||||
|
||||
val member = group.getMember(id)
|
||||
|
||||
return when (val type = readUByte().toInt()) {
|
||||
0x02 -> MemberQuitEvent(member, _operator = null)
|
||||
0x03 -> MemberQuitEvent(member, _operator = group.getMember(readUInt()))
|
||||
else -> unsupported("Unsupported type " + type.toUHexString())
|
||||
0x03 -> MemberQuitEvent(member, _operator = group.getMember(readQQ()))
|
||||
else -> error("Unsupported type " + type.toUHexString())
|
||||
}
|
||||
|
||||
// 某群员主动离开, 群号 853343432
|
||||
@ -94,5 +91,4 @@ internal object MemberGoneEventPacketHandler : KnownEventParserAndHandler<Member
|
||||
// 00 30 32 33 32 63 32 39 36 65 36 35 64 62 64 64 64 64 65 35 62 33 34 64 36 62 34 33 32 61 30 64 61 65 32 30 37 35 38 34 37 34 32 65 32 39 63 35 63 64
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE", "MemberVisibilityCanBePrivate")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.event
|
||||
package net.mamoe.mirai.timpc.network.packet.event
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.discardExact
|
||||
@ -9,75 +9,13 @@ import kotlinx.io.core.readUInt
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.contact.Group
|
||||
import net.mamoe.mirai.contact.Member
|
||||
import net.mamoe.mirai.getGroup
|
||||
import net.mamoe.mirai.event.events.*
|
||||
import net.mamoe.mirai.qqAccount
|
||||
import net.mamoe.mirai.utils.io.debugPrintIfFail
|
||||
import net.mamoe.mirai.utils.io.readQQ
|
||||
import net.mamoe.mirai.utils.io.readRemainingBytes
|
||||
import net.mamoe.mirai.utils.io.toUHexString
|
||||
|
||||
// region mute
|
||||
/**
|
||||
* 某群成员被禁言事件
|
||||
*/
|
||||
@Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
class MemberMuteEvent(
|
||||
val member: Member,
|
||||
override val durationSeconds: Int,
|
||||
override val operator: Member
|
||||
) : MuteEvent() {
|
||||
override val group: Group get() = operator.group
|
||||
override fun toString(): String = "MemberMuteEvent(member=${member.id}, group=${group.id}, operator=${operator.id}, duration=${durationSeconds}s"
|
||||
}
|
||||
|
||||
/**
|
||||
* 机器人被禁言事件
|
||||
*/
|
||||
class BeingMutedEvent(
|
||||
override val durationSeconds: Int,
|
||||
override val operator: Member
|
||||
) : MuteEvent() {
|
||||
override val group: Group get() = operator.group
|
||||
override fun toString(): String = "BeingMutedEvent(group=${group.id}, operator=${operator.id}, duration=${durationSeconds}s"
|
||||
}
|
||||
|
||||
sealed class MuteEvent : EventOfMute() {
|
||||
abstract override val operator: Member
|
||||
abstract override val group: Group
|
||||
abstract val durationSeconds: Int
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region unmute
|
||||
/**
|
||||
* 某群成员被解除禁言事件
|
||||
*/
|
||||
@Suppress("unused")
|
||||
class MemberUnmuteEvent(
|
||||
val member: Member,
|
||||
override val operator: Member
|
||||
) : UnmuteEvent() {
|
||||
override val group: Group get() = operator.group
|
||||
override fun toString(): String = "MemberUnmuteEvent(member=${member.id}, group=${group.id}, operator=${operator.id}"
|
||||
}
|
||||
|
||||
/**
|
||||
* 机器人被解除禁言事件
|
||||
*/
|
||||
@Suppress("SpellCheckingInspection")
|
||||
class BeingUnmutedEvent(
|
||||
override val operator: Member
|
||||
) : UnmuteEvent() {
|
||||
override val group: Group get() = operator.group
|
||||
override fun toString(): String = "BeingUnmutedEvent(group=${group.id}, operator=${operator.id}"
|
||||
}
|
||||
|
||||
sealed class UnmuteEvent : EventOfMute() {
|
||||
abstract override val operator: Member
|
||||
abstract override val group: Group
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
internal class Unknown0x02DCPacketFlag0x0EMaybeMutePacket(
|
||||
val remaining: ByteArray
|
||||
) : EventOfMute() {
|
||||
@ -86,11 +24,6 @@ internal class Unknown0x02DCPacketFlag0x0EMaybeMutePacket(
|
||||
override fun toString(): String = "Unknown0x02DCPacketFlag0x0EMaybeMutePacket(remaining=${remaining.toUHexString()})"
|
||||
}
|
||||
|
||||
sealed class EventOfMute : EventPacket {
|
||||
abstract val operator: Member
|
||||
abstract val group: Group
|
||||
}
|
||||
|
||||
// TODO: 2019/12/14 这可能不只是禁言的包.
|
||||
internal object MemberMuteEventPacketParserAndHandler : KnownEventParserAndHandler<EventOfMute>(0x02DCu) {
|
||||
override suspend fun ByteReadPacket.parse(bot: Bot, identity: EventPacketIdentity): EventOfMute {
|
||||
@ -133,12 +66,12 @@ internal object MemberMuteEventPacketParserAndHandler : KnownEventParserAndHandl
|
||||
0x11u -> debugPrintIfFail("解析禁言包(0x02DC)时"){ // 猜测这个失败是撤回??
|
||||
discardExact(15)
|
||||
discardExact(2)
|
||||
val group = bot.getGroup(readUInt())
|
||||
val group = bot.getGroup(readQQ())
|
||||
discardExact(2)
|
||||
val operator = group.getMember(readUInt())
|
||||
val operator = group.getMember(readQQ())
|
||||
discardExact(4) //time
|
||||
discardExact(2)
|
||||
val memberQQ = readUInt()
|
||||
val memberQQ = readQQ()
|
||||
|
||||
val durationSeconds = readUInt().toInt()
|
||||
if (durationSeconds == 0) {
|
@ -1,18 +1,18 @@
|
||||
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.event
|
||||
package net.mamoe.mirai.timpc.network.packet.event
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.discardExact
|
||||
import kotlinx.io.core.readUInt
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.Packet
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.PacketVersion
|
||||
import net.mamoe.mirai.network.data.Packet
|
||||
import net.mamoe.mirai.timpc.network.packet.PacketVersion
|
||||
import net.mamoe.mirai.utils.io.readQQ
|
||||
|
||||
|
||||
data class MemberPermissionChangePacket(
|
||||
val groupId: UInt,
|
||||
val qq: UInt,
|
||||
val groupId: Long,
|
||||
val qq: Long,
|
||||
val kind: Kind
|
||||
) : Packet {
|
||||
enum class Kind {
|
||||
@ -35,7 +35,7 @@ internal object GroupMemberPermissionChangedEventFactory : KnownEventParserAndHa
|
||||
// 取消管理员
|
||||
// 00 00 00 08 00 0A 00 04 01 00 00 00 22 96 29 7B 01 00 76 E4 B8 DD 00
|
||||
discardExact(remaining - 5)
|
||||
val qq = readUInt()
|
||||
val qq = readQQ()
|
||||
val kind = when (readByte().toInt()) {
|
||||
0x00 -> MemberPermissionChangePacket.Kind.NO_LONGER_OPERATOR
|
||||
0x01 -> MemberPermissionChangePacket.Kind.BECOME_OPERATOR
|
@ -0,0 +1,103 @@
|
||||
package net.mamoe.mirai.timpc.network.packet.event
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.discardExact
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.contact.MemberPermission
|
||||
import net.mamoe.mirai.message.GroupMessage
|
||||
import net.mamoe.mirai.message.internal.readMessageChain
|
||||
import net.mamoe.mirai.message.FriendMessage
|
||||
import net.mamoe.mirai.timpc.network.packet.PacketVersion
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
|
||||
|
||||
@UseExperimental(ExperimentalUnsignedTypes::class)
|
||||
internal object GroupMessageEventParserAndHandler : KnownEventParserAndHandler<GroupMessage>(0x0052u) {
|
||||
|
||||
@PacketVersion(date = "2019.11.2", timVersion = "2.3.2 (21173)")
|
||||
override suspend fun ByteReadPacket.parse(bot: Bot, identity: EventPacketIdentity): GroupMessage {
|
||||
discardExact(31)
|
||||
val groupNumber = readGroup()
|
||||
discardExact(1)
|
||||
val qq = readQQ()
|
||||
|
||||
discardExact(48)
|
||||
readUShortLVByteArray()
|
||||
discardExact(2)//2个0x00
|
||||
|
||||
//debugPrintIfFail {
|
||||
val message = readMessageChain()
|
||||
|
||||
var senderPermission: MemberPermission = MemberPermission.MEMBER
|
||||
var senderName = ""
|
||||
val map = readTLVMap(true)
|
||||
if (map.containsKey(18u)) {
|
||||
map.getValue(18u).read {
|
||||
val tlv = readTLVMap(true)
|
||||
senderPermission = when (tlv.takeIf { it.containsKey(0x04u) }?.get(0x04u)?.getOrNull(3)?.toUInt()) {
|
||||
null -> MemberPermission.MEMBER
|
||||
0x08u -> MemberPermission.OWNER
|
||||
0x10u -> MemberPermission.ADMINISTRATOR
|
||||
else -> {
|
||||
tlv.printTLVMap("TLV(tag=18) Map")
|
||||
MiraiLogger.warning("Could not determine member permission, default permission MEMBER is being used")
|
||||
MemberPermission.MEMBER
|
||||
}
|
||||
}
|
||||
|
||||
senderName = when {
|
||||
tlv.containsKey(0x01u) -> kotlinx.io.core.String(tlv.getValue(0x01u))//这个人的qq昵称
|
||||
tlv.containsKey(0x02u) -> kotlinx.io.core.String(tlv.getValue(0x02u))//这个人的群名片
|
||||
else -> {
|
||||
tlv.printTLVMap("TLV(tag=18) Map")
|
||||
MiraiLogger.warning("Could not determine senderName")
|
||||
"null"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val group = bot.getGroup(groupNumber)
|
||||
return GroupMessage(
|
||||
bot = bot,
|
||||
group = group,
|
||||
senderName = senderName,
|
||||
permission = senderPermission,
|
||||
sender = group.getMember(qq),
|
||||
message = message
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region friend message
|
||||
|
||||
|
||||
@Suppress("unused")
|
||||
@UseExperimental(ExperimentalUnsignedTypes::class)
|
||||
internal object FriendMessageEventParserAndHandler : KnownEventParserAndHandler<FriendMessage>(0x00A6u) {
|
||||
|
||||
@PacketVersion(date = "2019.11.2", timVersion = "2.3.2 (21173)")
|
||||
override suspend fun ByteReadPacket.parse(bot: Bot, identity: EventPacketIdentity): FriendMessage {
|
||||
discardExact(2)
|
||||
val l1 = readShort()
|
||||
discardExact(1)//0x00
|
||||
val previous = readByte().toInt() == 0x08
|
||||
discardExact(l1.toInt() - 2)
|
||||
//java.io.EOFException: Only 49 bytes were discarded of 69 requested
|
||||
//抖动窗口消息
|
||||
discardExact(69)
|
||||
readUShortLVByteArray()//font
|
||||
discardExact(2)//2个0x00
|
||||
val message = readMessageChain()
|
||||
return FriendMessage(
|
||||
bot = bot,
|
||||
previous = previous,
|
||||
sender = bot.getQQ(identity.from),
|
||||
message = message
|
||||
)
|
||||
}
|
||||
}
|
||||
// endregion
|
@ -1,12 +1,13 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.event
|
||||
package net.mamoe.mirai.timpc.network.packet.event
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.readBytes
|
||||
import kotlinx.io.pool.useInstance
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.data.EventPacket
|
||||
import net.mamoe.mirai.utils.io.ByteArrayPool
|
||||
import net.mamoe.mirai.utils.io.toUHexString
|
||||
|
||||
@ -28,11 +29,11 @@ Mirai 21:54:15 : Packet received: UnknownEventPacket(id=00 57, identity=(9205034
|
||||
internal class UnknownEventParserAndHandler(override val id: UShort) : EventParserAndHandler<UnknownEventPacket> {
|
||||
|
||||
override suspend fun ByteReadPacket.parse(bot: Bot, identity: EventPacketIdentity): UnknownEventPacket {
|
||||
// MiraiLogger.debug("UnknownEventPacket(${id.toUHexString()}) = ${readBytes().toUHexString()}")
|
||||
// MiraiLogger.debug("UnknownEventPacket(${id.toHexString()}) = ${readBytes().toHexString()}")
|
||||
return UnknownEventPacket(id, identity, this) //TODO the cause is that `this` reference.
|
||||
}
|
||||
|
||||
override suspend fun BotNetworkHandler<*>.handlePacket(packet: UnknownEventPacket) {
|
||||
override suspend fun BotNetworkHandler.handlePacket(packet: UnknownEventPacket) {
|
||||
ByteArrayPool.useInstance {
|
||||
packet.body.readAvailable(it)
|
||||
bot.logger.debug("Unknown packet(${packet.id}) data = " + it.toUHexString())
|
@ -1,11 +1,12 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS", "unused", "FunctionName")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.login
|
||||
package net.mamoe.mirai.timpc.network.packet.login
|
||||
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.*
|
||||
import net.mamoe.mirai.network.data.Packet
|
||||
import net.mamoe.mirai.timpc.network.TIMProtocol
|
||||
import net.mamoe.mirai.timpc.network.packet.*
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
|
||||
internal object CaptchaKey : DecrypterByteArray, DecrypterType<CaptchaKey> {
|
||||
@ -17,7 +18,7 @@ internal object CaptchaPacket : PacketFactory<CaptchaPacket.CaptchaResponse, Cap
|
||||
* 请求验证码传输
|
||||
*/
|
||||
fun RequestTransmission(
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
token0825: ByteArray,
|
||||
captchaSequence: Int,
|
||||
token00BA: ByteArray
|
||||
@ -45,7 +46,7 @@ internal object CaptchaPacket : PacketFactory<CaptchaPacket.CaptchaResponse, Cap
|
||||
* 刷新验证码
|
||||
*/
|
||||
fun Refresh(
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
token0825: ByteArray
|
||||
): OutgoingPacket = buildOutgoingPacket(name = "CaptchaPacket.Refresh") {
|
||||
writeQQ(bot)
|
||||
@ -67,7 +68,7 @@ internal object CaptchaPacket : PacketFactory<CaptchaPacket.CaptchaResponse, Cap
|
||||
* 提交验证码
|
||||
*/
|
||||
fun Submit(
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
token0825: ByteArray,
|
||||
captcha: String,
|
||||
captchaToken: IoBuffer
|
||||
@ -125,7 +126,7 @@ internal object CaptchaPacket : PacketFactory<CaptchaPacket.CaptchaResponse, Cap
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): CaptchaResponse =
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): CaptchaResponse =
|
||||
when (val flag = readByte().toUInt()) {
|
||||
0x14u -> {//00 05 00 00 00 00 00 00 38
|
||||
CaptchaResponse.Correct().apply {
|
@ -1,15 +1,15 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.login
|
||||
package net.mamoe.mirai.timpc.network.packet.login
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.writeFully
|
||||
import kotlinx.io.core.writeUByte
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.*
|
||||
import net.mamoe.mirai.network.data.Packet
|
||||
import net.mamoe.mirai.timpc.network.TIMProtocol
|
||||
import net.mamoe.mirai.timpc.network.packet.*
|
||||
import net.mamoe.mirai.utils.OnlineStatus
|
||||
import net.mamoe.mirai.utils.io.encryptAndWrite
|
||||
import net.mamoe.mirai.utils.io.writeHex
|
||||
import net.mamoe.mirai.utils.io.writeQQ
|
||||
|
||||
@ -18,7 +18,7 @@ import net.mamoe.mirai.utils.io.writeQQ
|
||||
*/
|
||||
internal object ChangeOnlineStatusPacket : PacketFactory<ChangeOnlineStatusPacket.ChangeOnlineStatusResponse, NoDecrypter>(NoDecrypter) {
|
||||
operator fun invoke(
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
sessionKey: SessionKey,
|
||||
loginStatus: OnlineStatus
|
||||
): OutgoingPacket = buildOutgoingPacket {
|
||||
@ -31,10 +31,10 @@ internal object ChangeOnlineStatusPacket : PacketFactory<ChangeOnlineStatusPacke
|
||||
}
|
||||
}
|
||||
|
||||
internal object ChangeOnlineStatusResponse : Packet {
|
||||
internal object ChangeOnlineStatusResponse : Packet {
|
||||
override fun toString(): String = this::class.simpleName!!
|
||||
}
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): ChangeOnlineStatusResponse =
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): ChangeOnlineStatusResponse =
|
||||
ChangeOnlineStatusResponse
|
||||
}
|
@ -1,21 +1,21 @@
|
||||
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.login
|
||||
package net.mamoe.mirai.timpc.network.packet.login
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.writeFully
|
||||
import net.mamoe.mirai.event.Subscribable
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.*
|
||||
import net.mamoe.mirai.utils.io.encryptAndWrite
|
||||
import net.mamoe.mirai.network.data.Packet
|
||||
import net.mamoe.mirai.timpc.network.TIMProtocol
|
||||
import net.mamoe.mirai.timpc.network.packet.*
|
||||
import net.mamoe.mirai.utils.io.writeHex
|
||||
import net.mamoe.mirai.utils.io.writeQQ
|
||||
|
||||
@NoLog
|
||||
internal object HeartbeatPacket : SessionPacketFactory<HeartbeatPacketResponse>() {
|
||||
operator fun invoke(
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
sessionKey: SessionKey
|
||||
): OutgoingPacket = buildOutgoingPacket {
|
||||
writeQQ(bot)
|
||||
@ -25,7 +25,7 @@ internal object HeartbeatPacket : SessionPacketFactory<HeartbeatPacketResponse>(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): HeartbeatPacketResponse =
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): HeartbeatPacketResponse =
|
||||
HeartbeatPacketResponse
|
||||
}
|
||||
|
@ -1,17 +1,16 @@
|
||||
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.login
|
||||
package net.mamoe.mirai.timpc.network.packet.login
|
||||
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.contact.data.Gender
|
||||
import net.mamoe.mirai.network.data.Gender
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.*
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
import net.mamoe.mirai.utils.decryptBy
|
||||
import net.mamoe.mirai.utils.encryptBy
|
||||
import net.mamoe.mirai.network.data.LoginResult
|
||||
import net.mamoe.mirai.network.data.Packet
|
||||
import net.mamoe.mirai.timpc.network.TIMProtocol
|
||||
import net.mamoe.mirai.timpc.network.packet.*
|
||||
import net.mamoe.mirai.utils.*
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
import net.mamoe.mirai.utils.writeCRC32
|
||||
|
||||
internal object ShareKey : DecrypterByteArray, DecrypterType<ShareKey> {
|
||||
override val value: ByteArray = TIMProtocol.shareKey
|
||||
@ -46,7 +45,7 @@ internal inline class SubmitPasswordResponseDecrypter(private val privateKey: Pr
|
||||
*/
|
||||
internal object SubmitPasswordPacket : PacketFactory<SubmitPasswordPacket.LoginResponse, SubmitPasswordResponseDecrypter>(SubmitPasswordResponseDecrypter) {
|
||||
operator fun invoke(
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
password: String,
|
||||
loginTime: Int,
|
||||
loginIP: String,
|
||||
@ -109,7 +108,7 @@ internal object SubmitPasswordPacket : PacketFactory<SubmitPasswordPacket.LoginR
|
||||
data class Failed(val result: LoginResult) : LoginResponse()
|
||||
}
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): LoginResponse {
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): LoginResponse {
|
||||
val size = remaining.toInt()
|
||||
return when {
|
||||
size == 229 || size == 271 || size == 207 || size == 165 /* TODO CHECK 165 */ -> {
|
||||
@ -267,7 +266,7 @@ internal inline class SessionResponseDecryptionKey(private val delegate: IoBuffe
|
||||
}
|
||||
|
||||
private fun BytePacketBuilder.writePart1(
|
||||
qq: UInt,
|
||||
qq: Long,
|
||||
password: String,
|
||||
loginTime: Int,
|
||||
loginIP: String,
|
||||
@ -296,7 +295,7 @@ private fun BytePacketBuilder.writePart1(
|
||||
this.writeFully(TIMProtocol.passwordSubmissionTLV2)
|
||||
this.writeHex("00 1A")//tag
|
||||
this.writeHex("00 40")//length
|
||||
this.writeFully(TIMProtocol.passwordSubmissionTLV2.encryptBy(privateKey))
|
||||
this.writeFully(TIMProtocol.passwordSubmissionTLV2.encryptBy(privateKey.value))
|
||||
this.writeFully(TIMProtocol.constantData1)
|
||||
this.writeFully(TIMProtocol.constantData2)
|
||||
this.writeQQ(qq)
|
||||
@ -310,6 +309,29 @@ private fun BytePacketBuilder.writePart1(
|
||||
this.writeHex("60 C9 5D A7 45 70 04 7F 21 7D 84 50 5C 66 A5 C6")//key
|
||||
}
|
||||
|
||||
private fun BytePacketBuilder.writeTLV0006(qq: Long, password: String, loginTime: Int, loginIP: String, privateKey: PrivateKey) {
|
||||
val firstMD5 = md5(password)
|
||||
val secondMD5 = md5(firstMD5 + byteArrayOf(0, 0, 0, 0) + qq.toUInt().toByteArray())
|
||||
|
||||
this.encryptAndWrite(secondMD5) {
|
||||
writeRandom(4)
|
||||
writeHex("00 02")
|
||||
writeQQ(qq)
|
||||
writeFully(TIMProtocol.constantData2)
|
||||
writeHex("00 00 01")
|
||||
|
||||
writeFully(firstMD5)
|
||||
writeInt(loginTime)
|
||||
writeByte(0)
|
||||
writeZero(4 * 3)
|
||||
writeIP(loginIP)
|
||||
writeZero(8)
|
||||
writeHex("00 10")//这两个hex是passwordSubmissionTLV2的末尾
|
||||
writeHex("15 74 C4 89 85 7A 19 F5 5E A9 C9 A3 5E 8A 5A 9B")//16
|
||||
writeFully(privateKey.value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun BytePacketBuilder.writePart2() {
|
||||
|
||||
this.writeHex("03 12")//tag
|
@ -1,19 +1,15 @@
|
||||
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE", "FunctionName")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.login
|
||||
package net.mamoe.mirai.timpc.network.packet.login
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.discardExact
|
||||
import kotlinx.io.core.writeFully
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.BotSession
|
||||
import net.mamoe.mirai.network.protocol.timpc.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.*
|
||||
import net.mamoe.mirai.network.qqAccount
|
||||
import net.mamoe.mirai.network.data.Packet
|
||||
import net.mamoe.mirai.timpc.network.TIMProtocol
|
||||
import net.mamoe.mirai.timpc.network.packet.*
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
import net.mamoe.mirai.withSession
|
||||
|
||||
internal fun BotSession.RequestSKeyPacket(): OutgoingPacket = RequestSKeyPacket(qqAccount, sessionKey)
|
||||
|
||||
internal inline class SKey(
|
||||
val value: String
|
||||
@ -25,7 +21,7 @@ internal inline class SKey(
|
||||
*/
|
||||
internal object RequestSKeyPacket : SessionPacketFactory<SKey>() {
|
||||
operator fun invoke(
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
sessionKey: SessionKey
|
||||
): OutgoingPacket = buildOutgoingPacket {
|
||||
writeQQ(bot)
|
||||
@ -35,7 +31,7 @@ internal object RequestSKeyPacket : SessionPacketFactory<SKey>() {
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): SKey {
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): SKey {
|
||||
//11 00 97 D7 0F 1C FD 50 7A 41 DD 4D 66 93 EF 8C 85 D1 84 3D 66 95 9D E5 B4 96 A5 E3 92 37 28 D8 80 DA EF 8C 85 D1 84 3D 66 95 9D E5 B4 96 A5 E3 92 37 28 D8 80 DA
|
||||
|
||||
discardExact(4)
|
||||
@ -44,9 +40,9 @@ internal object RequestSKeyPacket : SessionPacketFactory<SKey>() {
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun BotNetworkHandler<*>.handlePacket(packet: SKey) = bot.withSession {
|
||||
_sKey = packet.value
|
||||
_cookies = "uin=o$qqAccount;skey=$sKey;"
|
||||
override suspend fun BotNetworkHandler.handlePacket(packet: SKey) {
|
||||
// _sKey = packet.value
|
||||
// _cookies = "uin=o$qqAccount;skey=$sKey;"
|
||||
|
||||
// TODO: 2019/11/27 SKEY 实现
|
||||
/*
|
@ -1,17 +1,18 @@
|
||||
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.login
|
||||
package net.mamoe.mirai.timpc.network.packet.login
|
||||
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.*
|
||||
import net.mamoe.mirai.network.data.Packet
|
||||
import net.mamoe.mirai.timpc.network.TIMProtocol
|
||||
import net.mamoe.mirai.timpc.network.packet.*
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
import net.mamoe.mirai.utils.localIpAddress
|
||||
|
||||
internal object RequestSessionPacket : PacketFactory<RequestSessionPacket.SessionKeyResponse, SessionResponseDecryptionKey>(SessionResponseDecryptionKey) {
|
||||
operator fun invoke(
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
serverIp: String,
|
||||
token38: IoBuffer,
|
||||
token88: IoBuffer,
|
||||
@ -65,7 +66,7 @@ internal object RequestSessionPacket : PacketFactory<RequestSessionPacket.Sessio
|
||||
override fun toString(): String = "SessionKeyResponse"
|
||||
}
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): SessionKeyResponse {
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): SessionKeyResponse {
|
||||
when (remaining) {
|
||||
407L -> {
|
||||
discardExact(25)//todo test
|
@ -1,14 +1,15 @@
|
||||
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.login
|
||||
package net.mamoe.mirai.timpc.network.packet.login
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.discardExact
|
||||
import kotlinx.io.core.readBytes
|
||||
import kotlinx.io.core.writeFully
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.*
|
||||
import net.mamoe.mirai.network.data.Packet
|
||||
import net.mamoe.mirai.timpc.network.TIMProtocol
|
||||
import net.mamoe.mirai.timpc.network.packet.*
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
|
||||
internal object TouchKey : DecrypterByteArray, DecrypterType<TouchKey> {
|
||||
@ -22,7 +23,7 @@ internal object TouchKey : DecrypterByteArray, DecrypterType<TouchKey> {
|
||||
*/
|
||||
internal object TouchPacket : PacketFactory<TouchPacket.TouchResponse, TouchKey>(TouchKey) {
|
||||
operator fun invoke(
|
||||
bot: UInt,
|
||||
bot: Long,
|
||||
serverIp: String,
|
||||
isRedirect: Boolean
|
||||
): OutgoingPacket = buildOutgoingPacket {
|
||||
@ -60,7 +61,7 @@ internal object TouchPacket : PacketFactory<TouchPacket.TouchResponse, TouchKey>
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): TouchResponse {
|
||||
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler): TouchResponse {
|
||||
when (val flag = readByte().toUByte().toInt()) {
|
||||
0xFE -> {
|
||||
discardExact(94)
|
@ -0,0 +1,7 @@
|
||||
package net.mamoe.mirai.timpc.utils
|
||||
|
||||
/**
|
||||
* 表示这里是不可到达的位置.
|
||||
*/
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
internal inline fun assertUnreachable(): Nothing = error("This clause should not be reached")
|
@ -0,0 +1,11 @@
|
||||
package net.mamoe.mirai.timpc.utils
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.readBytes
|
||||
import net.mamoe.mirai.utils.io.toUHexString
|
||||
|
||||
internal fun ByteReadPacket.unsupportedFlag(name: String, flag: String): Nothing =
|
||||
error("Unsupported flag of $name. flag=$flag, remaining=${readBytes().toUHexString()}")
|
||||
|
||||
internal fun ByteReadPacket.unsupportedType(name: String, type: String): Nothing =
|
||||
error("Unsupported type of $name. type=$type, remaining=${readBytes().toUHexString()}")
|
@ -0,0 +1,30 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package net.mamoe.mirai.timpc
|
||||
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.io.InputStream
|
||||
import kotlinx.io.core.use
|
||||
import kotlinx.io.streams.inputStream
|
||||
import net.mamoe.mirai.BotAccount
|
||||
import net.mamoe.mirai.message.data.Image
|
||||
import net.mamoe.mirai.utils.ExternalImage
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
import net.mamoe.mirai.utils.toExternalImage
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.File
|
||||
import javax.imageio.ImageIO
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
internal actual class TIMPCBot actual constructor(
|
||||
account: BotAccount,
|
||||
logger: MiraiLogger?,
|
||||
context: CoroutineContext
|
||||
) : TIMPCBotBase(account, logger, context) {
|
||||
suspend inline fun Image.downloadAsStream(): InputStream = download().inputStream()
|
||||
suspend inline fun Image.downloadAsBufferedImage(): BufferedImage = withContext(IO) { downloadAsStream().use { ImageIO.read(it) } }
|
||||
suspend inline fun Image.downloadAsExternalImage(): ExternalImage = download().use { it.toExternalImage() }
|
||||
|
||||
suspend inline fun Image.downloadTo(file: File) = file.outputStream().use { downloadTo(it) }
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package net.mamoe.mirai.timpc.network
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* 包处理协程调度器.
|
||||
*
|
||||
* JVM: 独立的 4 thread 调度器
|
||||
*/
|
||||
internal actual val NetworkDispatcher: CoroutineDispatcher
|
||||
get() = Executors.newFixedThreadPool(4).asCoroutineDispatcher()
|
@ -1,14 +1,12 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package mirai.test
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.login
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.login.LoginResult
|
||||
import net.mamoe.mirai.network.data.LoginResult
|
||||
import net.mamoe.mirai.timpc.TIMPC
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@ -44,8 +42,8 @@ suspend fun main() {
|
||||
.map { Pair(it[0].toLong(), it[1]) }
|
||||
.forEach { (qq, password) ->
|
||||
runBlocking {
|
||||
val bot = Bot(
|
||||
qq.toUInt(),
|
||||
val bot = TIMPC.Bot(
|
||||
qq,
|
||||
if (password.endsWith(".")) password.substring(0, password.length - 1) else password
|
||||
)
|
||||
|
@ -1,29 +1,28 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "MemberVisibilityCanBePrivate", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package mirai.test.packetdebugger
|
||||
|
||||
import PacketDebugger.dataReceived
|
||||
import PacketDebugger.dataSent
|
||||
import PacketDebugger.qq
|
||||
import PacketDebugger.sessionKey
|
||||
import io.ktor.util.date.GMTDate
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.io.core.*
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.discardExact
|
||||
import kotlinx.io.core.readBytes
|
||||
import kotlinx.io.core.readUShort
|
||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.internal.ArrayListSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import mirai.test.packetdebugger.PacketDebugger.dataReceived
|
||||
import mirai.test.packetdebugger.PacketDebugger.dataSent
|
||||
import mirai.test.packetdebugger.PacketDebugger.qq
|
||||
import mirai.test.packetdebugger.PacketDebugger.sessionKey
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.BotSession
|
||||
import net.mamoe.mirai.network.protocol.timpc.TIMProtocol
|
||||
import net.mamoe.mirai.network.protocol.timpc.handler.DataPacketSocketAdapter
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.*
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.event.IgnoredEventPacket
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.login.CaptchaKey
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.login.LoginResult
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.login.ShareKey
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.login.TouchKey
|
||||
import net.mamoe.mirai.timpc.TIMPC
|
||||
import net.mamoe.mirai.timpc.network.TIMProtocol
|
||||
import net.mamoe.mirai.timpc.network.packet.*
|
||||
import net.mamoe.mirai.timpc.network.packet.event.IgnoredEventPacket
|
||||
import net.mamoe.mirai.timpc.network.packet.login.CaptchaKey
|
||||
import net.mamoe.mirai.timpc.network.packet.login.ShareKey
|
||||
import net.mamoe.mirai.timpc.network.packet.login.TouchKey
|
||||
import net.mamoe.mirai.utils.DecryptionFailedException
|
||||
import net.mamoe.mirai.utils.decryptBy
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
@ -37,7 +36,6 @@ import java.nio.charset.Charset
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.io.use
|
||||
|
||||
/**
|
||||
* 避免 print 重叠. 单线程处理足够调试
|
||||
@ -189,7 +187,7 @@ internal object PacketDebugger {
|
||||
/**
|
||||
* null 则不筛选
|
||||
*/
|
||||
val qq: UInt? = 761025446u
|
||||
val qq: Long? = 761025446
|
||||
/**
|
||||
* 打开后则记录每一个包到文件.
|
||||
*/
|
||||
@ -208,7 +206,7 @@ internal object PacketDebugger {
|
||||
discardExact(3)
|
||||
val id = matchPacketId(readUShort())
|
||||
val sequenceId = readUShort()
|
||||
val packetQQ = readUInt()
|
||||
val packetQQ = readQQ()
|
||||
if (id == KnownPacketId.HEARTBEAT || (qq != null && packetQQ != qq))
|
||||
return@read
|
||||
|
||||
@ -308,7 +306,7 @@ internal object PacketDebugger {
|
||||
if (IgnoredPacketIdList.contains(id)) {
|
||||
return
|
||||
}
|
||||
val packetQQ = readUInt()
|
||||
val packetQQ = readQQ()
|
||||
if (qq != null && packetQQ != qq) {
|
||||
return@read
|
||||
}
|
||||
@ -378,27 +376,12 @@ when (idHex.substring(0, 5)) {
|
||||
}
|
||||
|
||||
|
||||
internal object DebugNetworkHandler : BotNetworkHandler<DataPacketSocketAdapter>, CoroutineScope {
|
||||
internal object DebugNetworkHandler : BotNetworkHandler(), CoroutineScope {
|
||||
override val supervisor: CompletableJob = SupervisorJob()
|
||||
override val socket: DataPacketSocketAdapter = object : DataPacketSocketAdapter {
|
||||
override val serverIp: String
|
||||
get() = ""
|
||||
override val channel: PlatformDatagramChannel
|
||||
get() = error("UNSUPPORTED")
|
||||
override val isOpen: Boolean
|
||||
get() = true
|
||||
|
||||
override fun close() {
|
||||
}
|
||||
override val bot: Bot = TIMPC.run { this@DebugNetworkHandler.Bot(qq ?: 0L, "", null) }
|
||||
|
||||
override val owner: Bot
|
||||
get() = bot
|
||||
|
||||
}
|
||||
override val bot: Bot = Bot(qq ?: 0u, "", coroutineContext)
|
||||
override val session = BotSession(bot, SessionKey(byteArrayOf()))
|
||||
|
||||
override suspend fun login(): LoginResult = LoginResult.SUCCESS
|
||||
override suspend fun login() {}
|
||||
|
||||
override suspend fun awaitDisconnection() {
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
package mirai.test.packetdebugger
|
||||
package packetdebugger
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.discardExact
|
||||
import kotlinx.io.core.readUShort
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.PacketId
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.login.CaptchaPacket
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.matchPacketId
|
||||
import net.mamoe.mirai.timpc.network.packet.PacketId
|
||||
import net.mamoe.mirai.timpc.network.packet.matchPacketId
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
3
mirai-core-timpc/src/main/AndroidManifest.xml
Normal file
3
mirai-core-timpc/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="net.mamoe.mirai.timpc">
|
||||
</manifest>
|
@ -1,5 +1,6 @@
|
||||
package net.mamoe.mirai.network.protocol.timpc.packet.event
|
||||
package net.mamoe.mirai.message
|
||||
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.contact.Contact
|
||||
import net.mamoe.mirai.contact.QQ
|
||||
import net.mamoe.mirai.utils.MiraiInternalAPI
|
||||
@ -8,7 +9,7 @@ import net.mamoe.mirai.utils.MiraiInternalAPI
|
||||
* 平台相关扩展
|
||||
*/
|
||||
@UseExperimental(MiraiInternalAPI::class)
|
||||
actual abstract class MessagePacket<TSender : QQ, TSubject : Contact> : MessagePacketBase<TSender, TSubject>() {
|
||||
actual abstract class MessagePacket<TSender : QQ, TSubject : Contact> actual constructor(bot: Bot) : MessagePacketBase<TSender, TSubject>(bot) {
|
||||
// suspend inline fun uploadImage(image: Bitmap): Image = subject.uploadImage(image)
|
||||
//suspend inline fun uploadImage(image: URL): Image = subject.uploadImage(image)
|
||||
//suspend inline fun uploadImage(image: Input): Image = subject.uploadImage(image)
|
@ -1,30 +0,0 @@
|
||||
package net.mamoe.mirai.network
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.io.core.use
|
||||
import kotlinx.io.streams.inputStream
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.message.Image
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.SessionKey
|
||||
import net.mamoe.mirai.utils.MiraiInternalAPI
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* Android 平台相关扩展. 详情查看 [BotSessionBase]
|
||||
*
|
||||
* @author Him188moe
|
||||
*/
|
||||
@UseExperimental(MiraiInternalAPI::class)
|
||||
actual class BotSession internal actual constructor(
|
||||
bot: Bot,
|
||||
sessionKey: SessionKey
|
||||
) : BotSessionBase(bot, sessionKey) {
|
||||
|
||||
suspend inline fun Image.downloadAsStream(): InputStream = download().inputStream()
|
||||
suspend inline fun Image.downloadAsBitmap(): Bitmap = withContext(Dispatchers.IO) { downloadAsStream().use { BitmapFactory.decodeStream(it) } }
|
||||
//suspend inline fun Image.downloadAsExternalImage(): ExternalImage = download().use { it.toExternalImage() }
|
||||
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
package net.mamoe.mirai.network
|
||||
|
@ -3,8 +3,6 @@ package net.mamoe.mirai.utils
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.util.KtorExperimentalAPI
|
||||
import kotlinx.io.core.IoBuffer
|
||||
import kotlinx.io.core.readBytes
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.DataInput
|
||||
import java.io.EOFException
|
||||
@ -24,7 +22,7 @@ actual val deviceName: String get() = InetAddress.getLocalHost().hostName
|
||||
* Ktor HttpClient. 不同平台使用不同引擎.
|
||||
*/
|
||||
@KtorExperimentalAPI
|
||||
internal actual val Http: HttpClient
|
||||
actual val Http: HttpClient
|
||||
get() = HttpClient(CIO)
|
||||
|
||||
/**
|
||||
|
8
mirai-core/src/androidTest/java/ECDHTest.kt
Normal file
8
mirai-core/src/androidTest/java/ECDHTest.kt
Normal file
@ -0,0 +1,8 @@
|
||||
import net.mamoe.mirai.timpc.network.TIMProtocol
|
||||
import net.mamoe.mirai.utils.io.toUHexString
|
||||
|
||||
|
||||
fun main() {
|
||||
|
||||
println(EcdhCrypt().calShareKeyMd5ByPeerPublicKey(TIMProtocol.publicKey).toUHexString())
|
||||
}
|
224
mirai-core/src/androidTest/java/EcdhCrypt.java
Normal file
224
mirai-core/src/androidTest/java/EcdhCrypt.java
Normal file
@ -0,0 +1,224 @@
|
||||
import net.mamoe.mirai.utils.PlatformUtilsAndroidKt;
|
||||
|
||||
import javax.crypto.KeyAgreement;
|
||||
import java.security.*;
|
||||
import java.security.spec.ECGenParameterSpec;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
|
||||
public class EcdhCrypt {
|
||||
public static final String DEFAULT_PUB_KEY = "020b03cf3d99541f29ffec281bebbd4ea211292ac1f53d7128";
|
||||
public static final String DEFAULT_SHARE_KEY = "4da0f614fc9f29c2054c77048a6566d7";
|
||||
public static final String S_PUB_KEY = "04928D8850673088B343264E0C6BACB8496D697799F37211DEB25BB73906CB089FEA9639B4E0260498B51A992D50813DA8";
|
||||
public static final String X509_S_PUB_KEY = "3046301006072A8648CE3D020106052B8104001F03320004928D8850673088B343264E0C6BACB8496D697799F37211DEB25BB73906CB089FEA9639B4E0260498B51A992D50813DA8";
|
||||
public static byte[] _c_pri_key;
|
||||
public static byte[] _c_pub_key;
|
||||
private static byte[] _g_share_key;
|
||||
private static boolean initFlg;
|
||||
public static PrivateKey pkcs8PrivateKey;
|
||||
private static boolean userOpenSSLLib;
|
||||
public static PublicKey x509PublicKey;
|
||||
|
||||
static {
|
||||
EcdhCrypt.initFlg = false;
|
||||
EcdhCrypt.userOpenSSLLib = true;
|
||||
EcdhCrypt._c_pub_key = new byte[0];
|
||||
EcdhCrypt._c_pri_key = new byte[0];
|
||||
EcdhCrypt._g_share_key = new byte[0];
|
||||
}
|
||||
|
||||
public EcdhCrypt() {
|
||||
/// util.loadLibrary("wtecdh", context);
|
||||
}
|
||||
|
||||
public static String buf_to_string(final byte[] array) {
|
||||
String s;
|
||||
if (array == null) {
|
||||
s = "";
|
||||
} else {
|
||||
String string = "";
|
||||
int n = 0;
|
||||
while (true) {
|
||||
s = string;
|
||||
if (n >= array.length) {
|
||||
break;
|
||||
}
|
||||
string = string + Integer.toHexString(array[n] >> 4 & 0xF) + Integer.toHexString(array[n] & 0xF);
|
||||
++n;
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
private byte[] calShareKeyByBouncycastle(final byte[] array) {
|
||||
String str = "3046301006072A8648CE3D020106052B8104001F03320004";
|
||||
try {
|
||||
if (array.length < 30) {
|
||||
str = "302E301006072A8648CE3D020106052B8104001F031A00";
|
||||
}
|
||||
final PublicKey constructX509PublicKey = this.constructX509PublicKey(str + buf_to_string(array));
|
||||
final KeyAgreement instance = KeyAgreement.getInstance("ECDH", "BC");
|
||||
instance.init(EcdhCrypt.pkcs8PrivateKey);
|
||||
instance.doPhase(constructX509PublicKey, true);
|
||||
final byte[] generateSecret = instance.generateSecret();
|
||||
return PlatformUtilsAndroidKt.md5(generateSecret);
|
||||
} catch (ExceptionInInitializerError | Exception exceptionInInitializerError) {
|
||||
exceptionInInitializerError.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] calShareKeyByOpenSSL(final String s, final String str, final String s2) {
|
||||
//if (this.GenECDHKeyEx(s2, str, s) == 0) {
|
||||
return EcdhCrypt._g_share_key;
|
||||
//}
|
||||
// return null;
|
||||
}
|
||||
|
||||
private PublicKey constructX509PublicKey(final String str) throws NoSuchProviderException, NoSuchAlgorithmException, InvalidKeySpecException {
|
||||
return KeyFactory.getInstance("EC", "BC").generatePublic(new X509EncodedKeySpec(string_to_buf(str)));
|
||||
}
|
||||
|
||||
public static byte[] string_to_buf(final String s) {
|
||||
int i = 0;
|
||||
if (s == null) {
|
||||
return new byte[0];
|
||||
}
|
||||
final byte[] array = new byte[s.length() / 2];
|
||||
while (i < s.length() / 2) {
|
||||
array[i] = (byte) ((get_char((byte) s.charAt(i * 2)) << 4) + get_char((byte) s.charAt(i * 2 + 1)));
|
||||
++i;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
public static byte get_char(final byte b) {
|
||||
if (b >= 48 && b <= 57) {
|
||||
return (byte) (b - 48);
|
||||
}
|
||||
if (b >= 97 && b <= 102) {
|
||||
return (byte) (b - 97 + 10);
|
||||
}
|
||||
if (b >= 65 && b <= 70) {
|
||||
return (byte) (b - 65 + 10);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int initShareKeyByBouncycastle() {
|
||||
try {
|
||||
final KeyPairGenerator instance = KeyPairGenerator.getInstance("EC", "BC");
|
||||
instance.initialize(new ECGenParameterSpec("secp192k1"));
|
||||
final KeyPair genKeyPair = instance.genKeyPair();
|
||||
final PublicKey public1 = genKeyPair.getPublic();
|
||||
final byte[] encoded = public1.getEncoded();
|
||||
final PrivateKey private1 = genKeyPair.getPrivate();
|
||||
private1.getEncoded();
|
||||
final PublicKey constructX509PublicKey = this.constructX509PublicKey("3046301006072A8648CE3D020106052B8104001F03320004928D8850673088B343264E0C6BACB8496D697799F37211DEB25BB73906CB089FEA9639B4E0260498B51A992D50813DA8");
|
||||
final KeyAgreement instance2 = KeyAgreement.getInstance("ECDH", "BC");
|
||||
instance2.init(private1);
|
||||
instance2.doPhase(constructX509PublicKey, true);
|
||||
EcdhCrypt._g_share_key = PlatformUtilsAndroidKt.md5(instance2.generateSecret());
|
||||
System.arraycopy(encoded, 23, EcdhCrypt._c_pub_key = new byte[49], 0, 49);
|
||||
EcdhCrypt.x509PublicKey = public1;
|
||||
EcdhCrypt.pkcs8PrivateKey = private1;
|
||||
return 0;
|
||||
} catch (ExceptionInInitializerError exceptionInInitializerError) {
|
||||
exceptionInInitializerError.printStackTrace();
|
||||
return -1;
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
|
||||
private int initShareKeyByOpenSSL() {
|
||||
// if (Build$VERSION.SDK_INT >= 23 || this.GenereateKey() != 0) {
|
||||
// return -1;
|
||||
// }
|
||||
if (EcdhCrypt._c_pub_key == null || EcdhCrypt._c_pub_key.length == 0 || EcdhCrypt._c_pri_key == null || EcdhCrypt._c_pri_key.length == 0 || EcdhCrypt._g_share_key == null || EcdhCrypt._g_share_key.length == 0) {
|
||||
return -2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public native int GenECDHKeyEx(final String p0, final String p1, final String p2);
|
||||
|
||||
public int GenereateKey() {
|
||||
try {
|
||||
synchronized (EcdhCrypt.class) {
|
||||
return this.GenECDHKeyEx("04928D8850673088B343264E0C6BACB8496D697799F37211DEB25BB73906CB089FEA9639B4E0260498B51A992D50813DA8", "", "");
|
||||
}
|
||||
} catch (UnsatisfiedLinkError unsatisfiedLinkError) {
|
||||
unsatisfiedLinkError.printStackTrace();
|
||||
return -1;
|
||||
} catch (RuntimeException ex) {
|
||||
return -2;
|
||||
} catch (Exception ex2) {
|
||||
return -3;
|
||||
} catch (Error error) {
|
||||
return -4;
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] calShareKeyMd5ByPeerPublicKey(final byte[] array) {
|
||||
if (EcdhCrypt.userOpenSSLLib) {
|
||||
return this.calShareKeyByOpenSSL(buf_to_string(EcdhCrypt._c_pri_key), buf_to_string(EcdhCrypt._c_pub_key), buf_to_string(array));
|
||||
}
|
||||
return this.calShareKeyByBouncycastle(array);
|
||||
}
|
||||
|
||||
public byte[] get_c_pub_key() {
|
||||
return EcdhCrypt._c_pub_key.clone();
|
||||
}
|
||||
|
||||
public byte[] get_g_share_key() {
|
||||
return EcdhCrypt._g_share_key.clone();
|
||||
}
|
||||
|
||||
public int initShareKey() {
|
||||
if (EcdhCrypt.initFlg) {
|
||||
return 0;
|
||||
}
|
||||
EcdhCrypt.initFlg = true;
|
||||
if (this.initShareKeyByOpenSSL() == 0) {
|
||||
EcdhCrypt.userOpenSSLLib = true;
|
||||
return 0;
|
||||
}
|
||||
if (this.initShareKeyByBouncycastle() == 0) {
|
||||
EcdhCrypt.userOpenSSLLib = false;
|
||||
return 0;
|
||||
}
|
||||
return this.initShareKeyByDefault();
|
||||
}
|
||||
|
||||
public int initShareKeyByDefault() {
|
||||
// EcdhCrypt._c_pub_key = util.string_to_buf("020b03cf3d99541f29ffec281bebbd4ea211292ac1f53d7128");
|
||||
// EcdhCrypt._g_share_key = util.string_to_buf("4da0f614fc9f29c2054c77048a6566d7");
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void set_c_pri_key(final byte[] array) {
|
||||
if (array != null) {
|
||||
EcdhCrypt._c_pri_key = array.clone();
|
||||
return;
|
||||
}
|
||||
EcdhCrypt._c_pri_key = new byte[0];
|
||||
}
|
||||
|
||||
public void set_c_pub_key(final byte[] array) {
|
||||
if (array != null) {
|
||||
EcdhCrypt._c_pub_key = array.clone();
|
||||
return;
|
||||
}
|
||||
EcdhCrypt._c_pub_key = new byte[0];
|
||||
}
|
||||
|
||||
public void set_g_share_key(final byte[] array) {
|
||||
if (array != null) {
|
||||
EcdhCrypt._g_share_key = array.clone();
|
||||
return;
|
||||
}
|
||||
EcdhCrypt._g_share_key = new byte[0];
|
||||
}
|
||||
}
|
@ -1,20 +1,23 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "unused", "FunctionName", "NOTHING_TO_INLINE")
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "unused", "FunctionName")
|
||||
|
||||
package net.mamoe.mirai
|
||||
|
||||
import io.ktor.util.KtorExperimentalAPI
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.io.OutputStream
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.use
|
||||
import net.mamoe.mirai.contact.*
|
||||
import net.mamoe.mirai.message.data.Image
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.login.LoginResult
|
||||
import net.mamoe.mirai.network.data.AddFriendResult
|
||||
import net.mamoe.mirai.network.data.ImageLink
|
||||
import net.mamoe.mirai.utils.BotConfiguration
|
||||
import net.mamoe.mirai.utils.GroupNotFoundException
|
||||
import net.mamoe.mirai.utils.MiraiInternalAPI
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
import net.mamoe.mirai.utils.io.transferTo
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.coroutineContext
|
||||
import kotlin.jvm.JvmSynthetic
|
||||
|
||||
|
||||
/**
|
||||
* Mirai 的机器人. 一个机器人实例登录一个 QQ 账号.
|
||||
@ -22,77 +25,60 @@ import kotlin.jvm.JvmSynthetic
|
||||
*
|
||||
* @see Contact
|
||||
*/
|
||||
interface Bot : CoroutineScope {
|
||||
abstract class Bot : CoroutineScope {
|
||||
@UseExperimental(MiraiInternalAPI::class)
|
||||
companion object {
|
||||
suspend inline operator fun invoke(account: BotAccount, logger: MiraiLogger): Bot = BotImpl(account, logger, coroutineContext)
|
||||
suspend inline operator fun invoke(account: BotAccount): Bot = BotImpl(account, context = coroutineContext)
|
||||
@JvmSynthetic
|
||||
suspend inline operator fun invoke(qq: UInt, password: String): Bot = BotImpl(BotAccount(qq, password), context = coroutineContext)
|
||||
|
||||
suspend inline operator fun invoke(qq: Long, password: String): Bot = BotImpl(BotAccount(qq.toUInt(), password), context = coroutineContext)
|
||||
operator fun invoke(qq: Long, password: String, context: CoroutineContext): Bot = BotImpl(BotAccount(qq, password), context = context)
|
||||
@JvmSynthetic
|
||||
operator fun invoke(qq: UInt, password: String, context: CoroutineContext): Bot = BotImpl(BotAccount(qq, password), context = context)
|
||||
|
||||
operator fun invoke(account: BotAccount, context: CoroutineContext): Bot = BotImpl(account, context = context)
|
||||
|
||||
|
||||
inline fun forEachInstance(block: (Bot) -> Unit) = BotImpl.forEachInstance(block)
|
||||
|
||||
fun instanceWhose(qq: UInt): Bot = BotImpl.instanceWhose(qq = qq)
|
||||
fun instanceWhose(qq: Long): Bot = BotImpl.instanceWhose(qq = qq)
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号信息
|
||||
*/
|
||||
val account: BotAccount
|
||||
abstract val account: BotAccount
|
||||
|
||||
/**
|
||||
* 日志记录器
|
||||
*/
|
||||
val logger: MiraiLogger
|
||||
abstract val logger: MiraiLogger
|
||||
|
||||
override val coroutineContext: CoroutineContext
|
||||
abstract override val coroutineContext: CoroutineContext
|
||||
|
||||
// region contacts
|
||||
|
||||
/**
|
||||
* 与这个机器人相关的 QQ 列表. 机器人与 QQ 不一定是好友
|
||||
*/
|
||||
val qqs: ContactList<QQ>
|
||||
abstract val qqs: ContactList<QQ>
|
||||
|
||||
/**
|
||||
* 获取缓存的 QQ 对象. 若没有对应的缓存, 则会线程安全地创建一个.
|
||||
*/
|
||||
fun getQQ(id: UInt): QQ
|
||||
|
||||
/**
|
||||
* 获取缓存的 QQ 对象. 若没有对应的缓存, 则会线程安全地创建一个.
|
||||
*/
|
||||
fun getQQ(id: Long): QQ
|
||||
abstract fun getQQ(id: Long): QQ
|
||||
|
||||
/**
|
||||
* 与这个机器人相关的群列表. 机器人不一定是群成员.
|
||||
*/
|
||||
val groups: ContactList<Group>
|
||||
abstract val groups: ContactList<Group>
|
||||
|
||||
/**
|
||||
* 获取缓存的群对象. 若没有对应的缓存, 则会线程安全地创建一个.
|
||||
* 若 [id] 无效, 将会抛出 [GroupNotFoundException]
|
||||
*/
|
||||
suspend fun getGroup(id: GroupId): Group
|
||||
abstract suspend fun getGroup(id: GroupId): Group
|
||||
|
||||
/**
|
||||
* 获取缓存的群对象. 若没有对应的缓存, 则会线程安全地创建一个.
|
||||
* 若 [internalId] 无效, 将会抛出 [GroupNotFoundException]
|
||||
*/
|
||||
suspend fun getGroup(internalId: GroupInternalId): Group
|
||||
abstract suspend fun getGroup(internalId: GroupInternalId): Group
|
||||
|
||||
/**
|
||||
* 获取缓存的群对象. 若没有对应的缓存, 则会线程安全地创建一个.
|
||||
* 若 [id] 无效, 将会抛出 [GroupNotFoundException]
|
||||
*/
|
||||
suspend fun getGroup(id: Long): Group
|
||||
abstract suspend fun getGroup(id: Long): Group
|
||||
|
||||
// endregion
|
||||
|
||||
@ -101,36 +87,64 @@ interface Bot : CoroutineScope {
|
||||
/**
|
||||
* 网络模块
|
||||
*/
|
||||
val network: BotNetworkHandler<*>
|
||||
abstract val network: BotNetworkHandler
|
||||
|
||||
/**
|
||||
* [关闭][BotNetworkHandler.close]网络处理器, 取消所有运行在 [BotNetworkHandler] 下的协程.
|
||||
* 然后重新启动并尝试登录
|
||||
* 使用在默认配置基础上修改的配置进行登录
|
||||
*/
|
||||
fun tryReinitializeNetworkHandler(
|
||||
configuration: BotConfiguration,
|
||||
cause: Throwable? = null
|
||||
): Job
|
||||
suspend inline fun login(configuration: BotConfiguration.() -> Unit) {
|
||||
return this.login(BotConfiguration().apply(configuration))
|
||||
}
|
||||
|
||||
/**
|
||||
* [关闭][BotNetworkHandler.close]网络处理器, 取消所有运行在 [BotNetworkHandler] 下的协程.
|
||||
* 然后重新启动并尝试登录
|
||||
* 使用特定配置进行登录
|
||||
*/
|
||||
suspend fun reinitializeNetworkHandler(
|
||||
configuration: BotConfiguration,
|
||||
cause: Throwable? = null
|
||||
): LoginResult
|
||||
abstract suspend fun login(configuration: BotConfiguration = BotConfiguration.Default)
|
||||
// endregion
|
||||
|
||||
|
||||
// region actions
|
||||
|
||||
abstract suspend fun Image.getLink(): ImageLink
|
||||
|
||||
suspend fun Image.downloadAsByteArray(): ByteArray = getLink().downloadAsByteArray()
|
||||
|
||||
suspend fun Image.download(): ByteReadPacket = getLink().download()
|
||||
|
||||
/**
|
||||
* [关闭][BotNetworkHandler.close]网络处理器, 取消所有运行在 [BotNetworkHandler] 下的协程.
|
||||
* 然后重新启动并尝试登录
|
||||
* 添加一个好友
|
||||
*
|
||||
* @param message 若需要验证请求时的验证消息.
|
||||
* @param remark 好友备注
|
||||
*/
|
||||
fun reinitializeNetworkHandlerAsync(
|
||||
configuration: BotConfiguration,
|
||||
cause: Throwable? = null
|
||||
): Deferred<LoginResult>
|
||||
abstract suspend fun addFriend(id: Long, message: String? = null, remark: String? = null): AddFriendResult
|
||||
|
||||
/**
|
||||
* 同意来自陌生人的加好友请求
|
||||
*/
|
||||
abstract suspend fun approveFriendAddRequest(id: Long, remark: String?)
|
||||
|
||||
// endregion
|
||||
|
||||
fun close()
|
||||
abstract fun close(throwable: Throwable?)
|
||||
|
||||
// region extensions
|
||||
|
||||
fun Int.qq(): QQ = getQQ(this.toLong())
|
||||
fun Long.qq(): QQ = getQQ(this)
|
||||
|
||||
suspend inline fun Int.group(): Group = getGroup(this.toLong())
|
||||
suspend inline fun Long.group(): Group = getGroup(this)
|
||||
suspend inline fun GroupInternalId.group(): Group = getGroup(this)
|
||||
suspend inline fun GroupId.group(): Group = getGroup(this)
|
||||
|
||||
|
||||
/**
|
||||
* 需要调用者自行 close [output]
|
||||
*/
|
||||
@UseExperimental(KtorExperimentalAPI::class)
|
||||
suspend inline fun Image.downloadTo(output: OutputStream) =
|
||||
download().use { input -> input.transferTo(output) }
|
||||
|
||||
// endregion
|
||||
}
|
@ -3,8 +3,6 @@
|
||||
package net.mamoe.mirai
|
||||
|
||||
data class BotAccount(
|
||||
val id: UInt,
|
||||
val id: Long,
|
||||
val password: String
|
||||
) {
|
||||
constructor(id: Long, password: String) : this(id.toUInt(), password)
|
||||
}
|
||||
)
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,70 @@
|
||||
package net.mamoe.mirai
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
|
||||
/**
|
||||
* 构造 [Bot] 的工厂.
|
||||
*
|
||||
* 在协议模块中有各自的实现.
|
||||
* - `mirai-core-timpc`: `TIMPC`
|
||||
* - `mirai-core-qqandroid`: `QQAndroid`
|
||||
*/
|
||||
@Suppress("FunctionName")
|
||||
interface BotFactory {
|
||||
/**
|
||||
* 在当前 CoroutineScope 下构造 Bot 实例
|
||||
* 该 Bot 实例的生命周期将会跟随这个 CoroutineScope.
|
||||
* 这个 CoroutineScope 也会等待 Bot 的结束才会结束.
|
||||
*
|
||||
* ```kotlin
|
||||
* suspend fun myProcess(){
|
||||
* TIMPC.Bot(account, logger)
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
suspend fun Bot(account: BotAccount, logger: MiraiLogger? = null): Bot
|
||||
|
||||
/**
|
||||
* 在当前 CoroutineScope 下构造 Bot 实例
|
||||
* 该 Bot 实例的生命周期将会跟随这个 CoroutineScope.
|
||||
* 这个 CoroutineScope 也会等待 Bot 的结束才会结束.
|
||||
*
|
||||
* ```kotlin
|
||||
* suspend fun myProcess(){
|
||||
* TIMPC.Bot(account, logger)
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
suspend fun Bot(qq: Long, password: String, logger: MiraiLogger? = null): Bot
|
||||
|
||||
/**
|
||||
* 在特定的 CoroutineScope 下构造 Bot 实例
|
||||
* 该 Bot 实例的生命周期将会跟随这个 CoroutineScope.
|
||||
* 这个 CoroutineScope 也会等待 Bot 的结束才会结束.
|
||||
*
|
||||
* ```kotlin
|
||||
* fun myProcess(){
|
||||
* TIMPC.run {
|
||||
* GlobalScope.Bot(account, logger)
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
fun CoroutineScope.Bot(qq: Long, password: String, logger: MiraiLogger? = null): Bot
|
||||
|
||||
/**
|
||||
* 在特定的 CoroutineScope 下构造 Bot 实例
|
||||
* 该 Bot 实例的生命周期将会跟随这个 CoroutineScope.
|
||||
* 这个 CoroutineScope 也会等待 Bot 的结束才会结束.
|
||||
*
|
||||
* ```kotlin
|
||||
* fun myProcess(){
|
||||
* TIMPC.run {
|
||||
* GlobalScope.Bot(account, logger)
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
fun CoroutineScope.Bot(account: BotAccount, logger: MiraiLogger? = null): Bot
|
||||
}
|
@ -1,73 +1,26 @@
|
||||
@file:JvmMultifileClass
|
||||
@file:JvmName("BotHelperKt")
|
||||
@file:Suppress("unused", "EXPERIMENTAL_API_USAGE", "NOTHING_TO_INLINE")
|
||||
@file:Suppress("unused", "EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai
|
||||
|
||||
import net.mamoe.mirai.contact.*
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.BotSession
|
||||
import net.mamoe.mirai.network.protocol.timpc.TIMBotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.OutgoingPacket
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.login.LoginResult
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.login.requireSuccess
|
||||
import net.mamoe.mirai.utils.BotConfiguration
|
||||
import net.mamoe.mirai.utils.internal.PositiveNumbers
|
||||
import net.mamoe.mirai.utils.internal.coerceAtLeastOrFail
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
import kotlin.jvm.JvmMultifileClass
|
||||
import kotlin.jvm.JvmName
|
||||
import kotlin.jvm.JvmOverloads
|
||||
|
||||
/*
|
||||
* 在 [Bot] 中的方法的捷径
|
||||
*/
|
||||
|
||||
//Contacts
|
||||
suspend inline fun Bot.getGroup(id: UInt): Group = this.getGroup(GroupId(id))
|
||||
|
||||
|
||||
/**
|
||||
* 以 [BotSession] 作为接收器 (receiver) 并调用 [block], 返回 [block] 的返回值.
|
||||
* 这个方法将能帮助使用在 [BotSession] 中定义的一些扩展方法, 如 [BotSession.sendAndExpectAsync]
|
||||
*/
|
||||
@UseExperimental(ExperimentalContracts::class)
|
||||
inline fun <R> Bot.withSession(block: BotSession.() -> R): R {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
return with(this.network.session) { block() }
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送数据包
|
||||
* @throws IllegalStateException 当 [BotNetworkHandler.socket] 未开启时
|
||||
*/
|
||||
internal suspend inline fun Bot.sendPacket(packet: OutgoingPacket) =
|
||||
(this.network as TIMBotNetworkHandler).socket.sendPacket(packet)
|
||||
|
||||
/**
|
||||
* 使用在默认配置基础上修改的配置进行登录
|
||||
*/
|
||||
@UseExperimental(ExperimentalContracts::class)
|
||||
suspend inline fun Bot.login(configuration: BotConfiguration.() -> Unit): LoginResult {
|
||||
contract {
|
||||
callsInPlace(configuration, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
return this.reinitializeNetworkHandler(BotConfiguration().apply(configuration))
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用默认的配置 ([BotConfiguration.Default]) 登录, 返回登录结果
|
||||
*/
|
||||
suspend inline fun Bot.login(): LoginResult = this.reinitializeNetworkHandler(BotConfiguration.Default)
|
||||
|
||||
/**
|
||||
* 使用默认的配置 ([BotConfiguration.Default]) 登录, 返回 [this]
|
||||
*/
|
||||
suspend inline fun Bot.alsoLogin(): Bot = apply { login().requireSuccess() }
|
||||
suspend inline fun Bot.alsoLogin(configuration: BotConfiguration = BotConfiguration.Default): Bot =
|
||||
apply { login(configuration) }
|
||||
|
||||
/**
|
||||
* 使用在默认配置基础上修改的配置进行登录, 返回 [this]
|
||||
@ -77,20 +30,11 @@ suspend inline fun Bot.alsoLogin(configuration: BotConfiguration.() -> Unit): Bo
|
||||
contract {
|
||||
callsInPlace(configuration, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
this.reinitializeNetworkHandler(BotConfiguration().apply(configuration)).requireSuccess()
|
||||
this.login(configuration)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用默认的配置 ([BotConfiguration.Default]) 登录, 返回 [this]
|
||||
*/
|
||||
suspend inline fun Bot.alsoLogin(message: String): Bot {
|
||||
return this.apply {
|
||||
login().requireSuccess { message } // requireSuccess is inline, so no performance waste
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得机器人的 QQ 号
|
||||
*/
|
||||
inline val Bot.qqAccount: UInt get() = this.account.id
|
||||
inline val Bot.qqAccount: Long get() = this.account.id
|
@ -2,54 +2,43 @@
|
||||
|
||||
package net.mamoe.mirai
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import net.mamoe.mirai.contact.*
|
||||
import net.mamoe.mirai.contact.internal.Group
|
||||
import net.mamoe.mirai.contact.internal.QQ
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import net.mamoe.mirai.network.BotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.TIMBotNetworkHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.KnownPacketId
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.action.GroupNotFound
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.action.GroupPacket
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.action.RawGroupInfo
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.login.LoginResult
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.login.isSuccess
|
||||
import net.mamoe.mirai.network.qqAccount
|
||||
import net.mamoe.mirai.utils.*
|
||||
import net.mamoe.mirai.utils.internal.PositiveNumbers
|
||||
import net.mamoe.mirai.utils.internal.coerceAtLeastOrFail
|
||||
import net.mamoe.mirai.utils.io.inline
|
||||
import net.mamoe.mirai.utils.DefaultLogger
|
||||
import net.mamoe.mirai.utils.LockFreeLinkedList
|
||||
import net.mamoe.mirai.utils.MiraiInternalAPI
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
import net.mamoe.mirai.utils.io.logStacktrace
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.jvm.JvmSynthetic
|
||||
|
||||
@PublishedApi
|
||||
internal class BotImpl @PublishedApi internal constructor(
|
||||
/*
|
||||
* 泛型 N 不需要向外(接口)暴露.
|
||||
*/
|
||||
@MiraiInternalAPI
|
||||
abstract class BotImpl<N : BotNetworkHandler> constructor(
|
||||
override val account: BotAccount,
|
||||
override val logger: MiraiLogger = DefaultLogger("Bot(" + account.id + ")"),
|
||||
context: CoroutineContext
|
||||
) : Bot, CoroutineScope {
|
||||
) : Bot(), CoroutineScope {
|
||||
private val supervisorJob = SupervisorJob(context[Job])
|
||||
override val coroutineContext: CoroutineContext =
|
||||
context + supervisorJob + CoroutineExceptionHandler { _, e -> e.logStacktrace("An exception was thrown under a coroutine of Bot") }
|
||||
|
||||
init {
|
||||
launch {
|
||||
instances.addLast(this@BotImpl)
|
||||
}
|
||||
@Suppress("LeakingThis")
|
||||
instances.addLast(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
init {
|
||||
KnownPacketId.values() /* load id classes */
|
||||
}
|
||||
|
||||
@PublishedApi
|
||||
internal val instances: LockFreeLinkedList<Bot> = LockFreeLinkedList()
|
||||
|
||||
inline fun forEachInstance(block: (Bot) -> Unit) = instances.forEach(block)
|
||||
|
||||
fun instanceWhose(qq: UInt): Bot {
|
||||
fun instanceWhose(qq: Long): Bot {
|
||||
instances.forEach {
|
||||
if (it.qqAccount == qq) {
|
||||
return it
|
||||
@ -63,101 +52,21 @@ internal class BotImpl @PublishedApi internal constructor(
|
||||
|
||||
// region network
|
||||
|
||||
override val network: BotNetworkHandler<*> get() = _network
|
||||
private lateinit var _network: BotNetworkHandler<*>
|
||||
|
||||
override fun tryReinitializeNetworkHandler(// shouldn't be suspend!! This function MUST NOT inherit the context from the caller because the caller(NetworkHandler) is going to close
|
||||
configuration: BotConfiguration,
|
||||
cause: Throwable?
|
||||
): Job = launch {
|
||||
repeat(configuration.reconnectionRetryTimes) {
|
||||
if (reinitializeNetworkHandlerAsync(configuration, cause).await().isSuccess()) {
|
||||
logger.info("Reconnected successfully")
|
||||
return@launch
|
||||
} else {
|
||||
delay(configuration.reconnectPeriodMillis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun reinitializeNetworkHandler(
|
||||
configuration: BotConfiguration,
|
||||
cause: Throwable?
|
||||
): LoginResult {
|
||||
logger.info("BotAccount: ${qqAccount.toLong()}")
|
||||
logger.info("Initializing BotNetworkHandler")
|
||||
try {
|
||||
if (::_network.isInitialized) {
|
||||
_network.close(cause)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("Cannot close network handler", e)
|
||||
}
|
||||
_network = TIMBotNetworkHandler(this.coroutineContext + configuration, this)
|
||||
|
||||
return _network.login()
|
||||
}
|
||||
|
||||
override fun reinitializeNetworkHandlerAsync(
|
||||
configuration: BotConfiguration,
|
||||
cause: Throwable?
|
||||
): Deferred<LoginResult> = async { reinitializeNetworkHandler(configuration, cause) }
|
||||
|
||||
// endregion
|
||||
|
||||
// region contacts
|
||||
override val groups: ContactList<Group> = ContactList(LockFreeLinkedList())
|
||||
|
||||
override val qqs: ContactList<QQ> = ContactList(LockFreeLinkedList())
|
||||
|
||||
/**
|
||||
* 线程安全地获取缓存的 QQ 对象. 若没有对应的缓存, 则会创建一个.
|
||||
*/
|
||||
@UseExperimental(MiraiInternalAPI::class)
|
||||
@JvmSynthetic
|
||||
override fun getQQ(id: UInt): QQ = qqs.delegate.getOrAdd(id) { QQ(this, id, coroutineContext) }
|
||||
|
||||
@UseExperimental(MiraiInternalAPI::class)
|
||||
override fun getQQ(@PositiveNumbers id: Long): QQ = getQQ(id.coerceAtLeastOrFail(0).toUInt())
|
||||
|
||||
override suspend fun getGroup(internalId: GroupInternalId): Group = getGroup(internalId.toId())
|
||||
|
||||
@UseExperimental(MiraiInternalAPI::class, ExperimentalUnsignedTypes::class)
|
||||
override suspend fun getGroup(id: GroupId): Group = groups.delegate.getOrNull(id.value) ?: inline {
|
||||
val info: RawGroupInfo = try {
|
||||
when (val response =
|
||||
withSession { GroupPacket.QueryGroupInfo(qqAccount, id.toInternalId(), sessionKey).sendAndExpect<GroupPacket.InfoResponse>() }) {
|
||||
is RawGroupInfo -> response
|
||||
is GroupNotFound -> throw GroupNotFoundException("id=${id.value.toLong()}")
|
||||
else -> assertUnreachable()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw IllegalStateException("Cannot obtain group info for id ${id.value.toLong()}", e)
|
||||
}
|
||||
|
||||
return groups.delegate.getOrAdd(id.value) { Group(this, id, info, coroutineContext) }
|
||||
}
|
||||
|
||||
@UseExperimental(MiraiInternalAPI::class)
|
||||
override suspend fun getGroup(@PositiveNumbers id: Long): Group = id.coerceAtLeastOrFail(0).toUInt().let {
|
||||
groups.delegate.getOrNull(it) ?: inline {
|
||||
val info: RawGroupInfo = try {
|
||||
withSession { GroupPacket.QueryGroupInfo(qqAccount, GroupId(it).toInternalId(), sessionKey).sendAndExpect() }
|
||||
} catch (e: Exception) {
|
||||
e.logStacktrace()
|
||||
error("Cannot obtain group info for id ${it.toLong()}")
|
||||
}
|
||||
|
||||
return groups.delegate.getOrAdd(it) { Group(this, GroupId(it), info, coroutineContext) }
|
||||
}
|
||||
}
|
||||
abstract override val network: N
|
||||
// endregion
|
||||
|
||||
@UseExperimental(MiraiInternalAPI::class)
|
||||
override fun close() {
|
||||
_network.close()
|
||||
this.supervisorJob.complete()
|
||||
groups.delegate.clear()
|
||||
qqs.delegate.clear()
|
||||
override fun close(throwable: Throwable?) {
|
||||
if (throwable == null) {
|
||||
network.close()
|
||||
this.supervisorJob.complete()
|
||||
groups.delegate.clear()
|
||||
qqs.delegate.clear()
|
||||
} else {
|
||||
network.close(throwable)
|
||||
this.supervisorJob.completeExceptionally(throwable)
|
||||
groups.delegate.clear()
|
||||
qqs.delegate.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,15 +3,9 @@
|
||||
package net.mamoe.mirai.contact
|
||||
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.message.Message
|
||||
import net.mamoe.mirai.message.MessageChain
|
||||
import net.mamoe.mirai.message.chain
|
||||
import net.mamoe.mirai.message.singleChain
|
||||
import net.mamoe.mirai.network.BotSession
|
||||
import net.mamoe.mirai.utils.LockFreeLinkedList
|
||||
import net.mamoe.mirai.utils.MiraiInternalAPI
|
||||
import net.mamoe.mirai.utils.joinToString
|
||||
import net.mamoe.mirai.withSession
|
||||
import net.mamoe.mirai.message.data.*
|
||||
import net.mamoe.mirai.utils.ExternalImage
|
||||
import net.mamoe.mirai.utils.WeakRefProperty
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
@ -26,12 +20,13 @@ interface Contact {
|
||||
/**
|
||||
* 这个联系人所属 [Bot]
|
||||
*/
|
||||
val bot: Bot
|
||||
@WeakRefProperty
|
||||
val bot: Bot // weak ref
|
||||
|
||||
/**
|
||||
* 可以是 QQ 号码或者群号码 [GroupId].
|
||||
*/
|
||||
val id: UInt
|
||||
val id: Long
|
||||
|
||||
/**
|
||||
* 向这个对象发送消息.
|
||||
@ -39,6 +34,8 @@ interface Contact {
|
||||
* 速度太快会被服务器屏蔽(无响应). 在测试中不延迟地发送 6 条消息就会被屏蔽之后的数据包 1 秒左右.
|
||||
*/
|
||||
suspend fun sendMessage(message: MessageChain)
|
||||
|
||||
suspend fun uploadImage(image: ExternalImage): ImageId
|
||||
}
|
||||
|
||||
suspend inline fun Contact.sendMessage(message: Message) = sendMessage(message.chain())
|
||||
@ -46,13 +43,13 @@ suspend inline fun Contact.sendMessage(message: Message) = sendMessage(message.c
|
||||
suspend inline fun Contact.sendMessage(plain: String) = sendMessage(plain.singleChain())
|
||||
|
||||
/**
|
||||
* 以 [BotSession] 作为接收器 (receiver) 并调用 [block], 返回 [block] 的返回值.
|
||||
* 这个方法将能帮助使用在 [BotSession] 中定义的一些扩展方法, 如 [BotSession.sendAndExpectAsync]
|
||||
* 以 [Bot] 作为接收器 (receiver) 并调用 [block], 返回 [block] 的返回值.
|
||||
* 这个方法将能帮助使用在 [Bot] 中定义的一些扩展方法
|
||||
*/
|
||||
@UseExperimental(ExperimentalContracts::class)
|
||||
inline fun <R> Contact.withSession(block: BotSession.() -> R): R {
|
||||
inline fun <R> Contact.withBot(block: Bot.() -> R): R {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
return bot.withSession(block)
|
||||
return bot.run(block)
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import net.mamoe.mirai.utils.joinToString
|
||||
*/
|
||||
@UseExperimental(MiraiInternalAPI::class)
|
||||
@Suppress("unused")
|
||||
class ContactList<C : Contact>(@PublishedApi internal val delegate: LockFreeLinkedList<C>) {
|
||||
class ContactList<C : Contact>(@MiraiInternalAPI val delegate: LockFreeLinkedList<C>) {
|
||||
/**
|
||||
* ID 列表的字符串表示.
|
||||
* 如:
|
||||
@ -22,9 +22,9 @@ class ContactList<C : Contact>(@PublishedApi internal val delegate: LockFreeLink
|
||||
*/
|
||||
val idContentString: String get() = "[" + buildString { delegate.forEach { append(it.id).append(", ") } }.dropLast(2) + "]"
|
||||
|
||||
operator fun get(id: UInt): C = delegate[id]
|
||||
fun getOrNull(id: UInt): C? = delegate.getOrNull(id)
|
||||
fun containsId(id: UInt): Boolean = delegate.getOrNull(id) != null
|
||||
operator fun get(id: Long): C = delegate[id]
|
||||
fun getOrNull(id: Long): C? = delegate.getOrNull(id)
|
||||
fun containsId(id: Long): Boolean = delegate.getOrNull(id) != null
|
||||
|
||||
val size: Int get() = delegate.size
|
||||
operator fun contains(element: C): Boolean = delegate.contains(element)
|
||||
@ -35,14 +35,14 @@ class ContactList<C : Contact>(@PublishedApi internal val delegate: LockFreeLink
|
||||
override fun toString(): String = delegate.joinToString(separator = ", ", prefix = "ContactList(", postfix = ")")
|
||||
}
|
||||
|
||||
operator fun <C : Contact> LockFreeLinkedList<C>.get(id: UInt): C {
|
||||
operator fun <C : Contact> LockFreeLinkedList<C>.get(id: Long): C {
|
||||
forEach { if (it.id == id) return it }
|
||||
throw NoSuchElementException()
|
||||
}
|
||||
|
||||
fun <C : Contact> LockFreeLinkedList<C>.getOrNull(id: UInt): C? {
|
||||
fun <C : Contact> LockFreeLinkedList<C>.getOrNull(id: Long): C? {
|
||||
forEach { if (it.id == id) return it }
|
||||
return null
|
||||
}
|
||||
|
||||
fun <C : Contact> LockFreeLinkedList<C>.getOrAdd(id: UInt, supplier: () -> C): C = filteringGetOrAdd({ it.id == id }, supplier)
|
||||
fun <C : Contact> LockFreeLinkedList<C>.getOrAdd(id: Long, supplier: () -> C): C = filteringGetOrAdd({ it.id == id }, supplier)
|
||||
|
@ -3,10 +3,7 @@
|
||||
package net.mamoe.mirai.contact
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.action.GroupInfo
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.action.QuitGroupResponse
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalAPI
|
||||
import net.mamoe.mirai.utils.internal.PositiveNumbers
|
||||
import net.mamoe.mirai.network.data.GroupInfo
|
||||
import net.mamoe.mirai.utils.internal.coerceAtLeastOrFail
|
||||
|
||||
|
||||
@ -53,7 +50,7 @@ interface Group : Contact, CoroutineScope/*, Map<UInt, Member>*/ { // TODO: 2019
|
||||
/**
|
||||
* 获取群成员. 若此 ID 的成员不存在, 则会抛出 [kotlin.NoSuchElementException]
|
||||
*/
|
||||
fun getMember(id: UInt): Member
|
||||
fun getMember(id: Long): Member
|
||||
|
||||
/**
|
||||
* 更新群资料. 群资料会与服务器事件同步事件更新, 一般情况下不需要手动更新.
|
||||
@ -67,7 +64,7 @@ interface Group : Contact, CoroutineScope/*, Map<UInt, Member>*/ { // TODO: 2019
|
||||
*
|
||||
* @see QuitGroupResponse.isSuccess 判断是否成功
|
||||
*/
|
||||
suspend fun quit(): QuitGroupResponse
|
||||
suspend fun quit(): Boolean
|
||||
|
||||
fun toFullString(): String = "Group(id=${this.id}, name=$name, owner=${owner.id}, members=${members.idContentString})"
|
||||
}
|
||||
@ -81,27 +78,19 @@ interface Group : Contact, CoroutineScope/*, Map<UInt, Member>*/ { // TODO: 2019
|
||||
* @see GroupInternalId.toId 由 [GroupInternalId] 转换为 [GroupId]
|
||||
* @see GroupId.toInternalId 由 [GroupId] 转换为 [GroupInternalId]
|
||||
*/
|
||||
inline class GroupId(inline val value: UInt)
|
||||
|
||||
/**
|
||||
* 将 [this] 转为 [GroupId].
|
||||
*/
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun UInt.groupId(): GroupId = GroupId(this)
|
||||
inline class GroupId(inline val value: Long)
|
||||
|
||||
/**
|
||||
* 将 [this] 转为 [GroupInternalId].
|
||||
*/
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun UInt.groupInternalId(): GroupInternalId = GroupInternalId(this)
|
||||
fun Long.groupInternalId(): GroupInternalId = GroupInternalId(this)
|
||||
|
||||
/**
|
||||
* 将无符号整数格式的 [Long] 转为 [GroupId].
|
||||
*
|
||||
* 注: 在 Java 中常用 [Long] 来表示 [UInt]
|
||||
*/
|
||||
fun @receiver:PositiveNumbers Long.groupId(): GroupId =
|
||||
GroupId(this.coerceAtLeastOrFail(0).toUInt())
|
||||
fun Long.groupId(): GroupId = GroupId(this.coerceAtLeastOrFail(0))
|
||||
|
||||
/**
|
||||
* 一些群 API 使用的 ID. 在使用时会特别注明
|
||||
@ -111,4 +100,4 @@ fun @receiver:PositiveNumbers Long.groupId(): GroupId =
|
||||
* @see GroupInternalId.toId 由 [GroupInternalId] 转换为 [GroupId]
|
||||
* @see GroupId.toInternalId 由 [GroupId] 转换为 [GroupInternalId]
|
||||
*/
|
||||
inline class GroupInternalId(inline val value: UInt)
|
||||
inline class GroupInternalId(inline val value: Long)
|
||||
|
@ -6,7 +6,7 @@ import kotlin.math.pow
|
||||
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
private val `10EXP6` = 10.0.pow(6).toUInt()
|
||||
private val `10EXP6` = 10.0.pow(6)
|
||||
|
||||
|
||||
fun GroupId.toInternalId(): GroupInternalId {
|
||||
@ -23,12 +23,12 @@ fun GroupId.toInternalId(): GroupInternalId {
|
||||
in 1..10 -> plusLeft(202, 6)
|
||||
in 11..19 -> plusLeft(469, 6)
|
||||
in 20..66 -> plusLeft(208, 7)
|
||||
in 67..156 -> plusLeft(1943, 6)
|
||||
in 67..156 -> plusLeft(1943, 6)
|
||||
in 157..209 -> plusLeft(199, 7)
|
||||
in 210..309 -> plusLeft(389, 7)
|
||||
in 310..499 -> plusLeft(349, 7)
|
||||
else -> null
|
||||
}?.toUInt() ?: this.value
|
||||
}?.toLong() ?: this.value
|
||||
)
|
||||
}
|
||||
|
||||
@ -36,17 +36,17 @@ fun GroupInternalId.toId(): GroupId = with(value.toString()) {
|
||||
if (value < `10EXP6`) {
|
||||
return GroupId(value)
|
||||
}
|
||||
val left: UInt = this.dropLast(6).toUInt()
|
||||
val left = this.dropLast(6).toLong()
|
||||
|
||||
return GroupId(
|
||||
when (left.toInt()) {
|
||||
in 203..212 -> ((left - 202u).toString() + this.takeLast(6).toInt().toString()).toUInt()
|
||||
in 480..488 -> ((left - 469u).toString() + this.takeLast(6).toInt().toString()).toUInt()
|
||||
in 2100..2146 -> ((left.toString().take(3).toUInt() - 208u).toString() + this.takeLast(7).toInt().toString()).toUInt()
|
||||
in 2010..2099 -> ((left - 1943u).toString() + this.takeLast(6).toInt().toString()).toUInt()
|
||||
in 2147..2199 -> ((left.toString().take(3).toUInt() - 199u).toString() + this.takeLast(7).toInt().toString()).toUInt()
|
||||
in 4100..4199 -> ((left.toString().take(3).toUInt() - 389u).toString() + this.takeLast(7).toInt().toString()).toUInt()
|
||||
in 3800..3989 -> ((left.toString().take(3).toUInt() - 349u).toString() + this.takeLast(7).toInt().toString()).toUInt()
|
||||
in 203..212 -> ((left - 202).toString() + this.takeLast(6).toInt().toString()).toLong()
|
||||
in 480..488 -> ((left - 469).toString() + this.takeLast(6).toInt().toString()).toLong()
|
||||
in 2100..2146 -> ((left.toString().take(3).toLong() - 208).toString() + this.takeLast(7).toInt().toString()).toLong()
|
||||
in 2010..2099 -> ((left - 1943).toString() + this.takeLast(6).toInt().toString()).toLong()
|
||||
in 2147..2199 -> ((left.toString().take(3).toLong() - 199).toString() + this.takeLast(7).toInt().toString()).toLong()
|
||||
in 4100..4199 -> ((left.toString().take(3).toLong() - 389).toString() + this.takeLast(7).toInt().toString()).toLong()
|
||||
in 3800..3989 -> ((left.toString().take(3).toLong() - 349).toString() + this.takeLast(7).toInt().toString()).toLong()
|
||||
else -> value
|
||||
}
|
||||
)
|
||||
|
@ -4,12 +4,9 @@ package net.mamoe.mirai.contact
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.contact.data.Profile
|
||||
import net.mamoe.mirai.network.BotSession
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.action.AvatarLink
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.action.FriendNameRemark
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.action.PreviousNameList
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalAPI
|
||||
import net.mamoe.mirai.network.data.FriendNameRemark
|
||||
import net.mamoe.mirai.network.data.PreviousNameList
|
||||
import net.mamoe.mirai.network.data.Profile
|
||||
|
||||
/**
|
||||
* QQ 对象.
|
||||
|
@ -6,11 +6,10 @@ import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.contact.isAdministrator
|
||||
import net.mamoe.mirai.contact.isOperator
|
||||
import net.mamoe.mirai.contact.isOwner
|
||||
import net.mamoe.mirai.message.Message
|
||||
import net.mamoe.mirai.message.any
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.event.FriendMessage
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.event.GroupMessage
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.event.MessagePacket
|
||||
import net.mamoe.mirai.message.FriendMessage
|
||||
import net.mamoe.mirai.message.GroupMessage
|
||||
import net.mamoe.mirai.message.MessagePacket
|
||||
import net.mamoe.mirai.message.data.Message
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
@ -188,15 +187,9 @@ class MessageSubscribersBuilder<T : MessagePacket<*, *>>(
|
||||
* 如果是这个人发的消息, 就执行 [onEvent]. 消息可以是好友消息也可以是群消息
|
||||
*/
|
||||
@MessageDsl
|
||||
suspend inline fun sentBy(qqId: UInt, noinline onEvent: @MessageDsl suspend T.(String) -> Unit) =
|
||||
suspend inline fun sentBy(qqId: Long, noinline onEvent: @MessageDsl suspend T.(String) -> Unit) =
|
||||
content({ sender.id == qqId }, onEvent)
|
||||
|
||||
/**
|
||||
* 如果是这个人发的消息, 就执行 [onEvent]. 消息可以是好友消息也可以是群消息
|
||||
*/
|
||||
@MessageDsl
|
||||
suspend inline fun sentBy(qqId: Long, noinline onEvent: @MessageDsl suspend T.(String) -> Unit) = sentBy(qqId.toUInt(), onEvent)
|
||||
|
||||
/**
|
||||
* 如果是管理员或群主发的消息, 就执行 [onEvent]
|
||||
*/
|
||||
@ -222,21 +215,15 @@ class MessageSubscribersBuilder<T : MessagePacket<*, *>>(
|
||||
* 如果是来自这个群的消息, 就执行 [onEvent]
|
||||
*/
|
||||
@MessageDsl
|
||||
suspend inline fun sentFrom(id: UInt, noinline onEvent: @MessageDsl suspend T.(String) -> Unit) =
|
||||
suspend inline fun sentFrom(id: Long, noinline onEvent: @MessageDsl suspend T.(String) -> Unit) =
|
||||
content({ if (this is GroupMessage) group.id == id else false }, onEvent)
|
||||
|
||||
/**
|
||||
* 如果是来自这个群的消息, 就执行 [onEvent]
|
||||
*/
|
||||
@MessageDsl
|
||||
suspend inline fun sentFrom(id: Long, noinline onEvent: @MessageDsl suspend T.(String) -> Unit) = sentFrom(id.toUInt(), onEvent)
|
||||
|
||||
/**
|
||||
* 如果消息内容包含 [M] 类型的 [Message], 就执行 [onEvent]
|
||||
*/
|
||||
@MessageDsl
|
||||
suspend inline fun <reified M : Message> has(noinline onEvent: @MessageDsl suspend T.(String) -> Unit) =
|
||||
subscriber { if (message.any<M>()) onEvent(this) }
|
||||
subscriber { if (message.any { it::class == M::class }) onEvent(this) }
|
||||
|
||||
/**
|
||||
* 如果 [filter] 返回 `true` 就执行 `onEvent`
|
||||
@ -283,7 +270,7 @@ class MessageSubscribersBuilder<T : MessagePacket<*, *>>(
|
||||
*/
|
||||
@MessageDsl
|
||||
suspend inline fun Regex.matchingReply(noinline replier: AnyReplier<T>) {
|
||||
content({ this@matchingReply.matchEntire(it) != null }){
|
||||
content({ this@matchingReply.matchEntire(it) != null }) {
|
||||
@Suppress("DSL_SCOPE_VIOLATION_WARNING") // false negative warning
|
||||
executeAndReply(replier)
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import net.mamoe.mirai.event.events.BotEvent
|
||||
import net.mamoe.mirai.event.internal.HandlerWithSession
|
||||
import net.mamoe.mirai.event.internal.Listener
|
||||
import net.mamoe.mirai.event.internal.subscribeInternal
|
||||
import net.mamoe.mirai.network.BotSession
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
@ -19,88 +18,87 @@ import kotlin.reflect.KClass
|
||||
|
||||
// region 顶层方法
|
||||
|
||||
suspend inline fun <reified E : BotEvent> Bot.subscribe(noinline handler: suspend BotSession.(E) -> ListeningStatus): Listener<E> =
|
||||
suspend inline fun <reified E : BotEvent> Bot.subscribe(noinline handler: suspend Bot.(E) -> ListeningStatus): Listener<E> =
|
||||
E::class.subscribe(this, handler)
|
||||
|
||||
suspend inline fun <reified E : BotEvent> Bot.subscribeAlways(noinline listener: suspend BotSession.(E) -> Unit): Listener<E> =
|
||||
suspend inline fun <reified E : BotEvent> Bot.subscribeAlways(noinline listener: suspend Bot.(E) -> Unit): Listener<E> =
|
||||
E::class.subscribeAlways(this, listener)
|
||||
|
||||
suspend inline fun <reified E : BotEvent> Bot.subscribeOnce(noinline listener: suspend BotSession.(E) -> Unit): Listener<E> =
|
||||
suspend inline fun <reified E : BotEvent> Bot.subscribeOnce(noinline listener: suspend Bot.(E) -> Unit): Listener<E> =
|
||||
E::class.subscribeOnce(this, listener)
|
||||
|
||||
suspend inline fun <reified E : BotEvent, T> Bot.subscribeUntil(valueIfStop: T, noinline listener: suspend BotSession.(E) -> T): Listener<E> =
|
||||
suspend inline fun <reified E : BotEvent, T> Bot.subscribeUntil(valueIfStop: T, noinline listener: suspend Bot.(E) -> T): Listener<E> =
|
||||
E::class.subscribeUntil(this, valueIfStop, listener)
|
||||
|
||||
suspend inline fun <reified E : BotEvent> Bot.subscribeUntilFalse(noinline listener: suspend BotSession.(E) -> Boolean): Listener<E> =
|
||||
suspend inline fun <reified E : BotEvent> Bot.subscribeUntilFalse(noinline listener: suspend Bot.(E) -> Boolean): Listener<E> =
|
||||
E::class.subscribeUntilFalse(this, listener)
|
||||
|
||||
suspend inline fun <reified E : BotEvent> Bot.subscribeUntilTrue(noinline listener: suspend BotSession.(E) -> Boolean): Listener<E> =
|
||||
suspend inline fun <reified E : BotEvent> Bot.subscribeUntilTrue(noinline listener: suspend Bot.(E) -> Boolean): Listener<E> =
|
||||
E::class.subscribeUntilTrue(this, listener)
|
||||
|
||||
suspend inline fun <reified E : BotEvent> Bot.subscribeUntilNull(noinline listener: suspend BotSession.(E) -> Any?): Listener<E> =
|
||||
suspend inline fun <reified E : BotEvent> Bot.subscribeUntilNull(noinline listener: suspend Bot.(E) -> Any?): Listener<E> =
|
||||
E::class.subscribeUntilNull(this, listener)
|
||||
|
||||
|
||||
suspend inline fun <reified E : BotEvent, T> Bot.subscribeWhile(valueIfContinue: T, noinline listener: suspend BotSession.(E) -> T): Listener<E> =
|
||||
suspend inline fun <reified E : BotEvent, T> Bot.subscribeWhile(valueIfContinue: T, noinline listener: suspend Bot.(E) -> T): Listener<E> =
|
||||
E::class.subscribeWhile(this, valueIfContinue, listener)
|
||||
|
||||
suspend inline fun <reified E : BotEvent> Bot.subscribeWhileFalse(noinline listener: suspend BotSession.(E) -> Boolean): Listener<E> =
|
||||
suspend inline fun <reified E : BotEvent> Bot.subscribeWhileFalse(noinline listener: suspend Bot.(E) -> Boolean): Listener<E> =
|
||||
E::class.subscribeWhileFalse(this, listener)
|
||||
|
||||
suspend inline fun <reified E : BotEvent> Bot.subscribeWhileTrue(noinline listener: suspend BotSession.(E) -> Boolean): Listener<E> =
|
||||
suspend inline fun <reified E : BotEvent> Bot.subscribeWhileTrue(noinline listener: suspend Bot.(E) -> Boolean): Listener<E> =
|
||||
E::class.subscribeWhileTrue(this, listener)
|
||||
|
||||
suspend inline fun <reified E : BotEvent> Bot.subscribeWhileNull(noinline listener: suspend BotSession.(E) -> Any?): Listener<E> =
|
||||
suspend inline fun <reified E : BotEvent> Bot.subscribeWhileNull(noinline listener: suspend Bot.(E) -> Any?): Listener<E> =
|
||||
E::class.subscribeWhileNull(this, listener)
|
||||
|
||||
// endregion
|
||||
|
||||
|
||||
// region KClass 的扩展方法 (仅内部使用)
|
||||
|
||||
@PublishedApi
|
||||
internal suspend fun <E : BotEvent> KClass<E>.subscribe(bot: Bot, handler: suspend BotSession.(E) -> ListeningStatus) =
|
||||
internal suspend fun <E : BotEvent> KClass<E>.subscribe(bot: Bot, handler: suspend Bot.(E) -> ListeningStatus) =
|
||||
this.subscribeInternal(HandlerWithSession(bot, handler))
|
||||
|
||||
@PublishedApi
|
||||
internal suspend fun <E : BotEvent> KClass<E>.subscribeAlways(bot: Bot, listener: suspend BotSession.(E) -> Unit) =
|
||||
internal suspend fun <E : BotEvent> KClass<E>.subscribeAlways(bot: Bot, listener: suspend Bot.(E) -> Unit) =
|
||||
this.subscribeInternal(HandlerWithSession(bot) { listener(it); ListeningStatus.LISTENING })
|
||||
|
||||
@PublishedApi
|
||||
internal suspend fun <E : BotEvent> KClass<E>.subscribeOnce(bot: Bot, listener: suspend BotSession.(E) -> Unit) =
|
||||
internal suspend fun <E : BotEvent> KClass<E>.subscribeOnce(bot: Bot, listener: suspend Bot.(E) -> Unit) =
|
||||
this.subscribeInternal(HandlerWithSession(bot) { listener(it); ListeningStatus.STOPPED })
|
||||
|
||||
@PublishedApi
|
||||
internal suspend fun <E : BotEvent, T> KClass<E>.subscribeUntil(bot: Bot, valueIfStop: T, listener: suspend BotSession.(E) -> T) =
|
||||
internal suspend fun <E : BotEvent, T> KClass<E>.subscribeUntil(bot: Bot, valueIfStop: T, listener: suspend Bot.(E) -> T) =
|
||||
subscribeInternal(HandlerWithSession(bot) { if (listener(it) === valueIfStop) ListeningStatus.STOPPED else ListeningStatus.LISTENING })
|
||||
|
||||
@PublishedApi
|
||||
internal suspend inline fun <E : BotEvent> KClass<E>.subscribeUntilFalse(bot: Bot, noinline listener: suspend BotSession.(E) -> Boolean) =
|
||||
internal suspend inline fun <E : BotEvent> KClass<E>.subscribeUntilFalse(bot: Bot, noinline listener: suspend Bot.(E) -> Boolean) =
|
||||
subscribeUntil(bot, false, listener)
|
||||
|
||||
@PublishedApi
|
||||
internal suspend inline fun <E : BotEvent> KClass<E>.subscribeUntilTrue(bot: Bot, noinline listener: suspend BotSession.(E) -> Boolean) =
|
||||
internal suspend inline fun <E : BotEvent> KClass<E>.subscribeUntilTrue(bot: Bot, noinline listener: suspend Bot.(E) -> Boolean) =
|
||||
subscribeUntil(bot, true, listener)
|
||||
|
||||
@PublishedApi
|
||||
internal suspend inline fun <E : BotEvent> KClass<E>.subscribeUntilNull(bot: Bot, noinline listener: suspend BotSession.(E) -> Any?) =
|
||||
internal suspend inline fun <E : BotEvent> KClass<E>.subscribeUntilNull(bot: Bot, noinline listener: suspend Bot.(E) -> Any?) =
|
||||
subscribeUntil(bot, null, listener)
|
||||
|
||||
|
||||
@PublishedApi
|
||||
internal suspend fun <E : BotEvent, T> KClass<E>.subscribeWhile(bot: Bot, valueIfContinue: T, listener: suspend BotSession.(E) -> T) =
|
||||
internal suspend fun <E : BotEvent, T> KClass<E>.subscribeWhile(bot: Bot, valueIfContinue: T, listener: suspend Bot.(E) -> T) =
|
||||
subscribeInternal(HandlerWithSession(bot) { if (listener(it) !== valueIfContinue) ListeningStatus.STOPPED else ListeningStatus.LISTENING })
|
||||
|
||||
@PublishedApi
|
||||
internal suspend inline fun <E : BotEvent> KClass<E>.subscribeWhileFalse(bot: Bot, noinline listener: suspend BotSession.(E) -> Boolean) =
|
||||
internal suspend inline fun <E : BotEvent> KClass<E>.subscribeWhileFalse(bot: Bot, noinline listener: suspend Bot.(E) -> Boolean) =
|
||||
subscribeWhile(bot, false, listener)
|
||||
|
||||
@PublishedApi
|
||||
internal suspend inline fun <E : BotEvent> KClass<E>.subscribeWhileTrue(bot: Bot, noinline listener: suspend BotSession.(E) -> Boolean) =
|
||||
internal suspend inline fun <E : BotEvent> KClass<E>.subscribeWhileTrue(bot: Bot, noinline listener: suspend Bot.(E) -> Boolean) =
|
||||
subscribeWhile(bot, true, listener)
|
||||
|
||||
@PublishedApi
|
||||
internal suspend inline fun <E : BotEvent> KClass<E>.subscribeWhileNull(bot: Bot, noinline listener: suspend BotSession.(E) -> Any?) =
|
||||
internal suspend inline fun <E : BotEvent> KClass<E>.subscribeWhileNull(bot: Bot, noinline listener: suspend Bot.(E) -> Any?) =
|
||||
subscribeWhile(bot, null, listener)
|
||||
|
||||
// endregion
|
@ -0,0 +1,10 @@
|
||||
package net.mamoe.mirai.event.events
|
||||
|
||||
import net.mamoe.mirai.network.data.EventPacket
|
||||
|
||||
/**
|
||||
* 被挤下线. 只能获取到中文的消息
|
||||
*/
|
||||
inline class ConnectionOccupiedEvent(val message: String) : EventPacket {
|
||||
override fun toString(): String = "ConnectionOccupiedEvent(${message.replace("\n", "")})"
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package net.mamoe.mirai.event.events
|
||||
|
||||
import net.mamoe.mirai.contact.QQ
|
||||
import net.mamoe.mirai.network.data.EventPacket
|
||||
import net.mamoe.mirai.utils.OnlineStatus
|
||||
|
||||
data class FriendStatusChanged(
|
||||
val qq: QQ,
|
||||
val status: OnlineStatus
|
||||
) : EventPacket
|
@ -0,0 +1,75 @@
|
||||
package net.mamoe.mirai.event.events
|
||||
|
||||
import net.mamoe.mirai.contact.Group
|
||||
import net.mamoe.mirai.contact.Member
|
||||
import net.mamoe.mirai.network.data.EventPacket
|
||||
|
||||
|
||||
// region mute
|
||||
/**
|
||||
* 某群成员被禁言事件
|
||||
*/
|
||||
@Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
class MemberMuteEvent(
|
||||
val member: Member,
|
||||
override val durationSeconds: Int,
|
||||
override val operator: Member
|
||||
) : MuteEvent() {
|
||||
override val group: Group get() = operator.group
|
||||
override fun toString(): String = "MemberMuteEvent(member=${member.id}, group=${group.id}, operator=${operator.id}, duration=${durationSeconds}s"
|
||||
}
|
||||
|
||||
/**
|
||||
* 机器人被禁言事件
|
||||
*/
|
||||
class BeingMutedEvent(
|
||||
override val durationSeconds: Int,
|
||||
override val operator: Member
|
||||
) : MuteEvent() {
|
||||
override val group: Group get() = operator.group
|
||||
override fun toString(): String = "BeingMutedEvent(group=${group.id}, operator=${operator.id}, duration=${durationSeconds}s"
|
||||
}
|
||||
|
||||
sealed class MuteEvent : EventOfMute() {
|
||||
abstract override val operator: Member
|
||||
abstract override val group: Group
|
||||
abstract val durationSeconds: Int
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region unmute
|
||||
/**
|
||||
* 某群成员被解除禁言事件
|
||||
*/
|
||||
@Suppress("unused")
|
||||
class MemberUnmuteEvent(
|
||||
val member: Member,
|
||||
override val operator: Member
|
||||
) : UnmuteEvent() {
|
||||
override val group: Group get() = operator.group
|
||||
override fun toString(): String = "MemberUnmuteEvent(member=${member.id}, group=${group.id}, operator=${operator.id}"
|
||||
}
|
||||
|
||||
/**
|
||||
* 机器人被解除禁言事件
|
||||
*/
|
||||
@Suppress("SpellCheckingInspection")
|
||||
class BeingUnmutedEvent(
|
||||
override val operator: Member
|
||||
) : UnmuteEvent() {
|
||||
override val group: Group get() = operator.group
|
||||
override fun toString(): String = "BeingUnmutedEvent(group=${group.id}, operator=${operator.id}"
|
||||
}
|
||||
|
||||
sealed class UnmuteEvent : EventOfMute() {
|
||||
abstract override val operator: Member
|
||||
abstract override val group: Group
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
abstract class EventOfMute : EventPacket {
|
||||
abstract val operator: Member
|
||||
abstract val group: Group
|
||||
}
|
||||
|
@ -1,48 +0,0 @@
|
||||
package net.mamoe.mirai.event.events
|
||||
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.event.Cancellable
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.OutgoingPacket
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.Packet
|
||||
|
||||
/* Abstract */
|
||||
|
||||
/**
|
||||
* 数据包相关事件
|
||||
*/
|
||||
internal sealed class PacketEvent<P : Packet>(bot: Bot, open val packet: P) : BotEvent(bot)
|
||||
|
||||
|
||||
/* Client to Server */
|
||||
|
||||
/**
|
||||
* 发送给服务器的数据包的相关事件
|
||||
*/
|
||||
internal sealed class OutgoingPacketEvent(bot: Bot, packet: OutgoingPacket) : PacketEvent<OutgoingPacket>(bot, packet)
|
||||
|
||||
/**
|
||||
* 包已发送, 此时包数据已完全发送至服务器, 且包已被关闭.
|
||||
*
|
||||
* 不可被取消
|
||||
*/
|
||||
internal class PacketSentEvent(bot: Bot, packet: OutgoingPacket) : OutgoingPacketEvent(bot, packet)
|
||||
|
||||
/**
|
||||
* 包发送前, 此时包数据已经编码完成.
|
||||
*
|
||||
* 可被取消
|
||||
*/
|
||||
internal class BeforePacketSendEvent(bot: Bot, packet: OutgoingPacket) : OutgoingPacketEvent(bot, packet), Cancellable
|
||||
|
||||
|
||||
/* Server to Client */
|
||||
|
||||
/**
|
||||
* 来自服务器的数据包的相关事件
|
||||
*/
|
||||
internal sealed class ServerPacketEvent<P : Packet>(bot: Bot, packet: P) : PacketEvent<P>(bot, packet)
|
||||
|
||||
/**
|
||||
* 服务器数据包接收事件. 此时包已经解密完成.
|
||||
*/
|
||||
internal class ServerPacketReceivedEvent<P : Packet>(bot: Bot, packet: P) : ServerPacketEvent<P>(bot, packet), Cancellable
|
@ -0,0 +1,27 @@
|
||||
package net.mamoe.mirai.event.events
|
||||
|
||||
import net.mamoe.mirai.contact.QQ
|
||||
import net.mamoe.mirai.network.data.EventPacket
|
||||
import net.mamoe.mirai.utils.unsafeWeakRef
|
||||
import kotlin.jvm.JvmOverloads
|
||||
|
||||
/**
|
||||
* 陌生人请求添加机器人账号为好友
|
||||
*/
|
||||
class ReceiveFriendAddRequestEvent(
|
||||
_qq: QQ,
|
||||
/**
|
||||
* 验证消息
|
||||
*/
|
||||
val message: String
|
||||
) : EventPacket {
|
||||
val qq: QQ by _qq.unsafeWeakRef()
|
||||
|
||||
/**
|
||||
* 同意这个请求
|
||||
*
|
||||
* @param remark 备注名, 不设置则需为 `null`
|
||||
*/
|
||||
@JvmOverloads // TODO: 2019/12/17 协议抽象
|
||||
suspend fun approve(remark: String? = null): Unit = qq.bot.approveFriendAddRequest(qq.id, remark)
|
||||
}
|
@ -1,17 +1,17 @@
|
||||
package net.mamoe.mirai.event.internal
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CompletableJob
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.event.EventDebugLogger
|
||||
import net.mamoe.mirai.event.ListeningStatus
|
||||
import net.mamoe.mirai.event.Subscribable
|
||||
import net.mamoe.mirai.event.events.BotEvent
|
||||
import net.mamoe.mirai.network.BotSession
|
||||
import net.mamoe.mirai.network.session
|
||||
import net.mamoe.mirai.utils.internal.inlinedRemoveIf
|
||||
import net.mamoe.mirai.utils.LockFreeLinkedList
|
||||
import net.mamoe.mirai.utils.io.logStacktrace
|
||||
import net.mamoe.mirai.utils.unsafeWeakRef
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.coroutineContext
|
||||
import kotlin.jvm.JvmField
|
||||
@ -24,8 +24,6 @@ import kotlin.reflect.KFunction
|
||||
*/
|
||||
var EventDisabled = false
|
||||
|
||||
// TODO: 2019/11/29 修改监听为 lock-free 模式
|
||||
|
||||
/**
|
||||
* 监听和广播实现.
|
||||
* 它会首先检查这个事件是否正在被广播
|
||||
@ -34,38 +32,9 @@ var EventDisabled = false
|
||||
*
|
||||
* @author Him188moe
|
||||
*/ // inline to avoid a Continuation creation
|
||||
internal suspend inline fun <L : Listener<E>, E : Subscribable> KClass<E>.subscribeInternal(listener: L): L = with(this.listeners()) {
|
||||
if (mainMutex.tryLock(listener)) {//能锁则代表这个事件目前没有正在广播.
|
||||
try {
|
||||
add(listener)//直接修改主监听者列表
|
||||
EventDebugLogger.debug("Added a listener to ${this@subscribeInternal.simpleName}")
|
||||
} finally {
|
||||
mainMutex.unlock(listener)
|
||||
}
|
||||
return listener
|
||||
}
|
||||
|
||||
//不能锁住, 则这个事件正在广播, 那么要将新的监听者放入缓存
|
||||
cacheMutex.withLock {
|
||||
cache.add(listener)
|
||||
EventDebugLogger.debug("Added a listener to cache of ${this@subscribeInternal.simpleName}")
|
||||
}
|
||||
|
||||
GlobalScope.launch {
|
||||
//启动协程并等待正在进行的广播结束, 然后将缓存转移到主监听者列表
|
||||
//启动后的协程马上就会因为锁而被挂起
|
||||
mainMutex.withLock(listener) {
|
||||
cacheMutex.withLock {
|
||||
if (cache.size != 0) {
|
||||
addAll(cache)
|
||||
cache.clear()
|
||||
EventDebugLogger.debug("Cache of ${this@subscribeInternal.simpleName} is now transferred to main")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return@with listener
|
||||
internal suspend inline fun <L : Listener<E>, E : Subscribable> KClass<E>.subscribeInternal(listener: L): L {
|
||||
this.listeners().addLast(listener)
|
||||
return listener
|
||||
}
|
||||
|
||||
/**
|
||||
@ -77,8 +46,6 @@ internal suspend inline fun <L : Listener<E>, E : Subscribable> KClass<E>.subscr
|
||||
* @author Him188moe
|
||||
*/
|
||||
sealed class Listener<in E : Subscribable> : CompletableJob {
|
||||
@JvmField
|
||||
internal val lock = Mutex()
|
||||
abstract suspend fun onEvent(event: E): ListeningStatus
|
||||
}
|
||||
|
||||
@ -113,7 +80,7 @@ internal class Handler<in E : Subscribable>
|
||||
@Suppress("FunctionName")
|
||||
internal suspend inline fun <E : Subscribable> HandlerWithSession(
|
||||
bot: Bot,
|
||||
noinline handler: suspend BotSession.(E) -> ListeningStatus
|
||||
noinline handler: suspend Bot.(E) -> ListeningStatus
|
||||
): HandlerWithSession<E> {
|
||||
return HandlerWithSession(bot, coroutineContext[Job], coroutineContext, handler)
|
||||
}
|
||||
@ -125,10 +92,10 @@ internal suspend inline fun <E : Subscribable> HandlerWithSession(
|
||||
*/
|
||||
@PublishedApi
|
||||
internal class HandlerWithSession<E : Subscribable> @PublishedApi internal constructor(
|
||||
@JvmField val bot: Bot,
|
||||
parentJob: Job?, private val context: CoroutineContext, @JvmField val handler: suspend BotSession.(E) -> ListeningStatus
|
||||
) :
|
||||
Listener<E>(), CompletableJob by Job(parentJob) {
|
||||
bot: Bot,
|
||||
parentJob: Job?, private val context: CoroutineContext, @JvmField val handler: suspend Bot.(E) -> ListeningStatus
|
||||
) : Listener<E>(), CompletableJob by Job(parentJob) {
|
||||
val bot: Bot by bot.unsafeWeakRef()
|
||||
|
||||
override suspend fun onEvent(event: E): ListeningStatus {
|
||||
if (isCompleted || isCancelled) return ListeningStatus.STOPPED
|
||||
@ -137,10 +104,11 @@ internal class HandlerWithSession<E : Subscribable> @PublishedApi internal const
|
||||
if (event !is BotEvent || event.bot !== bot) return ListeningStatus.LISTENING
|
||||
|
||||
return try {
|
||||
withContext(context) { bot.session.handler(event) }.also { if (it == ListeningStatus.STOPPED) complete() }
|
||||
withContext(context) { bot.handler(event) }.also { if (it == ListeningStatus.STOPPED) complete() }
|
||||
} catch (e: Throwable) {
|
||||
e.logStacktrace()
|
||||
//completeExceptionally(e)
|
||||
complete()
|
||||
ListeningStatus.STOPPED
|
||||
}
|
||||
}
|
||||
@ -151,24 +119,7 @@ internal class HandlerWithSession<E : Subscribable> @PublishedApi internal const
|
||||
*/
|
||||
internal suspend fun <E : Subscribable> KClass<out E>.listeners(): EventListeners<E> = EventListenerManger.get(this)
|
||||
|
||||
internal class EventListeners<E : Subscribable> : MutableList<Listener<E>> by mutableListOf() {
|
||||
/**
|
||||
* 主监听者列表.
|
||||
* 广播事件时使用这个锁.
|
||||
*/
|
||||
@JvmField
|
||||
val mainMutex = Mutex()
|
||||
/**
|
||||
* 缓存(监听)事件时使用的锁
|
||||
*/
|
||||
@JvmField
|
||||
val cacheMutex = Mutex()
|
||||
/**
|
||||
* 等待加入到主 list 的监听者. 务必使用 [cacheMutex]
|
||||
*/
|
||||
@JvmField
|
||||
val cache: MutableList<Listener<E>> = mutableListOf()
|
||||
|
||||
internal class EventListeners<E : Subscribable> : LockFreeLinkedList<Listener<E>>() {
|
||||
init {
|
||||
this::class.members.filterIsInstance<KFunction<*>>().forEach {
|
||||
if (it.name == "add") {
|
||||
@ -198,51 +149,26 @@ internal object EventListenerManger {
|
||||
|
||||
}
|
||||
|
||||
internal suspend fun <E : Subscribable> E.broadcastInternal(): E {
|
||||
// inline: NO extra Continuation
|
||||
internal suspend inline fun <E : Subscribable> E.broadcastInternal(): E {
|
||||
if (EventDisabled) return this
|
||||
|
||||
callListeners(this::class.listeners())
|
||||
callAndRemoveIfRequired(this::class.listeners())
|
||||
|
||||
applySuperListeners(this::class) { callListeners(it) }
|
||||
this::class.supertypes.forEach { superType ->
|
||||
if (Subscribable::class.isInstance(superType)) {
|
||||
// the super type is a child of Subscribable, then we can cast.
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
callAndRemoveIfRequired((superType.classifier as KClass<out Subscribable>).listeners())
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
private suspend inline fun <E : Subscribable> E.callListeners(listeners: EventListeners<in E>) {
|
||||
//自己持有, 则是在一个事件中
|
||||
if (listeners.mainMutex.holdsLock(listeners)) {
|
||||
callAndRemoveIfRequired(listeners)
|
||||
} else {
|
||||
while (!listeners.mainMutex.tryLock(listeners)) {
|
||||
delay(10)
|
||||
}
|
||||
try {
|
||||
callAndRemoveIfRequired(listeners)
|
||||
} finally {
|
||||
listeners.mainMutex.unlock(listeners)
|
||||
private suspend inline fun <E : Subscribable> E.callAndRemoveIfRequired(listeners: EventListeners<E>) {
|
||||
listeners.forEach {
|
||||
if (it.onEvent(this) == ListeningStatus.STOPPED) {
|
||||
listeners.remove(it) // atomic remove
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun <E : Subscribable> E.callAndRemoveIfRequired(listeners: EventListeners<in E>) = listeners.inlinedRemoveIf {
|
||||
if (it.lock.tryLock()) {
|
||||
try {
|
||||
it.onEvent(this) == ListeningStatus.STOPPED
|
||||
} finally {
|
||||
it.lock.unlock()
|
||||
}
|
||||
} else false
|
||||
}
|
||||
|
||||
/**
|
||||
* apply [block] to all the [EventListeners] in [clazz]'s superclasses
|
||||
*/
|
||||
private tailrec suspend fun <E : Subscribable> applySuperListeners(
|
||||
clazz: KClass<out E>,
|
||||
block: suspend (EventListeners<in E>) -> Unit
|
||||
) {
|
||||
val superEventClass =
|
||||
clazz.supertypes.map { it.classifier }.filterIsInstance<KClass<out Subscribable>>().firstOrNull() ?: return
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
block(superEventClass.listeners() as EventListeners<in E>)
|
||||
applySuperListeners(superEventClass, block)
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package net.mamoe.mirai.message
|
||||
|
||||
/**
|
||||
* QQ 自带表情
|
||||
*/
|
||||
inline class Face(val id: FaceId) : Message {
|
||||
override val stringValue: String get() = "[face${id.value}]"
|
||||
override fun toString(): String = stringValue
|
||||
|
||||
companion object Key : Message.Key<Face>
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package net.mamoe.mirai.message
|
||||
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.contact.QQ
|
||||
import net.mamoe.mirai.event.BroadcastControllable
|
||||
import net.mamoe.mirai.message.data.MessageChain
|
||||
import net.mamoe.mirai.utils.MiraiInternalAPI
|
||||
|
||||
class FriendMessage(
|
||||
bot: Bot,
|
||||
/**
|
||||
* 是否是在这次登录之前的消息, 即消息记录
|
||||
*/
|
||||
val previous: Boolean,
|
||||
override val sender: QQ,
|
||||
override val message: MessageChain
|
||||
) : MessagePacket<QQ, QQ>(bot), BroadcastControllable {
|
||||
/**
|
||||
* 是否应被自动广播. 此为内部 API
|
||||
*/
|
||||
@MiraiInternalAPI
|
||||
override val shouldBroadcast: Boolean
|
||||
get() = !previous
|
||||
|
||||
override val subject: QQ get() = sender
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package net.mamoe.mirai.message
|
||||
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.contact.Group
|
||||
import net.mamoe.mirai.contact.Member
|
||||
import net.mamoe.mirai.contact.MemberPermission
|
||||
import net.mamoe.mirai.message.data.At
|
||||
import net.mamoe.mirai.message.data.MessageChain
|
||||
import net.mamoe.mirai.utils.unsafeWeakRef
|
||||
|
||||
@Suppress("unused", "NOTHING_TO_INLINE")
|
||||
class GroupMessage(
|
||||
bot: Bot,
|
||||
group: Group,
|
||||
val senderName: String,
|
||||
/**
|
||||
* 发送方权限.
|
||||
*/
|
||||
val permission: MemberPermission,
|
||||
sender: Member,
|
||||
override val message: MessageChain
|
||||
) : MessagePacket<Member, Group>(bot) {
|
||||
val group: Group by group.unsafeWeakRef()
|
||||
override val sender: Member by sender.unsafeWeakRef()
|
||||
|
||||
/*
|
||||
01 00 09 01 00 06 66 61 69 6C 65 64 19 00 45 01 00 42 AA 02 3F 08 06 50 02 60 00 68 00 88 01 00 9A 01 31 08 0A 78 00 C8 01 00 F0 01 00 F8 01 00 90 02 00 C8 02 00 98 03 00 A0 03 02 B0 03 00 C0 03 00 D0 03 00 E8 03 02 8A 04 04 08 02 08 01 90 04 80 C8 10 0E 00 0E 01 00 04 00 00 08 E4 07 00 04 00 00 00 01 12 00 1E 02 00 09 E9 85 B1 E9 87 8E E6 98 9F 03 00 01 02 05 00 04 00 00 00 03 08 00 04 00 00 00 04
|
||||
*/
|
||||
override val subject: Group get() = group
|
||||
|
||||
inline fun At.member(): Member = group.getMember(this.target)
|
||||
inline fun Long.member(): Member = group.getMember(this)
|
||||
override fun toString(): String =
|
||||
"GroupMessage(group=${group.id}, senderName=$senderName, sender=${sender.id}, permission=${permission.name}, message=$message)"
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.message
|
||||
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.contact.*
|
||||
import net.mamoe.mirai.event.events.BotEvent
|
||||
import net.mamoe.mirai.message.data.*
|
||||
import net.mamoe.mirai.network.data.EventPacket
|
||||
import net.mamoe.mirai.network.data.ImageLink
|
||||
import net.mamoe.mirai.utils.*
|
||||
import net.mamoe.mirai.utils.internal.coerceAtLeastOrFail
|
||||
import kotlin.jvm.JvmName
|
||||
|
||||
/**
|
||||
* 平台相关扩展
|
||||
*/
|
||||
@UseExperimental(MiraiInternalAPI::class)
|
||||
expect abstract class MessagePacket<TSender : QQ, TSubject : Contact>(bot: Bot) : MessagePacketBase<TSender, TSubject>
|
||||
|
||||
@MiraiInternalAPI
|
||||
abstract class MessagePacketBase<TSender : QQ, TSubject : Contact>(_bot: Bot) : EventPacket, BotEvent() {
|
||||
override val bot: Bot by _bot.unsafeWeakRef()
|
||||
|
||||
/**
|
||||
* 消息事件主体.
|
||||
*
|
||||
* 对于好友消息, 这个属性为 [QQ] 的实例;
|
||||
* 对于群消息, 这个属性为 [Group] 的实例
|
||||
*
|
||||
* 在回复消息时, 可通过 [subject] 作为回复对象
|
||||
*/
|
||||
abstract val subject: TSubject
|
||||
|
||||
/**
|
||||
* 发送人
|
||||
*/
|
||||
abstract val sender: TSender
|
||||
|
||||
abstract val message: MessageChain
|
||||
|
||||
|
||||
// region Send to subject
|
||||
|
||||
/**
|
||||
* 给这个消息事件的主体发送消息
|
||||
* 对于好友消息事件, 这个方法将会给好友 ([subject]) 发送消息
|
||||
* 对于群消息事件, 这个方法将会给群 ([subject]) 发送消息
|
||||
*/
|
||||
suspend inline fun reply(message: MessageChain) = subject.sendMessage(message)
|
||||
|
||||
suspend inline fun reply(message: Message) = subject.sendMessage(message.chain())
|
||||
suspend inline fun reply(plain: String) = subject.sendMessage(plain.toMessage())
|
||||
|
||||
@JvmName("reply1")
|
||||
suspend inline fun String.reply() = reply(this)
|
||||
|
||||
@JvmName("reply1")
|
||||
suspend inline fun Message.reply() = reply(this)
|
||||
|
||||
@JvmName("reply1")
|
||||
suspend inline fun MessageChain.reply() = reply(this)
|
||||
|
||||
suspend inline fun ExternalImage.send() = this.sendTo(subject)
|
||||
|
||||
suspend inline fun ExternalImage.upload(): Image = this.upload(subject)
|
||||
suspend inline fun Image.send() = this.sendTo(subject)
|
||||
suspend inline fun ImageId.send() = this.sendTo(subject)
|
||||
suspend inline fun Message.send() = this.sendTo(subject)
|
||||
suspend inline fun String.send() = this.toMessage().sendTo(subject)
|
||||
|
||||
// endregion
|
||||
|
||||
// region Image download
|
||||
suspend inline fun Image.getLink(): ImageLink = with(bot) { getLink() }
|
||||
|
||||
suspend inline fun Image.downloadAsByteArray(): ByteArray = getLink().downloadAsByteArray()
|
||||
suspend inline fun Image.download(): ByteReadPacket = getLink().download()
|
||||
// endregion
|
||||
|
||||
fun At.qq(): QQ = bot.getQQ(this.target)
|
||||
|
||||
fun Int.qq(): QQ = bot.getQQ(this.coerceAtLeastOrFail(0).toLong())
|
||||
fun Long.qq(): QQ = bot.getQQ(this.coerceAtLeastOrFail(0))
|
||||
|
||||
suspend inline fun Int.group(): Group = bot.getGroup(this.coerceAtLeastOrFail(0).toLong())
|
||||
suspend inline fun Long.group(): Group = bot.getGroup(this.coerceAtLeastOrFail(0))
|
||||
suspend inline fun GroupId.group(): Group = bot.getGroup(this)
|
||||
suspend inline fun GroupInternalId.group(): Group = bot.getGroup(this)
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.message
|
||||
package net.mamoe.mirai.message.data
|
||||
|
||||
import net.mamoe.mirai.contact.QQ
|
||||
|
||||
@ -8,7 +8,7 @@ import net.mamoe.mirai.contact.QQ
|
||||
/**
|
||||
* At 一个人. 只能发送给一个群.
|
||||
*/
|
||||
inline class At(val target: UInt) : Message {
|
||||
inline class At(val target: Long) : Message {
|
||||
constructor(target: QQ) : this(target.id)
|
||||
|
||||
override val stringValue: String get() = "[@$target]" // TODO: 2019/11/25 使用群名称进行 at. 因为手机端只会显示这个文字
|
@ -1,14 +1,23 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
|
||||
|
||||
package net.mamoe.mirai.message
|
||||
package net.mamoe.mirai.message.data
|
||||
|
||||
import kotlin.jvm.JvmStatic
|
||||
|
||||
/**
|
||||
* QQ 自带表情
|
||||
*/
|
||||
inline class Face(val id: FaceId) : Message {
|
||||
override val stringValue: String get() = "[face${id.value}]"
|
||||
override fun toString(): String = stringValue
|
||||
|
||||
companion object Key : Message.Key<Face>
|
||||
}
|
||||
|
||||
/**
|
||||
* @author LamGC
|
||||
*/
|
||||
@Suppress("SpellCheckingInspection", "unused")
|
||||
inline class FaceId(inline val value: UByte) {
|
||||
@UseExperimental(ExperimentalUnsignedTypes::class)
|
||||
inline class FaceId constructor(inline val value: UByte) {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
val unknown: FaceId = FaceId(0xffu)
|
@ -1,10 +1,9 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.message
|
||||
package net.mamoe.mirai.message.data
|
||||
|
||||
import net.mamoe.mirai.contact.Contact
|
||||
import net.mamoe.mirai.contact.sendMessage
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.action.FriendImagePacket
|
||||
import net.mamoe.mirai.utils.ExternalImage
|
||||
|
||||
|
||||
@ -30,7 +29,8 @@ inline class ImageId0x06(override inline val value: String) : ImageId {
|
||||
/**
|
||||
* 一般是群的图片的 id.
|
||||
*/
|
||||
class ImageId0x03 constructor(override inline val value: String, inline val uniqueId: UInt, inline val height: Int, inline val width: Int) : ImageId {
|
||||
class ImageId0x03 constructor(override inline val value: String, inline val uniqueId: UInt, inline val height: Int, inline val width: Int) :
|
||||
ImageId {
|
||||
override fun toString(): String = "ImageId(value=$value, uniqueId=${uniqueId}, height=$height, width=$width)"
|
||||
|
||||
val md5: ByteArray
|
||||
@ -46,7 +46,8 @@ class ImageId0x03 constructor(override inline val value: String, inline val uniq
|
||||
inline fun ImageId(value: String): ImageId = ImageId0x06(value)
|
||||
|
||||
@Suppress("FunctionName", "NOTHING_TO_INLINE")
|
||||
inline fun ImageId(value: String, uniqueId: UInt, height: Int, width: Int): ImageId = ImageId0x03(value, uniqueId, height, width)
|
||||
inline fun ImageId(value: String, uniqueId: UInt, height: Int, width: Int): ImageId =
|
||||
ImageId0x03(value, uniqueId, height, width)
|
||||
|
||||
|
||||
/**
|
||||
@ -65,6 +66,7 @@ fun ImageId.checkLength() = check(value.length == 37 || value.length == 42) { "I
|
||||
fun ImageId.requireLength() = require(value.length == 37 || value.length == 42) { "Illegal ImageId length" }
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun ImageId.image(): Image = Image(this)
|
||||
inline fun ImageId.image(): Image =
|
||||
Image(this)
|
||||
|
||||
suspend inline fun ImageId.sendTo(contact: Contact) = contact.sendMessage(this.image())
|
@ -1,6 +1,6 @@
|
||||
@file:Suppress("MemberVisibilityCanBePrivate", "unused", "EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.message
|
||||
package net.mamoe.mirai.message.data
|
||||
|
||||
import net.mamoe.mirai.contact.Contact
|
||||
import net.mamoe.mirai.contact.sendMessage
|
@ -1,5 +1,6 @@
|
||||
package net.mamoe.mirai.message
|
||||
package net.mamoe.mirai.message.data
|
||||
|
||||
import net.mamoe.mirai.message.data.NullMessageChain.toString
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalAPI
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.contract
|
||||
@ -75,7 +76,8 @@ fun MessageChain(vararg messages: Message): MessageChain =
|
||||
* 构造 [MessageChain]
|
||||
*/
|
||||
@Suppress("FunctionName")
|
||||
fun MessageChain(messages: Iterable<Message>): MessageChain = MessageChainImpl(messages.toMutableList())
|
||||
fun MessageChain(messages: Iterable<Message>): MessageChain =
|
||||
MessageChainImpl(messages.toMutableList())
|
||||
|
||||
/**
|
||||
* 构造单元素的不可修改的 [MessageChain]. 内部类实现为 [SingleMessageChain]
|
||||
@ -106,13 +108,16 @@ fun SingleMessageChain(delegate: Message): MessageChain {
|
||||
* 否则将调用 [MessageChain] 构造一个 [MessageChainImpl]
|
||||
*/
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun Message.chain(): MessageChain = if (this is MessageChain) this else MessageChain(this)
|
||||
inline fun Message.chain(): MessageChain = if (this is MessageChain) this else MessageChain(
|
||||
this
|
||||
)
|
||||
|
||||
/**
|
||||
* 构造 [MessageChain]
|
||||
*/
|
||||
@Suppress("unused", "NOTHING_TO_INLINE")
|
||||
inline fun List<Message>.messageChain(): MessageChain = MessageChain(this)
|
||||
inline fun List<Message>.messageChain(): MessageChain =
|
||||
MessageChain(this)
|
||||
|
||||
|
||||
/**
|
||||
@ -230,51 +235,45 @@ class EmptyMessageChain : MessageChain {
|
||||
* Null 的 [MessageChain].
|
||||
* 它不包含任何元素, 也没有创建任何 list.
|
||||
*
|
||||
* - 所有 get 方法均抛出 [IndexOutOfBoundsException]
|
||||
* - 所有 add 方法均抛出 [UnsupportedOperationException]
|
||||
* - 其他判断类方法均 false 或 -1
|
||||
* 除 [toString] 外, 其他方法均 [error]
|
||||
*/
|
||||
object NullMessageChain : MessageChain {
|
||||
override fun subList(fromIndex: Int, toIndex: Int): MutableList<Message> = unsupported()
|
||||
override fun subList(fromIndex: Int, toIndex: Int): MutableList<Message> = error("accessing NullMessageChain")
|
||||
|
||||
override val stringValue: String
|
||||
get() = ""
|
||||
override val stringValue: String get() = "null"
|
||||
|
||||
override fun toString(): String = stringValue
|
||||
override fun toString(): String = "null"
|
||||
|
||||
override fun contains(sub: String): Boolean = false
|
||||
override fun contains(element: Message): Boolean = false
|
||||
override fun followedBy(tail: Message): MessageChain =
|
||||
MessageChainImpl(tail)
|
||||
override fun contains(sub: String): Boolean = error("accessing NullMessageChain")
|
||||
override fun contains(element: Message): Boolean = error("accessing NullMessageChain")
|
||||
override fun followedBy(tail: Message): MessageChain = error("accessing NullMessageChain")
|
||||
|
||||
override val size: Int = 0
|
||||
override fun containsAll(elements: Collection<Message>): Boolean = false
|
||||
override fun get(index: Int): Message = throw IndexOutOfBoundsException()
|
||||
override fun indexOf(element: Message): Int = -1
|
||||
override fun isEmpty(): Boolean = true
|
||||
override fun iterator(): MutableIterator<Message> =
|
||||
EmptyMutableIterator()
|
||||
override val size: Int get() = error("accessing NullMessageChain")
|
||||
override fun containsAll(elements: Collection<Message>): Boolean = error("accessing NullMessageChain")
|
||||
override fun get(index: Int): Message = error("accessing NullMessageChain")
|
||||
override fun indexOf(element: Message): Int = error("accessing NullMessageChain")
|
||||
override fun isEmpty(): Boolean = error("accessing NullMessageChain")
|
||||
override fun iterator(): MutableIterator<Message> = error("accessing NullMessageChain")
|
||||
|
||||
override fun lastIndexOf(element: Message): Int = -1
|
||||
override fun add(element: Message): Boolean = unsupported()
|
||||
override fun add(index: Int, element: Message) = throw IndexOutOfBoundsException(index.toString())
|
||||
override fun addAll(index: Int, elements: Collection<Message>): Boolean =
|
||||
throw IndexOutOfBoundsException(index.toString())
|
||||
override fun lastIndexOf(element: Message): Int = error("accessing NullMessageChain")
|
||||
override fun add(element: Message): Boolean = error("accessing NullMessageChain")
|
||||
override fun add(index: Int, element: Message) = error("accessing NullMessageChain")
|
||||
override fun addAll(index: Int, elements: Collection<Message>): Boolean = error("accessing NullMessageChain")
|
||||
|
||||
override fun addAll(elements: Collection<Message>): Boolean = unsupported()
|
||||
override fun clear() {}
|
||||
override fun listIterator(): MutableListIterator<Message> =
|
||||
EmptyMutableListIterator()
|
||||
override fun addAll(elements: Collection<Message>): Boolean = error("accessing NullMessageChain")
|
||||
override fun clear() {
|
||||
error("accessing NullMessageChain")
|
||||
}
|
||||
|
||||
override fun listIterator(index: Int): MutableListIterator<Message> =
|
||||
throw IndexOutOfBoundsException(index.toString())
|
||||
override fun listIterator(): MutableListIterator<Message> = error("accessing NullMessageChain")
|
||||
|
||||
override fun remove(element: Message): Boolean = false
|
||||
override fun removeAll(elements: Collection<Message>): Boolean = false
|
||||
override fun removeAt(index: Int): Message = throw IndexOutOfBoundsException(index.toString())
|
||||
override fun retainAll(elements: Collection<Message>): Boolean = false
|
||||
override fun set(index: Int, element: Message): Message = throw IndexOutOfBoundsException(index.toString())
|
||||
private fun unsupported(): Nothing = throw UnsupportedOperationException()
|
||||
override fun listIterator(index: Int): MutableListIterator<Message> = error("accessing NullMessageChain")
|
||||
|
||||
override fun remove(element: Message): Boolean = error("accessing NullMessageChain")
|
||||
override fun removeAll(elements: Collection<Message>): Boolean = error("accessing NullMessageChain")
|
||||
override fun removeAt(index: Int): Message = error("accessing NullMessageChain")
|
||||
override fun retainAll(elements: Collection<Message>): Boolean = error("accessing NullMessageChain")
|
||||
override fun set(index: Int, element: Message): Message = error("accessing NullMessageChain")
|
||||
}
|
||||
|
||||
/**
|
@ -1,4 +1,4 @@
|
||||
package net.mamoe.mirai.message
|
||||
package net.mamoe.mirai.message.data
|
||||
|
||||
|
||||
inline class PlainText(override val stringValue: String) : Message {
|
@ -1,13 +1,14 @@
|
||||
@file:Suppress("MemberVisibilityCanBePrivate")
|
||||
|
||||
package net.mamoe.mirai.message
|
||||
package net.mamoe.mirai.message.data
|
||||
|
||||
/**
|
||||
* XML 消息, 如分享, 卡片等.
|
||||
*
|
||||
* @see buildXMLMessage
|
||||
*/
|
||||
inline class XMLMessage(override val stringValue: String) : Message, SingleOnly {
|
||||
inline class XMLMessage(override val stringValue: String) : Message,
|
||||
SingleOnly {
|
||||
override fun followedBy(tail: Message): Nothing = error("XMLMessage Message cannot be followed")
|
||||
override fun toString(): String = stringValue
|
||||
}
|
||||
@ -16,7 +17,8 @@ inline class XMLMessage(override val stringValue: String) : Message, SingleOnly
|
||||
* 构造一条 XML 消息
|
||||
*/
|
||||
@XMLDsl
|
||||
inline fun buildXMLMessage(block: @XMLDsl XMLMessageBuilder.() -> Unit): XMLMessage = XMLMessage(XMLMessageBuilder().apply(block).text)
|
||||
inline fun buildXMLMessage(block: @XMLDsl XMLMessageBuilder.() -> Unit): XMLMessage =
|
||||
XMLMessage(XMLMessageBuilder().apply(block).text)
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
@XMLDsl
|
@ -3,7 +3,8 @@
|
||||
package net.mamoe.mirai.message.internal
|
||||
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.message.*
|
||||
import net.mamoe.mirai.message.MessageType
|
||||
import net.mamoe.mirai.message.data.*
|
||||
import net.mamoe.mirai.utils.io.*
|
||||
import net.mamoe.mirai.utils.unzip
|
||||
|
||||
@ -27,7 +28,7 @@ internal fun IoBuffer.parsePlainTextOrAt(): Message {
|
||||
PlainText(msg)
|
||||
} else {
|
||||
discardExact(10)
|
||||
At(readUInt())
|
||||
At(readQQ())
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,13 +39,13 @@ internal fun IoBuffer.parseLongText0x19(): PlainText {
|
||||
//AA 02 33 50 00 60 00 68 00 9A 01 2A 08 09 20 CB 50 78 80 80 04 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 D3 02 A0 03 10 B0 03 00 C0 03 AF 9C 01 D0 03 00 E8 03 00
|
||||
//AA 02 30 50 00 60 00 68 00 9A 01 27 08 0A 78 A7 C0 04 80 01 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 00 A0 03 00 B0 03 00 C0 03 00 D0 03 00 E8 03 00
|
||||
// 应该是手机发送时的字体或气泡之类的
|
||||
// println("parseLongText0x19.raw=${raw.toUHexString()}")
|
||||
// println("parseLongText0x19.raw=${raw.toHexString()}")
|
||||
return PlainText("")
|
||||
}
|
||||
|
||||
internal fun IoBuffer.parseMessageImage0x06(): Image {
|
||||
discardExact(1)
|
||||
//MiraiLogger.debug(this.toUHexString())
|
||||
//MiraiLogger.debug(this.toHexString())
|
||||
val filenameLength = readShort()
|
||||
|
||||
discardExact(filenameLength.toInt())
|
||||
@ -199,7 +200,7 @@ internal fun ByteReadPacket.readMessage(): Message? {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ByteReadPacket.readMessageChain(): MessageChain {
|
||||
fun ByteReadPacket.readMessageChain(): MessageChain {
|
||||
val chain = MessageChain()
|
||||
do {
|
||||
if (this.remaining == 0L) {
|
||||
@ -209,7 +210,7 @@ internal fun ByteReadPacket.readMessageChain(): MessageChain {
|
||||
return chain
|
||||
}
|
||||
|
||||
internal fun MessageChain.toPacket(): ByteReadPacket = buildPacket {
|
||||
fun MessageChain.toPacket(): ByteReadPacket = buildPacket {
|
||||
this@toPacket.forEach { message ->
|
||||
writePacket(with(message) {
|
||||
when (this) {
|
||||
@ -244,7 +245,7 @@ internal fun MessageChain.toPacket(): ByteReadPacket = buildPacket {
|
||||
writeShortLVString(stringValue) // 这个应该是 "@群名", 手机上面会显示这个消息, 电脑会显示下面那个
|
||||
// 06 00 0D 00 01 00 00 00 08 00 76 E4 B8 DD 00 00
|
||||
writeHex("06 00 0D 00 01 00 00 00 08 00")
|
||||
writeUInt(target)
|
||||
writeQQ(target)
|
||||
writeZero(2)
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,11 @@
|
||||
@file:Suppress("EXPERIMENTAL_API_USAGE")
|
||||
|
||||
package net.mamoe.mirai.network
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CompletableJob
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.network.protocol.timpc.handler.DataPacketSocketAdapter
|
||||
import net.mamoe.mirai.network.protocol.timpc.handler.TemporaryPacketHandler
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.OutgoingPacket
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.Packet
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.login.HeartbeatPacket
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.login.RequestSKeyPacket
|
||||
import net.mamoe.mirai.network.protocol.timpc.packet.login.LoginResult
|
||||
import net.mamoe.mirai.utils.MiraiInternalAPI
|
||||
import net.mamoe.mirai.utils.io.PlatformDatagramChannel
|
||||
|
||||
/**
|
||||
@ -33,29 +27,41 @@ import net.mamoe.mirai.utils.io.PlatformDatagramChannel
|
||||
* A BotNetworkHandler is used to connect with Tencent servers.
|
||||
*/
|
||||
@Suppress("PropertyName")
|
||||
interface BotNetworkHandler<Socket : DataPacketSocketAdapter> : CoroutineScope {
|
||||
val socket: Socket
|
||||
val bot: Bot
|
||||
abstract class BotNetworkHandler : CoroutineScope {
|
||||
abstract val bot: Bot
|
||||
|
||||
val supervisor: CompletableJob
|
||||
|
||||
val session: BotSession
|
||||
abstract val supervisor: CompletableJob
|
||||
|
||||
/**
|
||||
* 依次尝试登录到可用的服务器. 在任一服务器登录完成后返回登录结果
|
||||
* 本函数将挂起直到登录成功.
|
||||
*/
|
||||
suspend fun login(): LoginResult
|
||||
abstract suspend fun login()
|
||||
|
||||
/**
|
||||
* 等待直到与服务器断开连接. 若未连接则立即返回
|
||||
*/
|
||||
suspend fun awaitDisconnection()
|
||||
abstract suspend fun awaitDisconnection()
|
||||
|
||||
/**
|
||||
* 关闭网络接口, 停止所有有关协程和任务
|
||||
*/
|
||||
fun close(cause: Throwable? = null) {
|
||||
open fun close(cause: Throwable? = null) {
|
||||
supervisor.cancel(CancellationException("handler closed", cause))
|
||||
}
|
||||
|
||||
/*
|
||||
@PublishedApi
|
||||
internal abstract fun CoroutineScope.QQ(bot: Bot, id: Long, coroutineContext: CoroutineContext): QQ
|
||||
|
||||
@PublishedApi
|
||||
internal abstract fun CoroutineScope.Group(bot: Bot, groupId: GroupId, info: RawGroupInfo, context: CoroutineContext): Group
|
||||
|
||||
@PublishedApi
|
||||
internal abstract fun Group.Member(delegate: QQ, permission: MemberPermission, coroutineContext: CoroutineContext): Member
|
||||
|
||||
|
||||
*/
|
||||
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user