1
0
mirror of https://github.com/mamoe/mirai.git synced 2025-04-25 04:50:26 +08:00
This commit is contained in:
Him188 2019-12-19 17:25:49 +08:00
parent da18aafa2f
commit 2f67f8363b
161 changed files with 3240 additions and 2952 deletions
README.md
mirai-api-http
build.gradle.kts
src/main/kotlin/net.mamoe.mirai.api.http
mirai-core-timpc
mirai-core/src

View File

@ -31,17 +31,18 @@ repositories{
您需要将 `VERSION` 替换为最新的版本(如 `0.5.1`): [![Download](https://api.bintray.com/packages/him188moe/mirai/mirai-core/images/download.svg)](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

View File

@ -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))

View File

@ -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()
}

View 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
}
}
}
}

View File

@ -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()
}

View File

@ -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()

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
)
}
}

View File

@ -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>()
}
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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
/**

View File

@ -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)
}
}

View File

@ -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 版本

View File

@ -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)

View File

@ -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,

View File

@ -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
/**
* 被忽略的数据包.
*/

View File

@ -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)
}

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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.
}
}

View File

@ -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
}

View File

@ -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(

View File

@ -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

View File

@ -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()

View File

@ -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 {

View File

@ -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

View File

@ -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())

View File

@ -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()
}

View File

@ -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
}

View File

@ -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

View File

@ -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)

View File

@ -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> {

View File

@ -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 恒定

View File

@ -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())
}
}

View File

@ -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)

View File

@ -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

View File

@ -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 {

View File

@ -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()

View File

@ -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
}
}
}

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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())

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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 实现
/*

View File

@ -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

View File

@ -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)

View File

@ -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")

View File

@ -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()}")

View File

@ -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) }
}

View File

@ -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()

View File

@ -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
)

View File

@ -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() {
}

View File

@ -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

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="net.mamoe.mirai.timpc">
</manifest>

View File

@ -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)

View File

@ -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() }
}

View File

@ -1,2 +0,0 @@
package net.mamoe.mirai.network

View File

@ -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)
/**

View 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())
}

View 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];
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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()
}
}
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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)

View File

@ -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
}
)

View File

@ -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 对象.

View File

@ -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)
}

View File

@ -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

View File

@ -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", "")})"
}

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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>
}

View File

@ -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
}

View File

@ -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)"
}

View File

@ -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)
}

View File

@ -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. 因为手机端只会显示这个文字

View File

@ -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)

View File

@ -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())

View File

@ -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

View File

@ -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")
}
/**

View File

@ -1,4 +1,4 @@
package net.mamoe.mirai.message
package net.mamoe.mirai.message.data
inline class PlainText(override val stringValue: String) : Message {

View File

@ -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

View File

@ -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)
}
}

View File

@ -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