Commit 71c86099 authored by jiahua.liu's avatar jiahua.liu

Merge branch 'master' of https://github.com/mamoe/mirai

 Conflicts:
	mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/events/BotEvents.kt
parents fbfb2610 905d4ede
...@@ -8,12 +8,12 @@ kotlin.parallel.tasks.in.project=true ...@@ -8,12 +8,12 @@ kotlin.parallel.tasks.in.project=true
kotlinVersion=1.3.61 kotlinVersion=1.3.61
# kotlin libraries # kotlin libraries
serializationVersion=0.14.0 serializationVersion=0.14.0
coroutinesVersion=1.3.2 coroutinesVersion=1.3.3
atomicFuVersion=0.14.1 atomicFuVersion=0.14.1
kotlinXIoVersion=0.1.16 kotlinXIoVersion=0.1.16
coroutinesIoVersion=0.1.16 coroutinesIoVersion=0.1.16
# utility # utility
ktorVersion=1.2.6 ktorVersion=1.3.1
klockVersion=1.7.0 klockVersion=1.7.0
# gradle plugin # gradle plugin
protobufJavaVersion=3.10.0 protobufJavaVersion=3.10.0
\ No newline at end of file
...@@ -4,7 +4,8 @@ import io.ktor.application.Application ...@@ -4,7 +4,8 @@ import io.ktor.application.Application
import io.ktor.application.call import io.ktor.application.call
import io.ktor.routing.routing import io.ktor.routing.routing
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import net.mamoe.mirai.api.http.data.* import net.mamoe.mirai.api.http.data.PermissionDeniedException
import net.mamoe.mirai.api.http.data.StateCode
import net.mamoe.mirai.api.http.data.common.DTO import net.mamoe.mirai.api.http.data.common.DTO
import net.mamoe.mirai.api.http.data.common.VerifyDTO import net.mamoe.mirai.api.http.data.common.VerifyDTO
import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.Group
...@@ -63,7 +64,7 @@ fun Application.groupManageModule() { ...@@ -63,7 +64,7 @@ fun Application.groupManageModule() {
val group = dto.session.bot.getGroup(dto.target) val group = dto.session.bot.getGroup(dto.target)
with(dto.config) { with(dto.config) {
name?.let { group.name = it } name?.let { group.name = it }
announcement?.let { group.announcement = it } announcement?.let { group.entranceAnnouncement = it }
confessTalk?.let { group.confessTalk = it } confessTalk?.let { group.confessTalk = it }
allowMemberInvite?.let { group.allowMemberInvite = it } allowMemberInvite?.let { group.allowMemberInvite = it }
// TODO: 待core接口实现设置可改 // TODO: 待core接口实现设置可改
...@@ -84,7 +85,7 @@ fun Application.groupManageModule() { ...@@ -84,7 +85,7 @@ fun Application.groupManageModule() {
miraiVerify<MemberInfoDTO>("/memberInfo") { dto -> miraiVerify<MemberInfoDTO>("/memberInfo") { dto ->
val member = dto.session.bot.getGroup(dto.target)[dto.memberId] val member = dto.session.bot.getGroup(dto.target)[dto.memberId]
with(dto.info) { with(dto.info) {
name?.let { member.groupCard = it } name?.let { member.nameCard = it }
specialTitle?.let { member.specialTitle = it } specialTitle?.let { member.specialTitle = it }
} }
call.respondStateCode(StateCode.Success) call.respondStateCode(StateCode.Success)
...@@ -127,7 +128,7 @@ private data class GroupDetailDTO( ...@@ -127,7 +128,7 @@ private data class GroupDetailDTO(
val anonymousChat: Boolean? = null val anonymousChat: Boolean? = null
) : DTO { ) : DTO {
constructor(group: Group) : this( constructor(group: Group) : this(
group.name, group.announcement, group.confessTalk, group.allowMemberInvite, group.name, group.entranceAnnouncement, group.confessTalk, group.allowMemberInvite,
group.autoApprove, group.anonymousChat group.autoApprove, group.anonymousChat
) )
} }
...@@ -145,5 +146,5 @@ private data class MemberDetailDTO( ...@@ -145,5 +146,5 @@ private data class MemberDetailDTO(
val name: String? = null, val name: String? = null,
val specialTitle: String? = null val specialTitle: String? = null
) : DTO { ) : DTO {
constructor(member: Member) : this(member.groupCard, member.specialTitle) constructor(member: Member) : this(member.nameCard, member.specialTitle)
} }
...@@ -93,10 +93,10 @@ kotlin { ...@@ -93,10 +93,10 @@ kotlin {
val androidTest by getting { val androidTest by getting {
dependencies { dependencies {
api(kotlin("test", kotlinVersion)) implementation(kotlin("test", kotlinVersion))
api(kotlin("test-junit", kotlinVersion)) implementation(kotlin("test-junit", kotlinVersion))
api(kotlin("test-annotations-common")) implementation(kotlin("test-annotations-common"))
api(kotlin("test-common")) implementation(kotlin("test-common"))
} }
} }
} }
......
...@@ -12,7 +12,10 @@ package net.mamoe.mirai.qqandroid ...@@ -12,7 +12,10 @@ package net.mamoe.mirai.qqandroid
import kotlinx.io.core.ByteReadPacket import kotlinx.io.core.ByteReadPacket
import net.mamoe.mirai.BotAccount import net.mamoe.mirai.BotAccount
import net.mamoe.mirai.BotImpl import net.mamoe.mirai.BotImpl
import net.mamoe.mirai.contact.* import net.mamoe.mirai.contact.ContactList
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.contact.filteringGetOrNull
import net.mamoe.mirai.data.AddFriendResult import net.mamoe.mirai.data.AddFriendResult
import net.mamoe.mirai.event.events.BotEvent import net.mamoe.mirai.event.events.BotEvent
import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.message.data.Image
...@@ -40,12 +43,7 @@ internal abstract class QQAndroidBotBase constructor( ...@@ -40,12 +43,7 @@ internal abstract class QQAndroidBotBase constructor(
override val uin: Long get() = client.uin override val uin: Long get() = client.uin
override val qqs: ContactList<QQ> = ContactList(LockFreeLinkedList()) override val qqs: ContactList<QQ> = ContactList(LockFreeLinkedList())
val selfQQ: QQ by lazy { QQ(uin) } override val selfQQ: QQ by lazy { QQ(uin) }
override fun getFriend(id: Long): QQ {
if (id == uin) return selfQQ
return qqs.delegate[id]
}
override fun QQ(id: Long): QQ { override fun QQ(id: Long): QQ {
return QQImpl(this as QQAndroidBot, coroutineContext, id) return QQImpl(this as QQAndroidBot, coroutineContext, id)
...@@ -57,15 +55,11 @@ internal abstract class QQAndroidBotBase constructor( ...@@ -57,15 +55,11 @@ internal abstract class QQAndroidBotBase constructor(
override val groups: ContactList<Group> = ContactList(LockFreeLinkedList()) override val groups: ContactList<Group> = ContactList(LockFreeLinkedList())
// internally visible only
fun getGroupByUin(uin: Long): Group { fun getGroupByUin(uin: Long): Group {
return groups.delegate.filteringGetOrNull { (it as GroupImpl).uin == uin } ?: throw NoSuchElementException("Can not found group with ID=${uin}") return groups.delegate.filteringGetOrNull { (it as GroupImpl).uin == uin } ?: throw NoSuchElementException("Can not found group with ID=${uin}")
} }
override fun getGroup(id: Long): Group {
return groups.delegate.getOrNull(id)
?: throw NoSuchElementException("Can not found group with GroupCode=${id}")
}
override fun onEvent(event: BotEvent): Boolean { override fun onEvent(event: BotEvent): Boolean {
return firstLoginSucceed return firstLoginSucceed
} }
......
...@@ -10,10 +10,10 @@ ...@@ -10,10 +10,10 @@
package net.mamoe.mirai.qqandroid.event package net.mamoe.mirai.qqandroid.event
import net.mamoe.mirai.data.Packet import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.event.Cancellable import net.mamoe.mirai.event.AbstractCancellableEvent
import net.mamoe.mirai.event.Event import net.mamoe.mirai.event.Event
/** /**
* 接收到数据包 * 接收到数据包
*/ */
class PacketReceivedEvent(val packet: Packet) : Event(), Cancellable data class PacketReceivedEvent(val packet: Packet) : Event, AbstractCancellableEvent()
\ No newline at end of file \ No newline at end of file
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.qqandroid.io
import kotlinx.io.charsets.Charset
import kotlinx.io.core.*
import kotlin.experimental.or
import kotlin.reflect.KClass
@PublishedApi
internal val CharsetGBK = Charset.forName("GBK")
@PublishedApi
internal val CharsetUTF8 = Charset.forName("UTF8")
inline fun buildJcePacket(stringCharset: Charset = CharsetGBK, block: JceOutput.() -> Unit): ByteReadPacket {
return JceOutput(stringCharset).apply(block).build()
}
inline fun BytePacketBuilder.writeJcePacket(stringCharset: Charset = CharsetGBK, block: JceOutput.() -> Unit) {
return this.writePacket(buildJcePacket(stringCharset, block))
}
fun jceStruct(tag: Int, struct: JceStruct): ByteArray{
return buildJcePacket {
writeJceStruct(struct, tag)
}.readBytes()
}
fun <K, V> jceMap(tag: Int, vararg entries: Pair<K, V>): ByteArray {
return buildJcePacket {
writeMap(mapOf(*entries), tag)
}.readBytes()
}
/**
*
* From: com.qq.taf.jce.JceOutputStream
*/
@Suppress("unused", "MemberVisibilityCanBePrivate")
@UseExperimental(ExperimentalIoApi::class)
class JceOutput(
private val stringCharset: Charset = CharsetGBK
) {
private val output: BytePacketBuilder = BytePacketBuilder()
fun build(): ByteReadPacket = output.build()
fun close() = output.close()
fun flush() = output.flush()
fun writeByte(v: Byte, tag: Int) {
if (v.toInt() == 0) {
writeHead(ZERO_TYPE, tag)
} else {
writeHead(BYTE, tag)
output.writeByte(v)
}
}
fun writeDouble(v: Double, tag: Int) {
writeHead(DOUBLE, tag)
output.writeDouble(v)
}
fun writeFloat(v: Float, tag: Int) {
writeHead(FLOAT, tag)
output.writeFloat(v)
}
fun writeFully(src: ByteArray, tag: Int) {
writeHead(SIMPLE_LIST, tag)
writeHead(BYTE, 0)
writeInt(src.size, 0)
output.writeFully(src)
}
fun writeFully(src: DoubleArray, tag: Int) {
writeHead(LIST, tag)
writeInt(src.size, 0)
src.forEach {
writeDouble(it, 0)
}
}
fun writeFully(src: FloatArray, tag: Int) {
writeHead(LIST, tag)
writeInt(src.size, 0)
src.forEach {
writeFloat(it, 0)
}
}
fun writeFully(src: IntArray, tag: Int) {
writeHead(LIST, tag)
writeInt(src.size, 0)
src.forEach {
writeInt(it, 0)
}
}
fun writeFully(src: LongArray, tag: Int) {
writeHead(LIST, tag)
writeInt(src.size, 0)
src.forEach {
writeLong(it, 0)
}
}
fun writeFully(src: ShortArray, tag: Int) {
writeHead(LIST, tag)
writeInt(src.size, 0)
src.forEach {
writeShort(it, 0)
}
}
fun writeFully(src: BooleanArray, tag: Int) {
writeHead(LIST, tag)
writeInt(src.size, 0)
src.forEach {
writeBoolean(it, 0)
}
}
fun <T> writeFully(src: Array<T>, tag: Int) {
writeHead(LIST, tag)
writeInt(src.size, 0)
src.forEach {
writeObject(it, 0)
}
}
fun writeInt(v: Int, tag: Int) {
if (v in Short.MIN_VALUE..Short.MAX_VALUE) {
writeShort(v.toShort(), tag)
} else {
writeHead(INT, tag)
output.writeInt(v)
}
}
fun writeLong(v: Long, tag: Int) {
if (v in Int.MIN_VALUE..Int.MAX_VALUE) {
writeInt(v.toInt(), tag)
} else {
writeHead(LONG, tag)
output.writeLong(v)
}
}
fun writeShort(v: Short, tag: Int) {
if (v in Byte.MIN_VALUE..Byte.MAX_VALUE) {
writeByte(v.toByte(), tag)
} else {
writeHead(SHORT, tag)
output.writeShort(v)
}
}
fun writeBoolean(v: Boolean, tag: Int) {
this.writeByte(if (v) 1 else 0, tag)
}
fun writeString(v: String, tag: Int) {
val array = v.toByteArray(stringCharset)
if (array.size > 255) {
writeHead(STRING4, tag)
output.writeInt(array.size)
output.writeFully(array)
} else {
writeHead(STRING1, tag)
output.writeByte(array.size.toByte())
output.writeFully(array)
}
}
fun <K, V> writeMap(map: Map<K, V>, tag: Int) {
writeHead(MAP, tag)
if (map.isEmpty()) {
writeInt(0, 0)
} else {
writeInt(map.size, 0)
map.forEach { (key, value) ->
writeObject(key, 0)
writeObject(value, 1)
}
}
}
fun writeCollection(collection: Collection<*>?, tag: Int) {
writeHead(LIST, tag)
if (collection == null || collection.isEmpty()) {
writeInt(0, 0)
} else {
writeInt(collection.size, 0)
collection.forEach {
writeObject(it, 0)
}
}
}
fun writeJceStruct(v: JceStruct, tag: Int) {
writeHead(STRUCT_BEGIN, tag)
v.writeTo(this)
writeHead(STRUCT_END, 0)
}
fun <T> writeObject(v: T, tag: Int) {
when (v) {
is Byte -> writeByte(v, tag)
is Short -> writeShort(v, tag)
is Int -> writeInt(v, tag)
is Long -> writeLong(v, tag)
is Float -> writeFloat(v, tag)
is Double -> writeDouble(v, tag)
is JceStruct -> writeJceStruct(v, tag)
is ByteArray -> writeFully(v, tag)
is Collection<*> -> writeCollection(v, tag)
is Boolean -> writeBoolean(v, tag)
is Map<*, *> -> writeMap(v, tag)
is IntArray -> writeFully(v, tag)
is ShortArray -> writeFully(v, tag)
is BooleanArray -> writeFully(v, tag)
is LongArray -> writeFully(v, tag)
is FloatArray -> writeFully(v, tag)
is DoubleArray -> writeFully(v, tag)
is Array<*> -> writeFully(v, tag)
is String -> writeString(v, tag)
//
// is ByteReadPacket -> ByteArrayPool.useInstance {
// v.readAvailable(it)
// writeFully(it, tag)
// }
else -> error("unsupported type: ${v.getClassName()}")
}
}
fun write(v: Int, tag: Int) = writeInt(v, tag)
fun write(v: Byte, tag: Int) = writeByte(v, tag)
fun write(v: Short, tag: Int) = writeShort(v, tag)
fun write(v: Long, tag: Int) = writeLong(v, tag)
fun write(v: Float, tag: Int) = writeFloat(v, tag)
fun write(v: Double, tag: Int) = writeDouble(v, tag)
fun write(v: String, tag: Int) = writeString(v, tag)
fun write(v: Boolean, tag: Int) = writeBoolean(v, tag)
fun write(v: Collection<*>, tag: Int) = writeCollection(v, tag)
fun write(v: Map<*, *>, tag: Int) = writeMap(v, tag)
fun write(v: ByteArray, tag: Int) = writeFully(v, tag)
fun write(v: IntArray, tag: Int) = writeFully(v, tag)
fun write(v: BooleanArray, tag: Int) = writeFully(v, tag)
fun write(v: LongArray, tag: Int) = writeFully(v, tag)
fun write(v: ShortArray, tag: Int) = writeFully(v, tag)
fun write(v: Array<*>, tag: Int) = writeFully(v, tag)
fun write(v: FloatArray, tag: Int) = writeFully(v, tag)
fun write(v: DoubleArray, tag: Int) = writeFully(v, tag)
@PublishedApi
internal companion object {
const val BYTE: Int = 0
const val DOUBLE: Int = 5
const val FLOAT: Int = 4
const val INT: Int = 2
const val JCE_MAX_STRING_LENGTH = 104857600
const val LIST: Int = 9
const val LONG: Int = 3
const val MAP: Int = 8
const val SHORT: Int = 1
const val SIMPLE_LIST: Int = 13
const val STRING1: Int = 6
const val STRING4: Int = 7
const val STRUCT_BEGIN: Int = 10
const val STRUCT_END: Int = 11
const val ZERO_TYPE: Int = 12
private fun Any?.getClassName(): KClass<out Any> = if (this == null) Unit::class else this::class
}
@PublishedApi
internal fun writeHead(type: Int, tag: Int) {
if (tag < 15) {
this.output.writeByte(((tag shl 4) or type).toByte())
return
}
if (tag < 256) {
this.output.writeByte((type.toByte() or 0xF0.toByte()))
this.output.writeByte(tag.toByte())
return
}
throw JceEncodeException("tag is too large: $tag")
}
}
class JceEncodeException(message: String) : RuntimeException(message)
\ No newline at end of file
...@@ -9,6 +9,4 @@ ...@@ -9,6 +9,4 @@
package net.mamoe.mirai.qqandroid.io package net.mamoe.mirai.qqandroid.io
interface JceStruct { interface JceStruct
fun writeTo(output: JceOutput) = Unit \ No newline at end of file
}
\ No newline at end of file
...@@ -274,12 +274,30 @@ internal class NotOnlineImageFromServer( ...@@ -274,12 +274,30 @@ internal class NotOnlineImageFromServer(
@UseExperimental(ExperimentalUnsignedTypes::class, MiraiInternalAPI::class) @UseExperimental(ExperimentalUnsignedTypes::class, MiraiInternalAPI::class)
internal fun MsgComm.Msg.toMessageChain(): MessageChain { internal fun MsgComm.Msg.toMessageChain(): MessageChain {
val elems = this.msgBody.richText.elems val elements = this.msgBody.richText.elems
val message = MessageChain(initialCapacity = elems.size + 1) val message = MessageChain(initialCapacity = elements.size + 1)
message.add(MessageSourceFromMsg(delegate = this)) message.add(MessageSourceFromMsg(delegate = this))
elements.joinToMessageChain(message)
return message
}
// These two functions are not the same.
elems.forEach { @UseExperimental(ExperimentalUnsignedTypes::class, MiraiInternalAPI::class)
internal fun ImMsgBody.SourceMsg.toMessageChain(): MessageChain {
val elements = this.elems!!
val message = MessageChain(initialCapacity = elements.size + 1)
message.add(MessageSourceFromServer(delegate = this))
elements.joinToMessageChain(message)
return message
}
@UseExperimental(MiraiInternalAPI::class, ExperimentalUnsignedTypes::class)
internal fun List<ImMsgBody.Elem>.joinToMessageChain(message: MessageChain) {
this.forEach {
when { when {
it.srcMsg != null -> message.add(QuoteReply(MessageSourceFromServer(it.srcMsg))) it.srcMsg != null -> message.add(QuoteReply(MessageSourceFromServer(it.srcMsg)))
it.notOnlineImage != null -> message.add(NotOnlineImageFromServer(it.notOnlineImage)) it.notOnlineImage != null -> message.add(NotOnlineImageFromServer(it.notOnlineImage))
...@@ -296,5 +314,4 @@ internal fun MsgComm.Msg.toMessageChain(): MessageChain { ...@@ -296,5 +314,4 @@ internal fun MsgComm.Msg.toMessageChain(): MessageChain {
} }
} }
return message
} }
\ No newline at end of file
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
package net.mamoe.mirai.qqandroid.message package net.mamoe.mirai.qqandroid.message
import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageSource import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.qqandroid.io.serialization.loadAs import net.mamoe.mirai.qqandroid.io.serialization.loadAs
import net.mamoe.mirai.qqandroid.io.serialization.toByteArray import net.mamoe.mirai.qqandroid.io.serialization.toByteArray
...@@ -20,8 +21,10 @@ import net.mamoe.mirai.qqandroid.network.protocol.data.proto.SourceMsg ...@@ -20,8 +21,10 @@ import net.mamoe.mirai.qqandroid.network.protocol.data.proto.SourceMsg
internal inline class MessageSourceFromServer( internal inline class MessageSourceFromServer(
val delegate: ImMsgBody.SourceMsg val delegate: ImMsgBody.SourceMsg
) : MessageSource { ) : MessageSource {
override val messageUid: Long override val messageUid: Long get() = delegate.pbReserve.loadAs(SourceMsg.ResvAttr.serializer()).origUids!!
get() = delegate.pbReserve.loadAs(SourceMsg.ResvAttr.serializer()).origUids!! override val sourceMessage: MessageChain get() = delegate.toMessageChain()
override val senderId: Long get() = delegate.senderUin
override val groupId: Long get() = Group.calculateGroupCodeByGroupUin(delegate.toUin)
override fun toString(): String = "" override fun toString(): String = ""
} }
...@@ -29,12 +32,14 @@ internal inline class MessageSourceFromServer( ...@@ -29,12 +32,14 @@ internal inline class MessageSourceFromServer(
internal inline class MessageSourceFromMsg( internal inline class MessageSourceFromMsg(
val delegate: MsgComm.Msg val delegate: MsgComm.Msg
) : MessageSource { ) : MessageSource {
override val messageUid: Long override val messageUid: Long get() = delegate.msgBody.richText.attr!!.random.toLong()
get() = delegate.msgBody.richText.attr!!.random.toLong() override val sourceMessage: MessageChain get() = delegate.toMessageChain()
override val senderId: Long get() = delegate.msgHead.fromUin
override val groupId: Long get() = delegate.msgHead.groupInfo!!.groupCode
fun toJceData(): ImMsgBody.SourceMsg { fun toJceData(): ImMsgBody.SourceMsg {
val groupUin = Group.calculateGroupIdByGroupCode(delegate.msgHead.groupInfo!!.groupCode) val groupUin = Group.calculateGroupUinByGroupCode(delegate.msgHead.groupInfo!!.groupCode)
return ImMsgBody.SourceMsg( return ImMsgBody.SourceMsg(
origSeqs = listOf(delegate.msgHead.msgSeq), origSeqs = listOf(delegate.msgHead.msgSeq),
......
...@@ -21,10 +21,10 @@ import kotlinx.io.core.use ...@@ -21,10 +21,10 @@ import kotlinx.io.core.use
import net.mamoe.mirai.contact.ContactList import net.mamoe.mirai.contact.ContactList
import net.mamoe.mirai.contact.Member import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.contact.MemberPermission import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.event.events.ForceOfflineEvent
import net.mamoe.mirai.data.MultiPacket import net.mamoe.mirai.data.MultiPacket
import net.mamoe.mirai.data.Packet import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.event.* import net.mamoe.mirai.event.*
import net.mamoe.mirai.event.events.BotOfflineEvent
import net.mamoe.mirai.network.BotNetworkHandler import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.qqandroid.GroupImpl import net.mamoe.mirai.qqandroid.GroupImpl
import net.mamoe.mirai.qqandroid.MemberImpl import net.mamoe.mirai.qqandroid.MemberImpl
...@@ -119,7 +119,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler ...@@ -119,7 +119,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
@UseExperimental(MiraiExperimentalAPI::class, ExperimentalTime::class) @UseExperimental(MiraiExperimentalAPI::class, ExperimentalTime::class)
override suspend fun init(): Unit = coroutineScope { override suspend fun init(): Unit = coroutineScope {
this@QQAndroidBotNetworkHandler.subscribeAlways<ForceOfflineEvent> { this@QQAndroidBotNetworkHandler.subscribeAlways<BotOfflineEvent> {
if (this@QQAndroidBotNetworkHandler.bot == this.bot) { if (this@QQAndroidBotNetworkHandler.bot == this.bot) {
this.bot.logger.error("被挤下线") this.bot.logger.error("被挤下线")
close() close()
...@@ -344,20 +344,20 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler ...@@ -344,20 +344,20 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
} }
// check top-level cancelling // check top-level cancelling
if (PacketReceivedEvent(packet).broadcast().cancelled) { if (PacketReceivedEvent(packet).broadcast().isCancelled) {
return return
} }
// broadcast // broadcast
if (packet is Subscribable) { if (packet is Event) {
if (packet is BroadcastControllable) { if (packet is BroadcastControllable) {
if (packet.shouldBroadcast) packet.broadcast() if (packet.shouldBroadcast) packet.broadcast()
} else { } else {
packet.broadcast() packet.broadcast()
} }
if (packet is Cancellable && packet.cancelled) return if (packet is CancellableEvent && packet.isCancelled) return
} }
bot.logger.info("Received packet: ${packet.toString().replace("\n", """\n""").replace("\r", "")}") bot.logger.info("Received packet: ${packet.toString().replace("\n", """\n""").replace("\r", "")}")
......
...@@ -16,7 +16,6 @@ import io.ktor.http.HttpStatusCode ...@@ -16,7 +16,6 @@ import io.ktor.http.HttpStatusCode
import io.ktor.http.URLProtocol import io.ktor.http.URLProtocol
import io.ktor.http.content.OutgoingContent import io.ktor.http.content.OutgoingContent
import io.ktor.http.userAgent import io.ktor.http.userAgent
import kotlinx.coroutines.io.ByteWriteChannel
import kotlinx.io.core.Input import kotlinx.io.core.Input
import kotlinx.io.core.readAvailable import kotlinx.io.core.readAvailable
import kotlinx.io.core.use import kotlinx.io.core.use
...@@ -64,7 +63,7 @@ internal suspend inline fun HttpClient.postImage( ...@@ -64,7 +63,7 @@ internal suspend inline fun HttpClient.postImage(
override val contentType: ContentType = ContentType.Image.Any override val contentType: ContentType = ContentType.Image.Any
override val contentLength: Long = inputSize override val contentLength: Long = inputSize
override suspend fun writeTo(channel: ByteWriteChannel) { override suspend fun writeTo(channel: io.ktor.utils.io.ByteWriteChannel) {
ByteArrayPool.useInstance { buffer: ByteArray -> ByteArrayPool.useInstance { buffer: ByteArray ->
var size: Int var size: Int
while (imageInput.readAvailable(buffer).also { size = it } != 0) { while (imageInput.readAvailable(buffer).also { size = it } != 0) {
......
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.qqandroid.network.http
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.request.headers
import io.ktor.client.request.post
import io.ktor.client.response.HttpResponse
import io.ktor.http.URLProtocol
import io.ktor.http.setCookie
import io.ktor.http.userAgent
import kotlinx.coroutines.io.readRemaining
import kotlinx.io.core.readBytes
import net.mamoe.mirai.qqandroid.network.QQAndroidClient
import net.mamoe.mirai.utils.currentTimeMillis
import net.mamoe.mirai.utils.io.toUHexString
/**
* 好像不需要了
*/
object HttpRequest {
private lateinit var cookie: String
}
internal suspend fun HttpClient.getPTLoginCookies(
client: QQAndroidClient
): String {
//$"https://ssl.ptlogin2.qq.com/jump?pt_clientver=5593&pt_src=1&keyindex=9&ptlang=2052&clientuin={QQ}&clientkey={Util.ToHex(TXProtocol.BufServiceTicketHttp, "", "{0}")}&u1=https:%2F%2Fuser.qzone.qq.com%2F417085811%3FADUIN=417085811%26ADSESSION={Util.GetTimeMillis(DateTime.Now)}%26ADTAG=CLIENT.QQ.5593_MyTip.0%26ADPUBNO=26841&source=namecardhoverstar"
// "https://ssl.ptlogin2.qq.com/jump?pt_clientver=5509&pt_src=1&keyindex=9&clientuin={0}&clientkey={1}&u1=http%3A%2F%2Fqun.qq.com%2Fmember.html%23gid%3D168209441",
val res = post<HttpResponse> {
println(client.wLoginSigInfo.userStWebSig.data.toUHexString().replace(" ", "").toLowerCase())
url {
protocol = URLProtocol.HTTPS
host = "ssl.ptlogin2.qq.com"
path(
"/jump?pt_clientver=5509&pt_src=1&keyindex=9&clientuin=${client.uin}&clientkey=${client.wLoginSigInfo.userStWebSig.data.toUHexString().replace(
" ",
""
)}&u1=http%3A%2F%2Fqun.qq.com%2Fmember.html%23gid%3D168209441&FADUIN=417085811&ADSESSION=${currentTimeMillis}&source=namecardhoverstar"
)
}
headers {
userAgent("Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36")
}
}
println(res.status)
println(res.setCookie())
println(res.content.readRemaining().readBytes().toUHexString())
return "done";
}
internal suspend fun HttpClient.getGroupList(
client: QQAndroidClient
): String {
// "https://ssl.ptlogin2.qq.com/jump?pt_clientver=5509&pt_src=1&keyindex=9&clientuin={0}&clientkey={1}&u1=http%3A%2F%2Fqun.qq.com%2Fmember.html%23gid%3D168209441",
val res = get<HttpResponse> {
url {
protocol = URLProtocol.HTTPS
host = "ssl.ptlogin2.qq.com"
path("jump")
parameters["pt_clientver"] = "5509"
parameters["pt_src"] = "1"
parameters["keyindex"] = "9"
parameters["u1"] = "http%3A%2F%2Fqun.qq.com%2Fmember.html%23gid%3D168209441"
parameters["clientuin"] = client.uin.toString()
parameters["clientkey"] = client.wLoginSigInfo.userStWebSig.data.toUHexString()
}
headers {
userAgent("Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36")
}
}
println(res.status)
println(res.setCookie())
return "done";
}
...@@ -12,7 +12,7 @@ package net.mamoe.mirai.qqandroid.network.protocol.packet ...@@ -12,7 +12,7 @@ package net.mamoe.mirai.qqandroid.network.protocol.packet
import kotlinx.io.core.* import kotlinx.io.core.*
import kotlinx.io.pool.useInstance import kotlinx.io.pool.useInstance
import net.mamoe.mirai.data.Packet import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.event.Subscribable import net.mamoe.mirai.event.Event
import net.mamoe.mirai.qqandroid.QQAndroidBot import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.ImgStore import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.ImgStore
...@@ -20,7 +20,6 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.LongConn ...@@ -20,7 +20,6 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.LongConn
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.MessageSvc import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.MessageSvc
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.OnlinePush import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.OnlinePush
import net.mamoe.mirai.qqandroid.network.protocol.packet.list.FriendList import net.mamoe.mirai.qqandroid.network.protocol.packet.list.FriendList
import net.mamoe.mirai.qqandroid.network.protocol.packet.login.*
import net.mamoe.mirai.qqandroid.network.protocol.packet.login.ConfigPushSvc import net.mamoe.mirai.qqandroid.network.protocol.packet.login.ConfigPushSvc
import net.mamoe.mirai.qqandroid.network.protocol.packet.login.Heartbeat import net.mamoe.mirai.qqandroid.network.protocol.packet.login.Heartbeat
import net.mamoe.mirai.qqandroid.network.protocol.packet.login.StatSvc import net.mamoe.mirai.qqandroid.network.protocol.packet.login.StatSvc
...@@ -59,7 +58,7 @@ internal abstract class OutgoingPacketFactory<TPacket : Packet>( ...@@ -59,7 +58,7 @@ internal abstract class OutgoingPacketFactory<TPacket : Packet>(
final override val receivingCommandName: String get() = commandName final override val receivingCommandName: String get() = commandName
/** /**
* **解码**服务器的回复数据包. 返回的包若是 [Subscribable], 则会 broadcast. * **解码**服务器的回复数据包. 返回的包若是 [Event], 则会 broadcast.
*/ */
abstract suspend fun ByteReadPacket.decode(bot: QQAndroidBot): TPacket abstract suspend fun ByteReadPacket.decode(bot: QQAndroidBot): TPacket
...@@ -85,7 +84,7 @@ internal abstract class IncomingPacketFactory<TPacket : Packet>( ...@@ -85,7 +84,7 @@ internal abstract class IncomingPacketFactory<TPacket : Packet>(
val responseCommandName: String = "" val responseCommandName: String = ""
) : PacketFactory<TPacket>() { ) : PacketFactory<TPacket>() {
/** /**
* **解码**服务器的回复数据包. 返回的包若是 [Subscribable], 则会 broadcast. * **解码**服务器的回复数据包. 返回的包若是 [Event], 则会 broadcast.
*/ */
abstract suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): TPacket abstract suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): TPacket
......
...@@ -27,7 +27,6 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY ...@@ -27,7 +27,6 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket
import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacketFactory import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacketFactory
import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement.GetGroupOperationInfo.decode
import net.mamoe.mirai.utils.daysToSeconds import net.mamoe.mirai.utils.daysToSeconds
import net.mamoe.mirai.utils.io.encodeToString import net.mamoe.mirai.utils.io.encodeToString
...@@ -66,9 +65,7 @@ internal class TroopManagement { ...@@ -66,9 +65,7 @@ internal class TroopManagement {
} }
object Response : Packet { object Response : Packet {
override fun toString(): String { override fun toString(): String = "Response(Mute)"
return "Response(Mute)"
}
} }
} }
...@@ -80,9 +77,7 @@ internal class TroopManagement { ...@@ -80,9 +77,7 @@ internal class TroopManagement {
val autoApprove: Boolean, val autoApprove: Boolean,
val confessTalk: Boolean val confessTalk: Boolean
) : Packet { ) : Packet {
override fun toString(): String { override fun toString(): String = "Response(GroupInfo)"
return "Response(GroupInfo)"
}
} }
operator fun invoke( operator fun invoke(
...@@ -146,9 +141,7 @@ internal class TroopManagement { ...@@ -146,9 +141,7 @@ internal class TroopManagement {
class Response( class Response(
val success: Boolean val success: Boolean
) : Packet { ) : Packet {
override fun toString(): String { override fun toString(): String = "Response(Kick Member)"
return "Response(Kick Member)"
}
} }
operator fun invoke( operator fun invoke(
...@@ -424,13 +417,4 @@ internal class TroopManagement { ...@@ -424,13 +417,4 @@ internal class TroopManagement {
} }
} }
/*
internal object Recall: OutgoingPacketFactory<WtLogin.Login.LoginPacketResponse>("wtlogin.login"){
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): WtLogin.Login.LoginPacketResponse {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
*/
} }
\ No newline at end of file
...@@ -11,10 +11,10 @@ package net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive ...@@ -11,10 +11,10 @@ package net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive
import kotlinx.io.core.ByteReadPacket import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact import kotlinx.io.core.discardExact
import net.mamoe.mirai.event.events.ForceOfflineEvent
import net.mamoe.mirai.data.MultiPacket import net.mamoe.mirai.data.MultiPacket
import net.mamoe.mirai.data.Packet import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.event.BroadcastControllable import net.mamoe.mirai.event.BroadcastControllable
import net.mamoe.mirai.event.events.BotOfflineEvent
import net.mamoe.mirai.message.FriendMessage import net.mamoe.mirai.message.FriendMessage
import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.qqandroid.QQAndroidBot import net.mamoe.mirai.qqandroid.QQAndroidBot
...@@ -178,10 +178,10 @@ internal class MessageSvc { ...@@ -178,10 +178,10 @@ internal class MessageSvc {
/** /**
* 被挤下线 * 被挤下线
*/ */
internal object PushForceOffline : OutgoingPacketFactory<ForceOfflineEvent>("MessageSvc.PushForceOffline") { internal object PushForceOffline : OutgoingPacketFactory<BotOfflineEvent.Force>("MessageSvc.PushForceOffline") {
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): ForceOfflineEvent { override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): BotOfflineEvent.Force {
val struct = this.decodeUniPacket(RequestPushForceOffline.serializer()) val struct = this.decodeUniPacket(RequestPushForceOffline.serializer())
return ForceOfflineEvent(bot, title = struct.title ?: "", tips = struct.tips ?: "") return BotOfflineEvent.Force(bot, title = struct.title ?: "", message = struct.tips ?: "")
} }
} }
......
...@@ -248,6 +248,7 @@ fun ByteReadPacket.analysisOneFullPacket(): ByteReadPacket = debugIfFail("Failed ...@@ -248,6 +248,7 @@ fun ByteReadPacket.analysisOneFullPacket(): ByteReadPacket = debugIfFail("Failed
val encryptedBody = readBytes((remaining - 1).toInt()) val encryptedBody = readBytes((remaining - 1).toInt())
@Suppress("NAME_SHADOWING")
val decrypted = kotlin.runCatching { val decrypted = kotlin.runCatching {
encryptedBody.decryptBy(encrypt).also { println("first by calculatedShareKey or sessionKey(method=7)") } encryptedBody.decryptBy(encrypt).also { println("first by calculatedShareKey or sessionKey(method=7)") }
}.getOrElse { }.getOrElse {
......
...@@ -75,7 +75,7 @@ private fun processFullPacketWithoutLength(packet: ByteReadPacket) { ...@@ -75,7 +75,7 @@ private fun processFullPacketWithoutLength(packet: ByteReadPacket) {
val flag3 = readByte().toInt() val flag3 = readByte().toInt()
val uinAccount = readString(readInt() - 4)//uin readString(readInt() - 4)//uin
//debugPrint("remaining") //debugPrint("remaining")
...@@ -163,17 +163,17 @@ private fun Map<Int, ByteArray>.getOrEmpty(key: Int): ByteArray { ...@@ -163,17 +163,17 @@ private fun Map<Int, ByteArray>.getOrEmpty(key: Int): ByteArray {
var randomKey: ByteArray = byteArrayOf() var randomKey: ByteArray = byteArrayOf()
private fun ByteReadPacket.parseOicqResponse(body: ByteReadPacket.() -> Unit) { private fun ByteReadPacket.parseOicqResponse(body: ByteReadPacket.() -> Unit) {
val qq: Long
readIoBuffer(readInt() - 4).withUse { readIoBuffer(readInt() - 4).withUse {
check(readByte().toInt() == 2) check(readByte().toInt() == 2)
this.discardExact(2) // 27 + 2 + body.size this.discardExact(2) // 27 + 2 + body.size
this.discardExact(2) // const, =8001 this.discardExact(2) // const, =8001
this.readUShort() // commandId this.readUShort() // commandId
this.readShort() // const, =0x0001 this.readShort() // const, =0x0001
qq = this.readUInt().toLong() this.readUInt().toLong() // qq
val encryptionMethod = this.readUShort().toInt() val encryptionMethod = this.readUShort().toInt()
this.discardExact(1) // const = 0 this.discardExact(1) // const = 0
@Suppress("UNUSED_VARIABLE")
val packet = when (encryptionMethod) { val packet = when (encryptionMethod) {
4 -> { // peer public key, ECDH 4 -> { // peer public key, ECDH
var data = this.decryptBy(shareKeyCalculatedByConstPubKey, 0, this.readRemaining - 1) var data = this.decryptBy(shareKeyCalculatedByConstPubKey, 0, this.readRemaining - 1)
...@@ -229,7 +229,7 @@ private fun parseSsoFrame(flag3: Int, input: ByteReadPacket): KnownPacketFactori ...@@ -229,7 +229,7 @@ private fun parseSsoFrame(flag3: Int, input: ByteReadPacket): KnownPacketFactori
commandName = readString(readInt() - 4) commandName = readString(readInt() - 4)
DebugLogger.warning("commandName=$commandName") DebugLogger.warning("commandName=$commandName")
val unknown = readBytes(readInt() - 4) readBytes(readInt() - 4) // unknown, sessionId?
//if (unknown.toInt() != 0x02B05B8B) DebugLogger.debug("got new unknown: ${unknown.toUHexString()}") //if (unknown.toInt() != 0x02B05B8B) DebugLogger.debug("got new unknown: ${unknown.toUHexString()}")
check(readInt() == 0) check(readInt() == 0)
...@@ -249,7 +249,7 @@ private fun parseSsoFrame(flag3: Int, input: ByteReadPacket): KnownPacketFactori ...@@ -249,7 +249,7 @@ private fun parseSsoFrame(flag3: Int, input: ByteReadPacket): KnownPacketFactori
} else { } else {
} }
return KnownPacketFactories.IncomingPacket(packetFactory, ssoSequenceId, data.toReadPacket()) return KnownPacketFactories.IncomingPacket(packetFactory, ssoSequenceId, data.toReadPacket(), commandName)
} }
...@@ -278,7 +278,7 @@ private fun parseUniFrame(input: ByteReadPacket): KnownPacketFactories.IncomingP ...@@ -278,7 +278,7 @@ private fun parseUniFrame(input: ByteReadPacket): KnownPacketFactories.IncomingP
commandName = readString(readInt() - 4) commandName = readString(readInt() - 4)
DebugLogger.warning("commandName=$commandName") DebugLogger.warning("commandName=$commandName")
val unknown = readBytes(readInt() - 4) readBytes(readInt() - 4) // unknown
//if (unknown.toInt() != 0x02B05B8B) DebugLogger.debug("got new unknown: ${unknown.toUHexString()}") //if (unknown.toInt() != 0x02B05B8B) DebugLogger.debug("got new unknown: ${unknown.toUHexString()}")
check(readInt() == 0) check(readInt() == 0)
...@@ -291,7 +291,7 @@ private fun parseUniFrame(input: ByteReadPacket): KnownPacketFactories.IncomingP ...@@ -291,7 +291,7 @@ private fun parseUniFrame(input: ByteReadPacket): KnownPacketFactories.IncomingP
println("找不到包 PacketFactory") println("找不到包 PacketFactory")
PacketLogger.verbose("传递给 PacketFactory 的数据 = ${input.readBytes().toUHexString()}") PacketLogger.verbose("传递给 PacketFactory 的数据 = ${input.readBytes().toUHexString()}")
} }
return KnownPacketFactories.IncomingPacket(packetFactory, ssoSequenceId, input) return KnownPacketFactories.IncomingPacket(packetFactory, ssoSequenceId, input, commandName)
} }
private inline fun <R> inline(block: () -> R): R = block() private inline fun <R> inline(block: () -> R): R = block()
...@@ -9,6 +9,8 @@ ...@@ -9,6 +9,8 @@
package net.mamoe.mirai.qqandroid.io.serialization package net.mamoe.mirai.qqandroid.io.serialization
/*
import kotlinx.io.core.readBytes import kotlinx.io.core.readBytes
import kotlinx.serialization.SerialId import kotlinx.serialization.SerialId
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
...@@ -310,4 +312,6 @@ class JceDecoderTest { ...@@ -310,4 +312,6 @@ class JceDecoderTest {
OuterStruct(listOf(TestSimpleJceStruct(), TestSimpleJceStruct())).contentToString() OuterStruct(listOf(TestSimpleJceStruct(), TestSimpleJceStruct())).contentToString()
) )
} }
} }
\ No newline at end of file
*/
\ No newline at end of file
...@@ -61,13 +61,13 @@ kotlin { ...@@ -61,13 +61,13 @@ kotlin {
languageSettings.useExperimentalAnnotation("kotlin.Experimental") languageSettings.useExperimentalAnnotation("kotlin.Experimental")
dependencies { dependencies {
api(kotlin("stdlib", kotlinVersion)) implementation(kotlin("stdlib", kotlinVersion))
api(kotlin("serialization", kotlinVersion)) implementation(kotlin("serialization", kotlinVersion))
api("org.jetbrains.kotlinx:atomicfu:$atomicFuVersion") implementation("org.jetbrains.kotlinx:atomicfu:$atomicFuVersion")
api(kotlinx("io", kotlinXIoVersion)) implementation(kotlinx("io", kotlinXIoVersion))
api(kotlinx("coroutines-io", coroutinesIoVersion)) implementation(kotlinx("coroutines-io", coroutinesIoVersion))
api(kotlinx("coroutines-core", coroutinesVersion)) implementation(kotlinx("coroutines-core", coroutinesVersion))
} }
} }
commonMain { commonMain {
...@@ -90,8 +90,8 @@ kotlin { ...@@ -90,8 +90,8 @@ kotlin {
} }
commonTest { commonTest {
dependencies { dependencies {
api(kotlin("test-annotations-common")) implementation(kotlin("test-annotations-common"))
api(kotlin("test-common")) implementation(kotlin("test-common"))
//runtimeOnly(files("build/classes/kotlin/metadata/test")) // classpath is not properly set by IDE //runtimeOnly(files("build/classes/kotlin/metadata/test")) // classpath is not properly set by IDE
} }
...@@ -102,6 +102,8 @@ kotlin { ...@@ -102,6 +102,8 @@ kotlin {
dependencies { dependencies {
api(kotlin("reflect", kotlinVersion)) api(kotlin("reflect", kotlinVersion))
api(kotlinx("io", kotlinXIoVersion))
api(kotlinx("io-jvm", kotlinXIoVersion))
api(kotlinx("serialization-runtime", serializationVersion)) api(kotlinx("serialization-runtime", serializationVersion))
api(kotlinx("coroutines-android", coroutinesVersion)) api(kotlinx("coroutines-android", coroutinesVersion))
...@@ -111,10 +113,10 @@ kotlin { ...@@ -111,10 +113,10 @@ kotlin {
val androidTest by getting { val androidTest by getting {
dependencies { dependencies {
api(kotlin("test", kotlinVersion)) implementation(kotlin("test", kotlinVersion))
api(kotlin("test-junit", kotlinVersion)) implementation(kotlin("test-junit", kotlinVersion))
api(kotlin("test-annotations-common")) implementation(kotlin("test-annotations-common"))
api(kotlin("test-common")) implementation(kotlin("test-common"))
} }
} }
} }
...@@ -128,6 +130,9 @@ kotlin { ...@@ -128,6 +130,9 @@ kotlin {
api(ktor("client-core-jvm", ktorVersion)) api(ktor("client-core-jvm", ktorVersion))
api(kotlinx("io-jvm", kotlinXIoVersion)) api(kotlinx("io-jvm", kotlinXIoVersion))
api(kotlinx("serialization-runtime", serializationVersion)) api(kotlinx("serialization-runtime", serializationVersion))
api(kotlinx("coroutines-io", coroutinesIoVersion))
api(kotlinx("coroutines-io-jvm", coroutinesIoVersion))
api(kotlinx("io-jvm", coroutinesIoVersion))
api("org.bouncycastle:bcprov-jdk15on:1.64") api("org.bouncycastle:bcprov-jdk15on:1.64")
runtimeOnly(files("build/classes/kotlin/jvm/main")) // classpath is not properly set by IDE runtimeOnly(files("build/classes/kotlin/jvm/main")) // classpath is not properly set by IDE
...@@ -136,8 +141,8 @@ kotlin { ...@@ -136,8 +141,8 @@ kotlin {
val jvmTest by getting { val jvmTest by getting {
dependencies { dependencies {
api(kotlin("test", kotlinVersion)) implementation(kotlin("test", kotlinVersion))
api(kotlin("test-junit", kotlinVersion)) implementation(kotlin("test-junit", kotlinVersion))
implementation("org.pcap4j:pcap4j-distribution:1.8.2") implementation("org.pcap4j:pcap4j-distribution:1.8.2")
runtimeOnly(files("build/classes/kotlin/jvm/test")) // classpath is not properly set by IDE runtimeOnly(files("build/classes/kotlin/jvm/test")) // classpath is not properly set by IDE
......
...@@ -12,7 +12,6 @@ ...@@ -12,7 +12,6 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import io.ktor.util.asStream
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.io.core.Input import kotlinx.io.core.Input
...@@ -100,7 +99,7 @@ suspend fun InputStream.suspendToExternalImage(): ExternalImage = withContext(IO ...@@ -100,7 +99,7 @@ suspend fun InputStream.suspendToExternalImage(): ExternalImage = withContext(IO
fun Input.toExternalImage(): ExternalImage { fun Input.toExternalImage(): ExternalImage {
val file = createTempFile().apply { deleteOnExit() } val file = createTempFile().apply { deleteOnExit() }
file.outputStream().asOutput().use { file.outputStream().asOutput().use {
this.asStream().asInput().copyTo(it) this.copyTo(it)
} }
return file.toExternalImage() return file.toExternalImage()
} }
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * https://github.com/mamoe/mirai/blob/master/LICENSE
*/ */
@file:Suppress("EXPERIMENTAL_API_USAGE", "unused", "FunctionName") @file:Suppress("EXPERIMENTAL_API_USAGE", "unused", "FunctionName", "NOTHING_TO_INLINE")
package net.mamoe.mirai package net.mamoe.mirai
...@@ -17,15 +17,15 @@ import kotlinx.io.OutputStream ...@@ -17,15 +17,15 @@ import kotlinx.io.OutputStream
import kotlinx.io.core.ByteReadPacket import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.IoBuffer import kotlinx.io.core.IoBuffer
import kotlinx.io.core.use import kotlinx.io.core.use
import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.contact.*
import net.mamoe.mirai.contact.ContactList
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.data.AddFriendResult import net.mamoe.mirai.data.AddFriendResult
import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.network.BotNetworkHandler import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.WeakRef
import net.mamoe.mirai.utils.io.transferTo import net.mamoe.mirai.utils.io.transferTo
import net.mamoe.mirai.utils.toList
/** /**
* 机器人对象. 一个机器人实例登录一个 QQ 账号. * 机器人对象. 一个机器人实例登录一个 QQ 账号.
...@@ -35,8 +35,8 @@ import net.mamoe.mirai.utils.io.transferTo ...@@ -35,8 +35,8 @@ import net.mamoe.mirai.utils.io.transferTo
* *
* @see Contact * @see Contact
*/ */
@UseExperimental(MiraiInternalAPI::class)
abstract class Bot : CoroutineScope { abstract class Bot : CoroutineScope {
@UseExperimental(MiraiInternalAPI::class)
companion object { companion object {
/** /**
* 复制一份此时的 [Bot] 实例列表. * 复制一份此时的 [Bot] 实例列表.
...@@ -72,6 +72,8 @@ abstract class Bot : CoroutineScope { ...@@ -72,6 +72,8 @@ abstract class Bot : CoroutineScope {
// region contacts // region contacts
abstract val selfQQ: QQ
/** /**
* 机器人的好友列表. 它将与服务器同步更新 * 机器人的好友列表. 它将与服务器同步更新
*/ */
...@@ -102,7 +104,11 @@ abstract class Bot : CoroutineScope { ...@@ -102,7 +104,11 @@ abstract class Bot : CoroutineScope {
/** /**
* 获取一个好友对象. 若没有这个好友, 则会抛出异常 [NoSuchElementException] * 获取一个好友对象. 若没有这个好友, 则会抛出异常 [NoSuchElementException]
*/ */
abstract fun getFriend(id: Long): QQ fun getFriend(id: Long): QQ {
if (id == uin) return selfQQ
return qqs.delegate.getOrNull(id)
?: throw NoSuchElementException("No such friend $id for bot ${this.uin}")
}
/** /**
* 构造一个 [QQ] 对象. 它持有对 [Bot] 的弱引用([WeakRef]). * 构造一个 [QQ] 对象. 它持有对 [Bot] 的弱引用([WeakRef]).
...@@ -120,7 +126,10 @@ abstract class Bot : CoroutineScope { ...@@ -120,7 +126,10 @@ abstract class Bot : CoroutineScope {
/** /**
* 获取一个机器人加入的群. 若没有这个群, 则会抛出异常 [NoSuchElementException] * 获取一个机器人加入的群. 若没有这个群, 则会抛出异常 [NoSuchElementException]
*/ */
abstract fun getGroup(id: Long): Group fun getGroup(id: Long): Group {
return groups.delegate.getOrNull(id)
?: throw NoSuchElementException("No such group $id for bot ${this.uin}")
}
// TODO 目前还不能构造群对象. 这将在以后支持 // TODO 目前还不能构造群对象. 这将在以后支持
...@@ -186,6 +195,7 @@ abstract class Bot : CoroutineScope { ...@@ -186,6 +195,7 @@ abstract class Bot : CoroutineScope {
@Deprecated(message = "这个函数有歧义, 将在不久后删除", replaceWith = ReplaceWith("getFriend(this.toLong())")) @Deprecated(message = "这个函数有歧义, 将在不久后删除", replaceWith = ReplaceWith("getFriend(this.toLong())"))
fun Int.qq(): QQ = getFriend(this.toLong()) fun Int.qq(): QQ = getFriend(this.toLong())
@Deprecated(message = "这个函数有歧义, 将在不久后删除", replaceWith = ReplaceWith("getFriend(this)")) @Deprecated(message = "这个函数有歧义, 将在不久后删除", replaceWith = ReplaceWith("getFriend(this)"))
fun Long.qq(): QQ = getFriend(this) fun Long.qq(): QQ = getFriend(this)
...@@ -200,4 +210,12 @@ abstract class Bot : CoroutineScope { ...@@ -200,4 +210,12 @@ abstract class Bot : CoroutineScope {
download().use { input -> input.transferTo(output) } download().use { input -> input.transferTo(output) }
// endregion // endregion
} }
\ No newline at end of file
inline fun Bot.containsFriend(id: Long): Boolean = this.qqs.contains(id)
inline fun Bot.containsGroup(id: Long): Boolean = this.groups.contains(id)
inline fun Bot.getFriendOrNull(id: Long): QQ? = this.qqs.getOrNull(id)
inline fun Bot.getGroupOrNull(id: Long): Group? = this.groups.getOrNull(id)
\ No newline at end of file
...@@ -107,7 +107,7 @@ abstract class BotImpl<N : BotNetworkHandler> constructor( ...@@ -107,7 +107,7 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
logger.info("Initializing BotNetworkHandler") logger.info("Initializing BotNetworkHandler")
try { try {
if (::_network.isInitialized) { if (::_network.isInitialized) {
BotOfflineEvent(this).broadcast() BotOfflineEvent.Active(this, cause).broadcast()
_network.closeAndJoin(cause) _network.closeAndJoin(cause)
} }
} catch (e: Exception) { } catch (e: Exception) {
......
...@@ -13,6 +13,10 @@ package net.mamoe.mirai.contact ...@@ -13,6 +13,10 @@ package net.mamoe.mirai.contact
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.event.events.BeforeImageUploadEvent
import net.mamoe.mirai.event.events.ImageUploadEvent
import net.mamoe.mirai.event.events.MessageSendEvent.FriendMessageSendEvent
import net.mamoe.mirai.event.events.MessageSendEvent.GroupMessageSendEvent
import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.ExternalImage import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.WeakRefProperty import net.mamoe.mirai.utils.WeakRefProperty
...@@ -40,6 +44,9 @@ interface Contact : CoroutineScope { ...@@ -40,6 +44,9 @@ interface Contact : CoroutineScope {
/** /**
* 向这个对象发送消息. * 向这个对象发送消息.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*/ */
suspend fun sendMessage(message: MessageChain) suspend fun sendMessage(message: MessageChain)
...@@ -47,6 +54,9 @@ interface Contact : CoroutineScope { ...@@ -47,6 +54,9 @@ interface Contact : CoroutineScope {
* 上传一个图片以备发送. * 上传一个图片以备发送.
* TODO: 群图片与好友图片之间是否通用还不确定. * TODO: 群图片与好友图片之间是否通用还不确定.
* TODO: 好友之间图片是否通用还不确定. * TODO: 好友之间图片是否通用还不确定.
*
* @see BeforeImageUploadEvent 图片发送前事件, cancellable
* @see ImageUploadEvent 图片发送完成事件
*/ */
suspend fun uploadImage(image: ExternalImage): Image suspend fun uploadImage(image: ExternalImage): Image
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
package net.mamoe.mirai.contact package net.mamoe.mirai.contact
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.utils.MiraiExperimentalAPI import net.mamoe.mirai.utils.MiraiExperimentalAPI
/** /**
...@@ -22,32 +23,41 @@ interface Group : Contact, CoroutineScope { ...@@ -22,32 +23,41 @@ interface Group : Contact, CoroutineScope {
* 群名称. * 群名称.
* *
* 在修改时将会异步上传至服务器. 无权限修改时将会抛出异常 [PermissionDeniedException] * 在修改时将会异步上传至服务器. 无权限修改时将会抛出异常 [PermissionDeniedException]
* 频繁修改可能会被服务器拒绝.
* *
* 注: 频繁修改可能会被服务器拒绝. * @see MemberPermissionChangeEvent
*/ */
var name: String var name: String
/** /**
* 入群公告, 没有时为空字符串. * 入群公告, 没有时为空字符串.
* *
* 在修改时将会异步上传至服务器. 无权限修改时将会抛出异常 [PermissionDeniedException] * 在修改时将会异步上传至服务器. 无权限修改时将会抛出异常 [PermissionDeniedException]
*
* @see GroupEntranceAnnouncementChangeEvent
*/ */
var announcement: String var entranceAnnouncement: String
/** /**
* 全体禁言状态. `true` 为开启. * 全体禁言状态. `true` 为开启.
* *
* 当前仅能修改状态. * 当前仅能修改状态.
*
* @see GroupMuteAllEvent
*/// TODO: 2020/2/5 实现 muteAll 的查询 */// TODO: 2020/2/5 实现 muteAll 的查询
var muteAll: Boolean var muteAll: Boolean
/** /**
* 坦白说状态. `true` 为允许. * 坦白说状态. `true` 为允许.
* *
* 在修改时将会异步上传至服务器. 无权限修改时将会抛出异常 [PermissionDeniedException] * 在修改时将会异步上传至服务器. 无权限修改时将会抛出异常 [PermissionDeniedException]
* @see GroupAllowConfessTalkEvent
*/ */
var confessTalk: Boolean var confessTalk: Boolean
/** /**
* 允许群员邀请好友入群的状态. `true` 为允许 * 允许群员邀请好友入群的状态. `true` 为允许
* *
* 在修改时将会异步上传至服务器. 无权限修改时将会抛出异常 [PermissionDeniedException] * 在修改时将会异步上传至服务器. 无权限修改时将会抛出异常 [PermissionDeniedException]
*
* @see GroupAllowMemberInviteEvent
*/ */
var allowMemberInvite: Boolean var allowMemberInvite: Boolean
/** /**
...@@ -74,6 +84,8 @@ interface Group : Contact, CoroutineScope { ...@@ -74,6 +84,8 @@ interface Group : Contact, CoroutineScope {
* 机器人在这个群里的权限 * 机器人在这个群里的权限
* *
* **MiraiExperimentalAPI**: 在未来可能会被修改 * **MiraiExperimentalAPI**: 在未来可能会被修改
*
* @see BotGroupPermissionChangeEvent
*/ */
@MiraiExperimentalAPI @MiraiExperimentalAPI
val botPermission: MemberPermission val botPermission: MemberPermission
...@@ -111,21 +123,37 @@ interface Group : Contact, CoroutineScope { ...@@ -111,21 +123,37 @@ interface Group : Contact, CoroutineScope {
/** /**
* by @kar98k * by @kar98k
*/ */
fun calculateGroupIdByGroupCode(groupCode: Long): Long { fun calculateGroupUinByGroupCode(groupCode: Long): Long {
var left: Long = groupCode / 1000000L var left: Long = groupCode / 1000000L
when { when (left) {
left <= 10 -> left += 202 in 0..10 -> left += 202
left <= 19 -> left += 480 - 11 in 11..19 -> left += 480 - 11
left <= 66 -> left += 2100 - 20 in 20..66 -> left += 2100 - 20
left <= 156 -> left += 2010 - 67 in 67..156 -> left += 2010 - 67
left <= 209 -> left += 2147 - 157 in 157..209 -> left += 2147 - 157
left <= 309 -> left += 4100 - 210 in 210..309 -> left += 4100 - 210
left <= 499 -> left += 3800 - 310 in 310..499 -> left += 3800 - 310
} }
return left * 1000000L + groupCode % 1000000L return left * 1000000L + groupCode % 1000000L
} }
fun calculateGroupCodeByGroupUin(groupUin: Long): Long {
var left: Long = groupUin / 1000000L
when (left) {
in 0 + 202..10 + 202 -> left -= 202
in 11 + 480 - 11..19 + 480 - 11 -> left -= 480 - 11
in 20 + 2100 - 20..66 + 2100 - 20 -> left -= 2100 - 20
in 67 + 2010 - 67..156 + 2010 - 67 -> left -= 2010 - 67
in 157 + 2147 - 157..209 + 2147 - 157 -> left -= 2147 - 157
in 210 + 4100 - 210..309 + 4100 - 210 -> left -= 4100 - 210
in 310 + 3800 - 310..499 + 3800 - 310 -> left -= 3800 - 310
}
return left * 1000000L + groupUin % 1000000L
}
} }
@MiraiExperimentalAPI @MiraiExperimentalAPI
......
...@@ -11,8 +11,9 @@ ...@@ -11,8 +11,9 @@
package net.mamoe.mirai.contact package net.mamoe.mirai.contact
import net.mamoe.mirai.Bot
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.utils.WeakRefProperty import net.mamoe.mirai.utils.WeakRefProperty
import kotlin.jvm.JvmName
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
...@@ -32,18 +33,22 @@ interface Member : QQ, Contact { ...@@ -32,18 +33,22 @@ interface Member : QQ, Contact {
val permission: MemberPermission val permission: MemberPermission
/** /**
* 群名片. 可能为空. * 群名片. 可能为空. 修改时将会触发事件
* *
* 在修改时将会异步上传至服务器. 无权限修改时将会抛出异常 [PermissionDeniedException] * 在修改时将会异步上传至服务器. 无权限修改时将会抛出异常 [PermissionDeniedException]
* *
* @see [groupCardOrNick] 获取非空群名片或昵称 * @see [groupCardOrNick] 获取非空群名片或昵称
*
* @see MemberCardChangeEvent 群名片被管理员, 自己或 [Bot] 改动事件
*/ */
var groupCard: String var nameCard: String
/** /**
* 群头衔 * 群头衔
* *
* 在修改时将会异步上传至服务器. 无权限修改时将会抛出异常 [PermissionDeniedException] * 在修改时将会异步上传至服务器. 无权限修改时将会抛出异常 [PermissionDeniedException]
*
* @see MemberSpecialTitleChangeEvent 群名片被管理员, 自己或 [Bot] 改动事件
*/ */
var specialTitle: String var specialTitle: String
...@@ -51,21 +56,27 @@ interface Member : QQ, Contact { ...@@ -51,21 +56,27 @@ interface Member : QQ, Contact {
* 禁言 * 禁言
* *
* @param durationSeconds 持续时间. 精确到秒. 范围区间表示为 `(0s, 30days]`. 超过范围则会抛出异常. * @param durationSeconds 持续时间. 精确到秒. 范围区间表示为 `(0s, 30days]`. 超过范围则会抛出异常.
* @return 当机器人无权限禁言这个群成员时返回 `false` * @return 机器人无权限时返回 `false`
* *
* @see Int.minutesToSeconds * @see Int.minutesToSeconds
* @see Int.hoursToSeconds * @see Int.hoursToSeconds
* @see Int.daysToSeconds * @see Int.daysToSeconds
*
* @see MemberMuteEvent 成员被禁言事件
*/ */
suspend fun mute(durationSeconds: Int): Boolean suspend fun mute(durationSeconds: Int): Boolean
/** /**
* 解除禁言. 在没有权限时会返回 `false`. * 解除禁言. 机器人无权限时返回 `false`.
*
* @see MemberUnmuteEvent 成员被取消禁言事件.
*/ */
suspend fun unmute(): Boolean suspend fun unmute(): Boolean
/** /**
* 踢出该成员. 机器人无权限时返回 `false` * 踢出该成员. 机器人无权限时返回 `false`.
*
* @see MemberLeaveEvent.Kick 成员被踢出事件.
*/ */
suspend fun kick(message: String = ""): Boolean suspend fun kick(message: String = ""): Boolean
...@@ -78,9 +89,9 @@ interface Member : QQ, Contact { ...@@ -78,9 +89,9 @@ interface Member : QQ, Contact {
/** /**
* 获取非空群名片或昵称. * 获取非空群名片或昵称.
* *
* 若 [群名片][Member.groupCard] 不为空则返回群名片, 为空则返回 [QQ.nick] * 若 [群名片][Member.nameCard] 不为空则返回群名片, 为空则返回 [QQ.nick]
*/ */
val Member.groupCardOrNick: String get() = this.groupCard.takeIf { it.isNotEmpty() } ?: this.nick val Member.groupCardOrNick: String get() = this.nameCard.takeIf { it.isNotEmpty() } ?: this.nick
@ExperimentalTime @ExperimentalTime
suspend inline fun Member.mute(duration: Duration): Boolean { suspend inline fun Member.mute(duration: Duration): Boolean {
......
...@@ -16,6 +16,7 @@ import net.mamoe.mirai.Bot ...@@ -16,6 +16,7 @@ import net.mamoe.mirai.Bot
import net.mamoe.mirai.data.FriendNameRemark import net.mamoe.mirai.data.FriendNameRemark
import net.mamoe.mirai.data.PreviousNameList import net.mamoe.mirai.data.PreviousNameList
import net.mamoe.mirai.data.Profile import net.mamoe.mirai.data.Profile
import net.mamoe.mirai.utils.MiraiExperimentalAPI
/** /**
* QQ 对象. * QQ 对象.
...@@ -49,6 +50,7 @@ interface QQ : Contact, CoroutineScope { ...@@ -49,6 +50,7 @@ interface QQ : Contact, CoroutineScope {
/** /**
* 查询用户资料 * 查询用户资料
*/ */
@MiraiExperimentalAPI("还未支持")
suspend fun queryProfile(): Profile suspend fun queryProfile(): Profile
/** /**
...@@ -58,10 +60,12 @@ interface QQ : Contact, CoroutineScope { ...@@ -58,10 +60,12 @@ interface QQ : Contact, CoroutineScope {
* - 昵称 * - 昵称
* - 共同群内的群名片 * - 共同群内的群名片
*/ */
@MiraiExperimentalAPI("还未支持")
suspend fun queryPreviousNameList(): PreviousNameList suspend fun queryPreviousNameList(): PreviousNameList
/** /**
* 查询机器人账号给这个人设置的备注 * 查询机器人账号给这个人设置的备注
*/ */
@MiraiExperimentalAPI("还未支持")
suspend fun queryRemark(): FriendNameRemark suspend fun queryRemark(): FriendNameRemark
} }
\ No newline at end of file
...@@ -9,11 +9,11 @@ ...@@ -9,11 +9,11 @@
package net.mamoe.mirai.data package net.mamoe.mirai.data
import net.mamoe.mirai.event.Subscribable import net.mamoe.mirai.event.Event
/** /**
* 事件包. 可被监听. * 事件包. 可被监听.
* *
* @see Subscribable * @see Event
*/ */
interface EventPacket : Subscribable, Packet interface EventPacket : Event, Packet
\ No newline at end of file \ No newline at end of file
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.data
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member
// 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
}
...@@ -11,6 +11,7 @@ package net.mamoe.mirai.data ...@@ -11,6 +11,7 @@ package net.mamoe.mirai.data
/** /**
* 从服务器收到的包解析之后的结构化数据. * 从服务器收到的包解析之后的结构化数据.
* 它是一个数据包工厂的处理的返回值.
*/ */
interface Packet { interface Packet {
/** /**
......
...@@ -17,61 +17,62 @@ import net.mamoe.mirai.event.internal.broadcastInternal ...@@ -17,61 +17,62 @@ import net.mamoe.mirai.event.internal.broadcastInternal
import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.MiraiInternalAPI
/** /**
* 可被监听的. * 可被监听的类, 可以是任何 class 或 object.
* *
* 可以是任何 class 或 object. * 若监听这个类, 监听器将会接收所有事件的广播.
* *
* @see subscribeAlways * @see subscribeAlways
* @see subscribeWhile * @see subscribeWhile
* *
* @see subscribeMessages * @see subscribeMessages
*/
interface Subscribable
/**
* 所有事件的基类.
* 若监听这个类, 监听器将会接收所有事件的广播.
* *
* @see [broadcast] 广播事件 * @see [broadcast] 广播事件
* @see [subscribe] 监听事件 * @see [subscribe] 监听事件
*/ */
abstract class Event : Subscribable { interface Event
/**
* 可被取消的事件
*/
interface CancellableEvent {
/** /**
* 事件是否已取消. 事件需实现 [Cancellable] 才可以被取消, 否则这个字段为常量值 false * 事件是否已取消.
*/ */
val cancelled: Boolean get() = _cancelled val isCancelled: Boolean
private var _cancelled: Boolean = false
get() = field.takeIf { this is Cancellable } ?: false
private set(value) =
if (this is Cancellable) field = value
else throw UnsupportedOperationException()
/** /**
* 取消事件. 事件需实现 [Cancellable] 才可以被取消 * 取消这个事件.
*
* @throws UnsupportedOperationException 如果事件没有实现 [Cancellable]
*/ */
fun cancel() { fun cancel()
_cancelled = true
}
} }
/** /**
* 实现这个接口的事件([Event])可以被取消. 在广播中取消不会影响广播过程. * 可被取消的事件的实现
*/ */
interface Cancellable : Subscribable { abstract class AbstractCancellableEvent : Event, CancellableEvent {
val cancelled: Boolean /**
* 事件是否已取消.
*/
override val isCancelled: Boolean get() = _cancelled
fun cancel() private var _cancelled: Boolean = false
/**
* 取消事件.
*/
override fun cancel() {
_cancelled = true
}
} }
/** /**
* 广播一个事件的唯一途径. * 广播一个事件的唯一途径.
*/ */
@UseExperimental(MiraiInternalAPI::class) @UseExperimental(MiraiInternalAPI::class)
suspend fun <E : Subscribable> E.broadcast(): E = apply { suspend fun <E : Event> E.broadcast(): E = apply {
if (this is BroadcastControllable && !this.shouldBroadcast) {
return@apply
}
if (this is BotEvent && !(this.bot as BotImpl<*>).onEvent(this)) { if (this is BotEvent && !(this.bot as BotImpl<*>).onEvent(this)) {
return@apply return@apply
} }
...@@ -81,7 +82,7 @@ suspend fun <E : Subscribable> E.broadcast(): E = apply { ...@@ -81,7 +82,7 @@ suspend fun <E : Subscribable> E.broadcast(): E = apply {
/** /**
* 可控制是否需要广播这个事件包 * 可控制是否需要广播这个事件包
*/ */
interface BroadcastControllable : Subscribable { interface BroadcastControllable : Event {
val shouldBroadcast: Boolean val shouldBroadcast: Boolean
get() = true get() = true
} }
\ No newline at end of file
...@@ -39,7 +39,7 @@ enum class ListeningStatus { ...@@ -39,7 +39,7 @@ enum class ListeningStatus {
* 事件监听器. * 事件监听器.
* 由 [subscribe] 等方法返回. * 由 [subscribe] 等方法返回.
*/ */
interface Listener<in E : Subscribable> : CompletableJob { interface Listener<in E : Event> : CompletableJob {
suspend fun onEvent(event: E): ListeningStatus suspend fun onEvent(event: E): ListeningStatus
} }
...@@ -47,7 +47,7 @@ interface Listener<in E : Subscribable> : CompletableJob { ...@@ -47,7 +47,7 @@ interface Listener<in E : Subscribable> : CompletableJob {
/** /**
* 在指定的 [CoroutineScope] 下订阅所有 [E] 及其子类事件. * 在指定的 [CoroutineScope] 下订阅所有 [E] 及其子类事件.
* 每当 [事件广播][Subscribable.broadcast] 时, [handler] 都会被执行. * 每当 [事件广播][Event.broadcast] 时, [handler] 都会被执行.
* *
* 当 [handler] 返回 [ListeningStatus.STOPPED] 时停止监听. * 当 [handler] 返回 [ListeningStatus.STOPPED] 时停止监听.
* 或 [Listener] complete 时结束. * 或 [Listener] complete 时结束.
...@@ -57,7 +57,7 @@ interface Listener<in E : Subscribable> : CompletableJob { ...@@ -57,7 +57,7 @@ interface Listener<in E : Subscribable> : CompletableJob {
* 例如: * 例如:
* ```kotlin * ```kotlin
* runBlocking { // this: CoroutineScope * runBlocking { // this: CoroutineScope
* subscribe<Subscribable> { /* 一些处理 */ } // 返回 Listener, 即 CompletableJob * subscribe<Event> { /* 一些处理 */ } // 返回 Listener, 即 CompletableJob
* } * }
* foo() * foo()
* ``` * ```
...@@ -66,7 +66,7 @@ interface Listener<in E : Subscribable> : CompletableJob { ...@@ -66,7 +66,7 @@ interface Listener<in E : Subscribable> : CompletableJob {
* *
* 要创建一个全局都存在的监听, 即守护协程, 请在 [GlobalScope] 下调用本函数: * 要创建一个全局都存在的监听, 即守护协程, 请在 [GlobalScope] 下调用本函数:
* ```kotlin * ```kotlin
* GlobalScope.subscribe<Subscribable> { /* 一些处理 */ } * GlobalScope.subscribe<Event> { /* 一些处理 */ }
* ``` * ```
* *
* *
...@@ -75,52 +75,52 @@ interface Listener<in E : Subscribable> : CompletableJob { ...@@ -75,52 +75,52 @@ interface Listener<in E : Subscribable> : CompletableJob {
* bot.subscribe<Subscribe> { /* 一些处理 */ } * bot.subscribe<Subscribe> { /* 一些处理 */ }
* ``` * ```
*/ */
inline fun <reified E : Subscribable> CoroutineScope.subscribe(crossinline handler: suspend E.(E) -> ListeningStatus): Listener<E> = inline fun <reified E : Event> CoroutineScope.subscribe(crossinline handler: suspend E.(E) -> ListeningStatus): Listener<E> =
E::class.subscribeInternal(Handler { it.handler(it) }) E::class.subscribeInternal(Handler { it.handler(it) })
/** /**
* 在指定的 [CoroutineScope] 下订阅所有 [E] 及其子类事件. * 在指定的 [CoroutineScope] 下订阅所有 [E] 及其子类事件.
* 每当 [事件广播][Subscribable.broadcast] 时, [listener] 都会被执行. * 每当 [事件广播][Event.broadcast] 时, [listener] 都会被执行.
* *
* 仅当 [Listener] complete 时结束. * 仅当 [Listener] complete 时结束.
* *
* @see subscribe 获取更多说明 * @see subscribe 获取更多说明
*/ */
inline fun <reified E : Subscribable> CoroutineScope.subscribeAlways(crossinline listener: suspend E.(E) -> Unit): Listener<E> = inline fun <reified E : Event> CoroutineScope.subscribeAlways(crossinline listener: suspend E.(E) -> Unit): Listener<E> =
E::class.subscribeInternal(Handler { it.listener(it); ListeningStatus.LISTENING }) E::class.subscribeInternal(Handler { it.listener(it); ListeningStatus.LISTENING })
/** /**
* 在指定的 [CoroutineScope] 下订阅所有 [E] 及其子类事件. * 在指定的 [CoroutineScope] 下订阅所有 [E] 及其子类事件.
* 仅在第一次 [事件广播][Subscribable.broadcast] 时, [listener] 会被执行. * 仅在第一次 [事件广播][Event.broadcast] 时, [listener] 会被执行.
* *
* 在这之前, 可通过 [Listener.complete] 来停止监听. * 在这之前, 可通过 [Listener.complete] 来停止监听.
* *
* @see subscribe 获取更多说明 * @see subscribe 获取更多说明
*/ */
inline fun <reified E : Subscribable> CoroutineScope.subscribeOnce(crossinline listener: suspend E.(E) -> Unit): Listener<E> = inline fun <reified E : Event> CoroutineScope.subscribeOnce(crossinline listener: suspend E.(E) -> Unit): Listener<E> =
E::class.subscribeInternal(Handler { it.listener(it); ListeningStatus.STOPPED }) E::class.subscribeInternal(Handler { it.listener(it); ListeningStatus.STOPPED })
/** /**
* 在指定的 [CoroutineScope] 下订阅所有 [E] 及其子类事件. * 在指定的 [CoroutineScope] 下订阅所有 [E] 及其子类事件.
* 每当 [事件广播][Subscribable.broadcast] 时, [listener] 都会被执行, 直到 [listener] 的返回值 [equals] 于 [valueIfStop] * 每当 [事件广播][Event.broadcast] 时, [listener] 都会被执行, 直到 [listener] 的返回值 [equals] 于 [valueIfStop]
* *
* 可在任意时刻通过 [Listener.complete] 来停止监听. * 可在任意时刻通过 [Listener.complete] 来停止监听.
* *
* @see subscribe 获取更多说明 * @see subscribe 获取更多说明
*/ */
inline fun <reified E : Subscribable, T> CoroutineScope.subscribeUntil(valueIfStop: T, crossinline listener: suspend E.(E) -> T): Listener<E> = inline fun <reified E : Event, T> CoroutineScope.subscribeUntil(valueIfStop: T, crossinline listener: suspend E.(E) -> T): Listener<E> =
E::class.subscribeInternal(Handler { if (it.listener(it) == valueIfStop) ListeningStatus.STOPPED else ListeningStatus.LISTENING }) E::class.subscribeInternal(Handler { if (it.listener(it) == valueIfStop) ListeningStatus.STOPPED else ListeningStatus.LISTENING })
/** /**
* 在指定的 [CoroutineScope] 下订阅所有 [E] 及其子类事件. * 在指定的 [CoroutineScope] 下订阅所有 [E] 及其子类事件.
* 每当 [事件广播][Subscribable.broadcast] 时, [listener] 都会被执行, * 每当 [事件广播][Event.broadcast] 时, [listener] 都会被执行,
* 如果 [listener] 的返回值 [equals] 于 [valueIfContinue], 则继续监听, 否则停止 * 如果 [listener] 的返回值 [equals] 于 [valueIfContinue], 则继续监听, 否则停止
* *
* 可在任意时刻通过 [Listener.complete] 来停止监听. * 可在任意时刻通过 [Listener.complete] 来停止监听.
* *
* @see subscribe 获取更多说明 * @see subscribe 获取更多说明
*/ */
inline fun <reified E : Subscribable, T> CoroutineScope.subscribeWhile(valueIfContinue: T, crossinline listener: suspend E.(E) -> T): Listener<E> = inline fun <reified E : Event, T> CoroutineScope.subscribeWhile(valueIfContinue: T, crossinline listener: suspend E.(E) -> T): Listener<E> =
E::class.subscribeInternal(Handler { if (it.listener(it) != valueIfContinue) ListeningStatus.STOPPED else ListeningStatus.LISTENING }) E::class.subscribeInternal(Handler { if (it.listener(it) != valueIfContinue) ListeningStatus.STOPPED else ListeningStatus.LISTENING })
// endregion // endregion
...@@ -146,7 +146,7 @@ inline fun <reified E : Subscribable, T> CoroutineScope.subscribeWhile(valueIfCo ...@@ -146,7 +146,7 @@ inline fun <reified E : Subscribable, T> CoroutineScope.subscribeWhile(valueIfCo
*/ */
@ListenersBuilderDsl @ListenersBuilderDsl
@Suppress("MemberVisibilityCanBePrivate", "unused") @Suppress("MemberVisibilityCanBePrivate", "unused")
inline class ListenerBuilder<out E : Subscribable>( inline class ListenerBuilder<out E : Event>(
@PublishedApi internal inline val handlerConsumer: CoroutineCoroutineScope.(Listener<E>) -> Unit @PublishedApi internal inline val handlerConsumer: CoroutineCoroutineScope.(Listener<E>) -> Unit
) { ) {
fun CoroutineCoroutineScope.handler(listener: suspend E.(E) -> ListeningStatus) { fun CoroutineCoroutineScope.handler(listener: suspend E.(E) -> ListeningStatus) {
......
...@@ -10,89 +10,327 @@ ...@@ -10,89 +10,327 @@
package net.mamoe.mirai.event.events package net.mamoe.mirai.event.events
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.*
import net.mamoe.mirai.contact.Member import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.contact.MemberPermission import net.mamoe.mirai.event.AbstractCancellableEvent
import net.mamoe.mirai.event.Event import net.mamoe.mirai.event.CancellableEvent
import net.mamoe.mirai.utils.WeakRef import net.mamoe.mirai.message.data.Image
import kotlin.properties.Delegates import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.utils.ExternalImage
abstract class BotEvent : Event { @Suppress("unused")
private lateinit var _bot: Bot class EventCancelledException : RuntimeException {
open val bot: Bot get() = _bot constructor() : super()
constructor(message: String?) : super(message)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
}
constructor(bot: Bot) : super() {
this._bot = bot
}
constructor() : super() // region Bot 状态
/**
* [Bot] 登录完成, 好友列表, 群组列表初始化完成
*/
data class BotOnlineEvent(override val bot: Bot) : BotActiveEvent
/**
* [Bot] 离线.
*/
sealed class BotOfflineEvent : BotActiveEvent {
/**
* 主动离线
*/
data class Active(override val bot: Bot, val cause: Throwable?) : BotOfflineEvent()
/**
* 被挤下线
*/
data class Force(override val bot: Bot, val title: String, val message: String) : BotOfflineEvent(), Packet
} }
class BotLoginSucceedEvent(bot: Bot) : BotEvent(bot) // endregion
class BotOfflineEvent(bot: Bot) : BotEvent(bot) // region 消息
class BotReadyEvent(bot: Bot) : BotEvent(bot) sealed class MessageSendEvent : BotEvent, BotActiveEvent, AbstractCancellableEvent() {
abstract val target: Contact
final override val bot: Bot
get() = target.bot
interface GroupEvent { data class GroupMessageSendEvent(
val group: Group override val target: Group,
var message: MessageChain
) : MessageSendEvent(), CancellableEvent
data class FriendMessageSendEvent(
override val target: QQ,
var message: MessageChain
) : MessageSendEvent(), CancellableEvent
} }
class AddGroupEvent(bot: Bot, override val group: Group) : BotEvent(bot), GroupEvent // endregion
// region 图片
/**
* 图片上传前. 可以阻止上传
*/
data class BeforeImageUploadEvent(
val target: Contact,
val source: ExternalImage
) : BotEvent, BotActiveEvent, AbstractCancellableEvent() {
override val bot: Bot
get() = target.bot
}
class RemoveGroupEvent(bot: Bot, override val group: Group) : BotEvent(bot), GroupEvent /**
* 图片上传完成
*/
sealed class ImageUploadEvent : BotEvent, BotActiveEvent, AbstractCancellableEvent() {
abstract val target: Contact
abstract val source: ExternalImage
override val bot: Bot
get() = target.bot
data class Succeed(
override val target: Contact,
override val source: ExternalImage,
val image: Image
) : ImageUploadEvent(), CancellableEvent
data class Failed(
override val target: Contact,
override val source: ExternalImage,
val errno: Int,
val message: String
) : ImageUploadEvent(), CancellableEvent
}
class BotGroupPermissionChangeEvent( // endregion
bot: Bot,
// region 群
/**
* Bot 在群里的权限被改变. 操作人一定是群主
*/
data class BotGroupPermissionChangeEvent(
override val group: Group, override val group: Group,
val origin: MemberPermission, val origin: MemberPermission,
val new: MemberPermission val new: MemberPermission
) : BotEvent(bot), GroupEvent ) : BotPassiveEvent, GroupEvent
// region 群设置
interface GroupSettingChangeEvent<T> : GroupEvent { /**
* 群设置改变. 此事件广播前修改就已经完成.
*/
interface GroupSettingChangeEvent<T> : GroupEvent, BotPassiveEvent {
val origin: T val origin: T
val new: T val new: T
} }
class GroupNameChangeEvent( /**
bot: Bot, * 群名改变. 此事件广播前修改就已经完成.
*/
data class GroupNameChangeEvent(
override val origin: String,
override val new: String,
override val group: Group, override val group: Group,
/**
* 操作人. 为 null 时则是机器人操作
*/
val operator: Member?
) : GroupSettingChangeEvent<String>, Packet
/**
* 入群公告改变. 此事件广播前修改就已经完成.
*/
data class GroupEntranceAnnouncementChangeEvent(
override val origin: String, override val origin: String,
override val new: String override val new: String,
) : BotEvent(bot), GroupSettingChangeEvent<String> override val group: Group,
/**
* 操作人. 为 null 时则是机器人操作
*/
val operator: Member?
) : GroupSettingChangeEvent<String>, Packet
class GroupMuteAllEvent(
bot: Bot, /**
* 群 "全员禁言" 功能状态改变. 此事件广播前修改就已经完成.
*/
data class GroupMuteAllEvent(
override val origin: Boolean,
override val new: Boolean,
override val group: Group, override val group: Group,
/**
* 操作人. 为 null 时则是机器人操作
*/
val operator: Member?
) : GroupSettingChangeEvent<Boolean>, Packet
/**
* 群 "匿名聊天" 功能状态改变. 此事件广播前修改就已经完成.
*/
data class GroupAllowAnonymousChatEvent(
override val origin: Boolean, override val origin: Boolean,
override val new: Boolean override val new: Boolean,
) : BotEvent(bot), GroupSettingChangeEvent<Boolean> override val group: Group,
/**
* 操作人. 为 null 时则是机器人操作
*/
val operator: Member?
) : GroupSettingChangeEvent<Boolean>, Packet
class GroupConfessTalkEvent( /**
bot: Bot, * 群 "坦白说" 功能状态改变. 此事件广播前修改就已经完成.
*/
data class GroupAllowConfessTalkEvent(
override val origin: Boolean,
override val new: Boolean,
override val group: Group, override val group: Group,
/**
* 操作人. 为 null 时则是机器人操作
*/
val operator: Member?
) : GroupSettingChangeEvent<Boolean>, Packet
/**
* 群 "允许群员邀请好友加群" 功能状态改变. 此事件广播前修改就已经完成.
*/
data class GroupAllowMemberInviteEvent(
override val origin: Boolean, override val origin: Boolean,
override val new: Boolean override val new: Boolean,
) : BotEvent(bot), GroupSettingChangeEvent<Boolean> override val group: Group,
/**
* 操作人. 为 null 时则是机器人操作
*/
val operator: Member?
) : GroupSettingChangeEvent<Boolean>, Packet
// endregion
// region 群成员
// region 成员变更
/**
* 成员加入群的事件
*/
data class MemberJoinEvent(override val member: Member) : GroupMemberEvent, BotPassiveEvent
interface GroupMemberEvent : GroupEvent { /**
val member: Member * 成员离开群的事件
override val group: Group */
get() = member.group sealed class MemberLeaveEvent : GroupMemberEvent {
/**
* 成员被踢出群. 成员不可能是机器人自己.
*/
data class Kick(
override val member: Member,
/**
* 操作人. 为 null 则是机器人操作
*/
val operator: Member?
) : MemberLeaveEvent(), Packet
/**
* 成员主动离开
*/
data class Quit(override val member: Member) : MemberLeaveEvent()
} }
class MemberJoinEvent(bot: Bot, override val member: Member) : BotEvent(bot), GroupMemberEvent // endregion
class MemberLeftEvent(bot: Bot, override val member: Member) : BotEvent(bot), GroupMemberEvent // region 名片和头衔
class MemberMuteEvent(bot: Bot, override val member: Member) : BotEvent(bot), GroupMemberEvent /**
* 群名片改动. 此事件广播前修改就已经完成.
*/
data class MemberCardChangeEvent(
/**
* 修改前
*/
val origin: String,
/**
* 修改后
*/
val new: String,
class MemberPermissionChangeEvent( override val member: Member,
bot: Bot,
/**
* 操作人. 为 null 时则是机器人操作. 可能与 [member] 引用相同, 此时为群员自己修改.
*/
val operator: Member?
) : GroupMemberEvent
/**
* 群头衔改动. 一定为群主操作
*/
data class MemberSpecialTitleChangeEvent(
/**
* 修改前
*/
val origin: String,
/**
* 修改后
*/
val new: String,
override val member: Member
) : GroupMemberEvent
// endregion
// region 成员权限
/**
* 成员权限改变的事件. 成员不可能是机器人自己.
*/
data class MemberPermissionChangeEvent(
override val member: Member, override val member: Member,
val origin: MemberPermission, val origin: MemberPermission,
val new: MemberPermission val new: MemberPermission
) : BotEvent(bot), GroupMemberEvent ) : GroupMemberEvent, BotPassiveEvent, Packet
// endregion
// region 禁言
/**
* 群成员被禁言事件. 操作人和被禁言的成员都不可能是机器人本人
*/
data class MemberMuteEvent(
override val member: Member,
val durationSeconds: Int,
/**
* 操作人. 为 null 则为机器人操作
*/
val operator: Member?
) : GroupMemberEvent, Packet
/**
* 群成员被取消禁言事件. 操作人和被禁言的成员都不可能是机器人本人
*/
data class MemberUnmuteEvent(
override val member: Member,
/**
* 操作人. 为 null 则为机器人操作
*/
val operator: Member?
) : GroupMemberEvent, Packet
// endregion
// endregion
// endregion
\ No newline at end of file
package net.mamoe.mirai.event.events
import net.mamoe.mirai.Bot
import net.mamoe.mirai.data.Packet
/**
* 被挤下线
*/
data class ForceOfflineEvent(
override val bot: Bot,
val title: String,
val tips: String
) : BotEvent(), Packet
\ No newline at end of file
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.event.events
import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.data.EventPacket
import net.mamoe.mirai.data.OnlineStatus
data class FriendStatusChanged(
val qq: QQ,
val status: OnlineStatus
) : EventPacket
...@@ -9,29 +9,54 @@ ...@@ -9,29 +9,54 @@
package net.mamoe.mirai.event.events package net.mamoe.mirai.event.events
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.contact.QQ import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.data.EventPacket import net.mamoe.mirai.event.Event
import net.mamoe.mirai.utils.getValue
import net.mamoe.mirai.utils.unsafeWeakRef
import kotlin.jvm.JvmOverloads
/** /**
* 陌生人请求添加机器人账号为好友 * 有关一个 [Bot] 的事件
*/ */
class ReceiveFriendAddRequestEvent( interface BotEvent : Event {
_qq: QQ, val bot: Bot
/**
* 验证消息
*/
val message: String
) : EventPacket {
val qq: QQ by _qq.unsafeWeakRef()
/**
* 同意这个请求
*
* @param remark 备注名, 不设置则需为 `null`
*/
@JvmOverloads
suspend fun approve(remark: String? = null): Unit = qq.bot.approveFriendAddRequest(qq.id, remark)
} }
/**
* [Bot] 被动接收的事件. 这些事件可能与机器人有关
*/
interface BotPassiveEvent : BotEvent
/**
* 由 [Bot] 主动发起的动作的事件
*/
interface BotActiveEvent : BotEvent
/**
* 有关群的事件
*/
interface GroupEvent : BotEvent {
val group: Group
override val bot: Bot
get() = group.bot
}
/**
* 有关群成员的事件
*/
interface GroupMemberEvent : GroupEvent {
val member: Member
override val group: Group
get() = member.group
}
/**
* 有关群的事件
*/
interface FriendEvent : BotEvent {
val friend: QQ
override val bot: Bot
get() = friend.bot
}
\ No newline at end of file
...@@ -10,9 +10,9 @@ ...@@ -10,9 +10,9 @@
package net.mamoe.mirai.event.internal package net.mamoe.mirai.event.internal
import kotlinx.coroutines.* import kotlinx.coroutines.*
import net.mamoe.mirai.event.Event
import net.mamoe.mirai.event.Listener import net.mamoe.mirai.event.Listener
import net.mamoe.mirai.event.ListeningStatus import net.mamoe.mirai.event.ListeningStatus
import net.mamoe.mirai.event.Subscribable
import net.mamoe.mirai.utils.LockFreeLinkedList import net.mamoe.mirai.utils.LockFreeLinkedList
import net.mamoe.mirai.utils.MiraiDebugAPI import net.mamoe.mirai.utils.MiraiDebugAPI
import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.MiraiLogger
...@@ -29,14 +29,14 @@ import kotlin.reflect.KClass ...@@ -29,14 +29,14 @@ import kotlin.reflect.KClass
var EventDisabled = false var EventDisabled = false
@PublishedApi @PublishedApi
internal fun <L : Listener<E>, E : Subscribable> KClass<out E>.subscribeInternal(listener: L): L { internal fun <L : Listener<E>, E : Event> KClass<out E>.subscribeInternal(listener: L): L {
this.listeners().addLast(listener) this.listeners().addLast(listener)
return listener return listener
} }
@PublishedApi @PublishedApi
@Suppress("FunctionName") @Suppress("FunctionName")
internal fun <E : Subscribable> CoroutineScope.Handler(handler: suspend (E) -> ListeningStatus): Handler<E> { internal fun <E : Event> CoroutineScope.Handler(handler: suspend (E) -> ListeningStatus): Handler<E> {
return Handler(coroutineContext[Job], coroutineContext, handler) return Handler(coroutineContext[Job], coroutineContext, handler)
} }
...@@ -45,7 +45,7 @@ private inline fun inline(block: () -> Unit) = block() ...@@ -45,7 +45,7 @@ private inline fun inline(block: () -> Unit) = block()
* 事件处理器. * 事件处理器.
*/ */
@PublishedApi @PublishedApi
internal class Handler<in E : Subscribable> internal class Handler<in E : Event>
@PublishedApi internal constructor(parentJob: Job?, private val subscriberContext: CoroutineContext, @JvmField val handler: suspend (E) -> ListeningStatus) : @PublishedApi internal constructor(parentJob: Job?, private val subscriberContext: CoroutineContext, @JvmField val handler: suspend (E) -> ListeningStatus) :
Listener<E>, CompletableJob by Job(parentJob) { Listener<E>, CompletableJob by Job(parentJob) {
...@@ -79,21 +79,21 @@ internal class Handler<in E : Subscribable> ...@@ -79,21 +79,21 @@ internal class Handler<in E : Subscribable>
/** /**
* 这个事件类的监听器 list * 这个事件类的监听器 list
*/ */
internal fun <E : Subscribable> KClass<out E>.listeners(): EventListeners<E> = EventListenerManager.get(this) internal fun <E : Event> KClass<out E>.listeners(): EventListeners<E> = EventListenerManager.get(this)
internal class EventListeners<E : Subscribable> : LockFreeLinkedList<Listener<E>>() internal class EventListeners<E : Event> : LockFreeLinkedList<Listener<E>>()
/** /**
* 管理每个事件 class 的 [EventListeners]. * 管理每个事件 class 的 [EventListeners].
* [EventListeners] 是 lazy 的: 它们只会在被需要的时候才创建和存储. * [EventListeners] 是 lazy 的: 它们只会在被需要的时候才创建和存储.
*/ */
internal object EventListenerManager { internal object EventListenerManager {
private data class Registry<E : Subscribable>(val clazz: KClass<E>, val listeners: EventListeners<E>) private data class Registry<E : Event>(val clazz: KClass<E>, val listeners: EventListeners<E>)
private val registries = LockFreeLinkedList<Registry<*>>() private val registries = LockFreeLinkedList<Registry<*>>()
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
internal fun <E : Subscribable> get(clazz: KClass<out E>): EventListeners<E> { internal fun <E : Event> get(clazz: KClass<out E>): EventListeners<E> {
return registries.filteringGetOrAdd({ it.clazz == clazz }) { return registries.filteringGetOrAdd({ it.clazz == clazz }) {
Registry( Registry(
clazz, clazz,
...@@ -105,7 +105,7 @@ internal object EventListenerManager { ...@@ -105,7 +105,7 @@ internal object EventListenerManager {
// inline: NO extra Continuation // inline: NO extra Continuation
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
internal suspend inline fun Subscribable.broadcastInternal() { internal suspend inline fun Event.broadcastInternal() {
if (EventDisabled) return if (EventDisabled) return
callAndRemoveIfRequired(this::class.listeners()) callAndRemoveIfRequired(this::class.listeners())
...@@ -113,18 +113,18 @@ internal suspend inline fun Subscribable.broadcastInternal() { ...@@ -113,18 +113,18 @@ internal suspend inline fun Subscribable.broadcastInternal() {
var supertypes = this::class.supertypes var supertypes = this::class.supertypes
while (true) { while (true) {
val superSubscribableType = supertypes.firstOrNull { val superSubscribableType = supertypes.firstOrNull {
it.classifier as? KClass<out Subscribable> != null it.classifier as? KClass<out Event> != null
} }
superSubscribableType?.let { superSubscribableType?.let {
callAndRemoveIfRequired((it.classifier as KClass<out Subscribable>).listeners()) callAndRemoveIfRequired((it.classifier as KClass<out Event>).listeners())
} }
supertypes = (superSubscribableType?.classifier as? KClass<*>)?.supertypes ?: return supertypes = (superSubscribableType?.classifier as? KClass<*>)?.supertypes ?: return
} }
} }
private suspend inline fun <E : Subscribable> E.callAndRemoveIfRequired(listeners: EventListeners<E>) { private suspend inline fun <E : Event> E.callAndRemoveIfRequired(listeners: EventListeners<E>) {
// atomic foreach // atomic foreach
listeners.forEach { listeners.forEach {
if (it.onEvent(this) == ListeningStatus.STOPPED) { if (it.onEvent(this) == ListeningStatus.STOPPED) {
......
...@@ -13,7 +13,6 @@ import net.mamoe.mirai.Bot ...@@ -13,7 +13,6 @@ import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.contact.MemberPermission import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.event.BroadcastControllable
import net.mamoe.mirai.message.data.At import net.mamoe.mirai.message.data.At
import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.utils.getValue import net.mamoe.mirai.utils.getValue
...@@ -30,21 +29,14 @@ class GroupMessage( ...@@ -30,21 +29,14 @@ class GroupMessage(
val permission: MemberPermission, val permission: MemberPermission,
sender: Member, sender: Member,
override val message: MessageChain override val message: MessageChain
) : MessagePacket<Member, Group>(bot), BroadcastControllable { ) : MessagePacket<Member, Group>(bot) {
val group: Group by group.unsafeWeakRef() val group: Group by group.unsafeWeakRef()
override val sender: Member by sender.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 override val subject: Group get() = group
inline fun At.member(): Member = group[this.target] inline fun At.member(): Member = group[this.target]
inline fun Long.member(): Member = group[this] inline fun Long.member(): Member = group[this]
override fun toString(): String = override fun toString(): String =
"GroupMessage(group=${group.id}, senderName=$senderName, sender=${sender.id}, permission=${permission.name}, message=$message)" "GroupMessage(group=${group.id}, senderName=$senderName, sender=${sender.id}, permission=${permission.name}, message=$message)"
override val shouldBroadcast: Boolean
get() = bot.uin != sender.id // 自己会收到自己发的消息
} }
\ No newline at end of file
...@@ -37,7 +37,7 @@ expect abstract class MessagePacket<TSender : QQ, TSubject : Contact>(bot: Bot) ...@@ -37,7 +37,7 @@ expect abstract class MessagePacket<TSender : QQ, TSubject : Contact>(bot: Bot)
*/ // Tips: 在 IntelliJ 中 (左侧边栏) 打开 `Structure`, 可查看类结构 */ // Tips: 在 IntelliJ 中 (左侧边栏) 打开 `Structure`, 可查看类结构
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")
@MiraiInternalAPI @MiraiInternalAPI
abstract class MessagePacketBase<TSender : QQ, TSubject : Contact>(_bot: Bot) : EventPacket, BotEvent() { abstract class MessagePacketBase<TSender : QQ, TSubject : Contact>(_bot: Bot) : EventPacket, BotEvent {
/** /**
* 接受到这条消息的 * 接受到这条消息的
*/ */
......
...@@ -59,14 +59,14 @@ interface Message { ...@@ -59,14 +59,14 @@ interface Message {
infix fun eq(other: Message): Boolean = this == other infix fun eq(other: Message): Boolean = this == other
/** /**
* 将 [stringValue] 与 [other] 比较 * 将 [toString] 与 [other] 比较
*/ */
infix fun eq(other: String): Boolean = this.toString() == other infix fun eq(other: String): Boolean = this.toString() == other
operator fun contains(sub: String): Boolean = false operator fun contains(sub: String): Boolean = false
/** /**
* 把 [this] 连接到 [tail] 的头部. 类似于字符串相加. * 把 `this` 连接到 [tail] 的头部. 类似于字符串相加.
* *
* 例: * 例:
* ```kotlin * ```kotlin
...@@ -92,7 +92,7 @@ interface Message { ...@@ -92,7 +92,7 @@ interface Message {
override fun toString(): String override fun toString(): String
operator fun plus(another: Message): MessageChain = this.followedBy(another) operator fun plus(another: Message): MessageChain = this.followedBy(another)
operator fun plus(another: String): MessageChain = this.followedBy(another.toString().toMessage()) operator fun plus(another: String): MessageChain = this.followedBy(another.toMessage())
// `+ ""` will be resolved to `plus(String)` instead of `plus(CharSeq)` // `+ ""` will be resolved to `plus(String)` instead of `plus(CharSeq)`
operator fun plus(another: CharSequence): MessageChain = this.followedBy(another.toString().toMessage()) operator fun plus(another: CharSequence): MessageChain = this.followedBy(another.toString().toMessage())
} }
......
...@@ -23,6 +23,21 @@ interface MessageSource : Message { ...@@ -23,6 +23,21 @@ interface MessageSource : Message {
*/ */
val messageUid: Long val messageUid: Long
/**
* 发送人号码
*/
val senderId: Long
/**
* 群号码
*/
val groupId: Long
/**
* 原消息内容
*/
val sourceMessage: MessageChain
/** /**
* 固定返回空字符串 ("") * 固定返回空字符串 ("")
*/ */
......
...@@ -17,6 +17,6 @@ package net.mamoe.mirai.event ...@@ -17,6 +17,6 @@ package net.mamoe.mirai.event
object Events { object Events {
/* /*
@JvmStatic @JvmStatic
fun <E : Subscribable> subscribe(type: Class<E>, handler: suspend (E) -> ListeningStatus) = fun <E : Event> subscribe(type: Class<E>, handler: suspend (E) -> ListeningStatus) =
runBlocking { type.kotlin.subscribe(handler) }*/ runBlocking { type.kotlin.subscribe(handler) }*/
} }
...@@ -9,11 +9,11 @@ ...@@ -9,11 +9,11 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import io.ktor.util.cio.use
import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.io.ByteWriteChannel import kotlinx.coroutines.io.ByteWriteChannel
import kotlinx.coroutines.io.close
import kotlinx.coroutines.io.jvm.nio.copyTo import kotlinx.coroutines.io.jvm.nio.copyTo
import kotlinx.coroutines.io.reader import kotlinx.coroutines.io.reader
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
...@@ -44,7 +44,7 @@ internal class DefaultLoginSolver : LoginSolver() { ...@@ -44,7 +44,7 @@ internal class DefaultLoginSolver : LoginSolver() {
tempFile.createNewFile() tempFile.createNewFile()
bot.logger.info("需要图片验证码登录, 验证码为 4 字母") bot.logger.info("需要图片验证码登录, 验证码为 4 字母")
try { try {
tempFile.writeChannel().use { writeFully(data) } tempFile.writeChannel().apply { writeFully(data); close() }
bot.logger.info("将会显示字符图片. 若看不清字符图片, 请查看文件 ${tempFile.absolutePath}") bot.logger.info("将会显示字符图片. 若看不清字符图片, 请查看文件 ${tempFile.absolutePath}")
} catch (e: Exception) { } catch (e: Exception) {
bot.logger.info("无法写出验证码文件(${e.message}), 请尝试查看以上字符图片") bot.logger.info("无法写出验证码文件(${e.message}), 请尝试查看以上字符图片")
......
...@@ -11,7 +11,6 @@ ...@@ -11,7 +11,6 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import io.ktor.util.asStream
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.io.core.Input import kotlinx.io.core.Input
...@@ -132,7 +131,7 @@ suspend inline fun InputStream.suspendToExternalImage(): ExternalImage = withCon ...@@ -132,7 +131,7 @@ suspend inline fun InputStream.suspendToExternalImage(): ExternalImage = withCon
fun Input.toExternalImage(): ExternalImage { fun Input.toExternalImage(): ExternalImage {
val file = createTempFile().apply { deleteOnExit() } val file = createTempFile().apply { deleteOnExit() }
file.outputStream().asOutput().use { file.outputStream().asOutput().use {
this.asStream().asInput().copyTo(it) this.copyTo(it)
} }
return file.toExternalImage() return file.toExternalImage()
} }
......
...@@ -17,7 +17,7 @@ import kotlin.system.exitProcess ...@@ -17,7 +17,7 @@ import kotlin.system.exitProcess
import kotlin.test.Test import kotlin.test.Test
class TestEvent : Subscribable { class TestEvent : Event {
var triggered = false var triggered = false
} }
...@@ -46,7 +46,7 @@ class EventTests { ...@@ -46,7 +46,7 @@ class EventTests {
} }
open class ParentEvent : Subscribable { open class ParentEvent : Event {
var triggered = false var triggered = false
} }
......
...@@ -31,6 +31,7 @@ android { ...@@ -31,6 +31,7 @@ android {
exclude 'META-INF/ktor-http-cio.kotlin_module' exclude 'META-INF/ktor-http-cio.kotlin_module'
exclude 'META-INF/ktor-client-core.kotlin_module' exclude 'META-INF/ktor-client-core.kotlin_module'
exclude "META-INF/kotlinx-serialization-runtime.kotlin_module" exclude "META-INF/kotlinx-serialization-runtime.kotlin_module"
exclude 'META-INF/ktor-io.kotlin_module'
} }
} }
......
...@@ -20,8 +20,8 @@ import net.mamoe.mirai.Bot ...@@ -20,8 +20,8 @@ import net.mamoe.mirai.Bot
import net.mamoe.mirai.alsoLogin import net.mamoe.mirai.alsoLogin
import net.mamoe.mirai.contact.Member import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.contact.MemberPermission import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.event.Subscribable import net.mamoe.mirai.event.Event
import net.mamoe.mirai.event.events.ReceiveFriendAddRequestEvent import net.mamoe.mirai.event.events.BotEvent
import net.mamoe.mirai.event.subscribeAlways import net.mamoe.mirai.event.subscribeAlways
import net.mamoe.mirai.event.subscribeGroupMessages import net.mamoe.mirai.event.subscribeGroupMessages
import net.mamoe.mirai.event.subscribeMessages import net.mamoe.mirai.event.subscribeMessages
...@@ -48,14 +48,14 @@ suspend fun main() { ...@@ -48,14 +48,14 @@ suspend fun main() {
// override config here. // override config here.
}.alsoLogin() }.alsoLogin()
// 任何可以监听的对象都继承 Subscribable, 因此这个订阅会订阅全部的事件. // 任何可以监听的对象都继承 Event, 因此这个订阅会订阅全部的事件.
GlobalScope.subscribeAlways<Subscribable> { GlobalScope.subscribeAlways<Event> {
//bot.logger.verbose("收到了一个事件: $this") //bot.logger.verbose("收到了一个事件: $this")
} }
// 全局范围订阅事件, 不受 bot 实例影响 // 全局范围订阅事件, 不受 bot 实例影响
GlobalScope.subscribeAlways<ReceiveFriendAddRequestEvent> { GlobalScope.subscribeAlways<BotEvent> {
it.approve()
} }
// 订阅来自这个 bot 的群消息事件 // 订阅来自这个 bot 的群消息事件
...@@ -81,14 +81,6 @@ suspend fun main() { ...@@ -81,14 +81,6 @@ suspend fun main() {
"你好" reply "你好!" "你好" reply "你好!"
startsWith("profile", removePrefix = true) {
val account = it.trim()
if (account.isNotEmpty()) {
bot.getFriend(account.toLong())
} else {
sender
}.queryProfile().toString().reply()
}
"grouplist" reply { "grouplist" reply {
//"https://ssl.ptlogin2.qq.com/jump?pt_clientver=5509&pt_src=1&keyindex=9&clientuin=" + bot.qqAccount + "&clientkey=" + com.tick_tock.pctim.utils.Util.byte2HexString( //"https://ssl.ptlogin2.qq.com/jump?pt_clientver=5509&pt_src=1&keyindex=9&clientuin=" + bot.qqAccount + "&clientkey=" + com.tick_tock.pctim.utils.Util.byte2HexString(
......
...@@ -52,7 +52,7 @@ internal class BlockingGroupImpl(private val delegate: Group) : BlockingGroup { ...@@ -52,7 +52,7 @@ internal class BlockingGroupImpl(private val delegate: Group) : BlockingGroup {
override fun toFullString(): String = delegate.toFullString() override fun toFullString(): String = delegate.toFullString()
override fun getMember(id: Long): BlockingMember = delegate[id].blocking() override fun getMember(id: Long): BlockingMember = delegate[id].blocking()
override fun getBot(): BlockingBot = delegate.bot.blocking() override fun getBot(): BlockingBot = delegate.bot.blocking()
override fun getAnnouncement(): String = delegate.announcement override fun getAnnouncement(): String = delegate.entranceAnnouncement
@UseExperimental(MiraiInternalAPI::class) @UseExperimental(MiraiInternalAPI::class)
override fun getMembers(): Map<Long, BlockingMember> = override fun getMembers(): Map<Long, BlockingMember> =
delegate.members.delegate.toList().associateBy { it.id }.mapValues { it.value.blocking() } delegate.members.delegate.toList().associateBy { it.id }.mapValues { it.value.blocking() }
......
...@@ -11,7 +11,7 @@ package net.mamoe.mirai.imageplugin ...@@ -11,7 +11,7 @@ package net.mamoe.mirai.imageplugin
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import net.mamoe.mirai.event.events.BotLoginSucceedEvent import net.mamoe.mirai.event.events.BotOnlineEvent
import net.mamoe.mirai.event.subscribeAlways import net.mamoe.mirai.event.subscribeAlways
import net.mamoe.mirai.event.subscribeMessages import net.mamoe.mirai.event.subscribeMessages
import net.mamoe.mirai.plugin.PluginBase import net.mamoe.mirai.plugin.PluginBase
...@@ -22,7 +22,7 @@ class ImageSenderMain : PluginBase() { ...@@ -22,7 +22,7 @@ class ImageSenderMain : PluginBase() {
@MiraiExperimentalAPI @MiraiExperimentalAPI
override fun onEnable() { override fun onEnable() {
logger.info("Image Sender plugin enabled") logger.info("Image Sender plugin enabled")
GlobalScope.subscribeAlways<BotLoginSucceedEvent> { GlobalScope.subscribeAlways<BotOnlineEvent> {
logger.info("${this.bot.uin} login succeed, it will be controlled by Image Sender Plugin") logger.info("${this.bot.uin} login succeed, it will be controlled by Image Sender Plugin")
this.bot.subscribeMessages { this.bot.subscribeMessages {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment