Commit 579b8da7 authored by Him188's avatar Him188

Merge branch 'dev'

parents 9892af59 181f4a53
# Version 1.x # Version 1.x
## `1.1.0` 2020/7/9
- 支持 Android 手表协议 (`BotConfiguration.MiraiProtocol.ANDROID_PAD`)
- `EventHandler` 现在支持 `Nothing` 类型.
- 修复无需同意直接进群时,在加载新群信息完成前收到消息过早处理的问题 (#370)
- 修复在某些情况下,管理员邀请群Bot加群会被误判为群成员申请加群的问题 (#402 by [@kenvix](https://github.com/kenvix))
- 修复从其他客户端加群时未同步的问题 (#404, #410)
- 修复 `ConfigPushSvc.PushReq` 解析失败的问题 (#417)
- 修复 `_lowLevelGetGroupActiveData`
- 修复 `SimpleListenerHost.coroutineScope` 潜在的 Job 被覆盖的问题
## `1.0.4` 2020/7/2
- 修复上传图片失败时内存泄露的问题 (#385)
- 修复大量图片同时上传时出错的问题 (#387)
- 修复在一些情况下 BotOfflineEvent 没有正常处理而无法继续接收消息的问题 (#376)
- 修复 Bot 在某个群 T 出某个人导致 Bot 终止的问题 (#372)
- 修复 `@PlannedRemoval` 的文档
## `1.1-EA2` 2020/7/2
- 添加 `BotConfiguration.json`, 作为序列化时使用的 Json format, 修复潜在的因 kotlinx.serialization 进行不兼容更新而导致的不兼容.
**不兼容变更**:
- Image.imageId 后缀由 `.mirai` 变为图片文件实际类型, 如 `.png`, `.jpg`. 兼容原 `.mirai` 后缀.
**修复**:
- ([1.0.4](https://github.com/mamoe/mirai/releases/tag/1.0.4) 中修复的问题)
- ([1.0.3](https://github.com/mamoe/mirai/releases/tag/1.0.3) 中修复的问题)
## `1.0.3` 2020/6/29
- 修复 friendlist.GetTroopListReqV2:java.lang.IllegalStateException: type mismatch 10 (#405)
## `1.1-EA` 2020/6/16 ## `1.1-EA` 2020/6/16
**1.1.0 Early Access** / **1.1.0 预览版**
**此版本新增的 API 可能不稳定, 且可能在下一个版本中删除.**
**主要**: **主要**:
- 添加实验性 `CodableMessage` 作为支持 mirai 码的 `Message` 的接口. - 添加实验性 `CodableMessage` 作为支持 mirai 码的 `Message` 的接口.
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
object Versions { object Versions {
object Mirai { object Mirai {
const val version = "1.1-EA" const val version = "1.1.0"
} }
object Kotlin { object Kotlin {
......
...@@ -74,7 +74,7 @@ object GitHub { ...@@ -74,7 +74,7 @@ object GitHub {
fun upload(file: File, project: Project, repo: String, targetFilePath: String) = runBlocking { fun upload(file: File, project: Project, repo: String, targetFilePath: String) = runBlocking {
val token = getGithubToken(project) val token = getGithubToken(project)
println("token.length=${token.length}") println("token.length=${token.length}")
val url = "https://api.github.com/repos/mamoe/$repo/contents/$targetFilePath" val url = "https://api.github.com/repos/project-mirai/$repo/contents/$targetFilePath"
retryCatching(100, onFailure = { delay(30_000) }) { // 403 forbidden? retryCatching(100, onFailure = { delay(30_000) }) { // 403 forbidden?
Http.put<String>("$url?access_token=$token") { Http.put<String>("$url?access_token=$token") {
val sha = retryCatching(3, onFailure = { delay(30_000) }) { val sha = retryCatching(3, onFailure = { delay(30_000) }) {
...@@ -121,7 +121,7 @@ object GitHub { ...@@ -121,7 +121,7 @@ object GitHub {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
val response = Jsoup val response = Jsoup
.connect( .connect(
"https://api.github.com/repos/mamoe/$repo/contents/$filePath?access_token=" + getGithubToken( "https://api.github.com/repos/project-mirai/$repo/contents/$filePath?access_token=" + getGithubToken(
project project
) )
) )
...@@ -150,7 +150,7 @@ object GitHub { ...@@ -150,7 +150,7 @@ object GitHub {
val resp = withContext(Dispatchers.IO) { val resp = withContext(Dispatchers.IO) {
Jsoup Jsoup
.connect( .connect(
"https://api.github.com/repos/mamoe/$repo/git/ref/heads/$branch?access_token=" + getGithubToken( "https://api.github.com/repos/project-mirai/$repo/git/ref/heads/$branch?access_token=" + getGithubToken(
project project
) )
) )
......
...@@ -24,7 +24,7 @@ bintray { ...@@ -24,7 +24,7 @@ bintray {
pkg { pkg {
repo = 'mirai' repo = 'mirai'
name = project.name name = "mirai-core"
licenses = ['AGPL'] licenses = ['AGPL']
vcsUrl = miraiGitHubUrl vcsUrl = miraiGitHubUrl
websiteUrl = miraiGitHubUrl websiteUrl = miraiGitHubUrl
......
...@@ -74,11 +74,13 @@ internal abstract class BotImpl<N : BotNetworkHandler> constructor( ...@@ -74,11 +74,13 @@ internal abstract class BotImpl<N : BotNetworkHandler> constructor(
// bot 还未登录就被 close // bot 还未登录就被 close
return@subscribeAlways return@subscribeAlways
} }
if (network.areYouOk() && event !is BotOfflineEvent.Force) { /*
if (network.areYouOk() && event !is BotOfflineEvent.Force && event !is BotOfflineEvent.MsfOffline) {
// network 运行正常 // network 运行正常
return@subscribeAlways return@subscribeAlways
} }*/
when (event) { when (event) {
is BotOfflineEvent.MsfOffline,
is BotOfflineEvent.Dropped, is BotOfflineEvent.Dropped,
is BotOfflineEvent.RequireReconnect is BotOfflineEvent.RequireReconnect
-> { -> {
...@@ -105,7 +107,12 @@ internal abstract class BotImpl<N : BotNetworkHandler> constructor( ...@@ -105,7 +107,12 @@ internal abstract class BotImpl<N : BotNetworkHandler> constructor(
@OptIn(ThisApiMustBeUsedInWithConnectionLockBlock::class) @OptIn(ThisApiMustBeUsedInWithConnectionLockBlock::class)
relogin((event as? BotOfflineEvent.Dropped)?.cause) relogin((event as? BotOfflineEvent.Dropped)?.cause)
} }
launch { BotReloginEvent(bot, (event as? BotOfflineEvent.Dropped)?.cause).broadcast() } launch {
BotReloginEvent(
bot,
(event as? BotOfflineEvent.CauseAware)?.cause
).broadcast()
}
return return
}.getOrElse { }.getOrElse {
if (it is LoginFailedException && !it.killBot) { if (it is LoginFailedException && !it.killBot) {
......
...@@ -144,6 +144,12 @@ internal class QQAndroidBot constructor( ...@@ -144,6 +144,12 @@ internal class QQAndroidBot constructor(
@Suppress("DuplicatedCode") @Suppress("DuplicatedCode")
@OptIn(LowLevelAPI::class) @OptIn(LowLevelAPI::class)
override suspend fun rejectMemberJoinRequest(event: MemberJoinRequestEvent, blackList: Boolean) { override suspend fun rejectMemberJoinRequest(event: MemberJoinRequestEvent, blackList: Boolean) {
rejectMemberJoinRequest(event, blackList, "")
}
@Suppress("DuplicatedCode")
@OptIn(LowLevelAPI::class)
override suspend fun rejectMemberJoinRequest(event: MemberJoinRequestEvent, blackList: Boolean, message: String) {
checkGroupPermission(event.bot, event.group) { event::class.simpleName ?: "<anonymous class>" } checkGroupPermission(event.bot, event.group) { event::class.simpleName ?: "<anonymous class>" }
check(event.responded.compareAndSet(false, true)) { check(event.responded.compareAndSet(false, true)) {
"the request $this has already been responded" "the request $this has already been responded"
...@@ -159,7 +165,8 @@ internal class QQAndroidBot constructor( ...@@ -159,7 +165,8 @@ internal class QQAndroidBot constructor(
fromNick = event.fromNick, fromNick = event.fromNick,
groupId = event.groupId, groupId = event.groupId,
accept = false, accept = false,
blackList = blackList blackList = blackList,
message = message
) )
} }
...@@ -578,13 +585,15 @@ internal abstract class QQAndroidBotBase constructor( ...@@ -578,13 +585,15 @@ internal abstract class QQAndroidBotBase constructor(
@LowLevelAPI @LowLevelAPI
@MiraiExperimentalAPI @MiraiExperimentalAPI
override suspend fun _lowLevelGetGroupActiveData(groupId: Long): GroupActiveData { override suspend fun _lowLevelGetGroupActiveData(groupId: Long, page: Int): GroupActiveData {
val data = network.async { val data = network.async {
HttpClient().get<String> { HttpClient().get<String> {
url("https://qqweb.qq.com/c/activedata/get_mygroup_data") url("https://qqweb.qq.com/c/activedata/get_mygroup_data")
parameter("bkn", bkn) parameter("bkn", bkn)
parameter("gc", groupId) parameter("gc", groupId)
if (page != -1) {
parameter("page", page)
}
headers { headers {
append( append(
"cookie", "cookie",
...@@ -755,7 +764,8 @@ internal abstract class QQAndroidBotBase constructor( ...@@ -755,7 +764,8 @@ internal abstract class QQAndroidBotBase constructor(
fromNick: String, fromNick: String,
groupId: Long, groupId: Long,
accept: Boolean?, accept: Boolean?,
blackList: Boolean blackList: Boolean,
message: String
) { ) {
network.apply { network.apply {
NewContact.SystemMsgNewGroup.Action( NewContact.SystemMsgNewGroup.Action(
...@@ -765,7 +775,8 @@ internal abstract class QQAndroidBotBase constructor( ...@@ -765,7 +775,8 @@ internal abstract class QQAndroidBotBase constructor(
groupId = groupId, groupId = groupId,
isInvited = false, isInvited = false,
accept = accept, accept = accept,
blackList = blackList blackList = blackList,
message = message
).sendWithoutExpect() ).sendWithoutExpect()
if (accept ?: return) if (accept ?: return)
groups[groupId].apply { groups[groupId].apply {
......
...@@ -114,7 +114,7 @@ internal class FriendImpl( ...@@ -114,7 +114,7 @@ internal class FriendImpl(
fileId = 0, fileId = 0,
fileMd5 = image.md5, fileMd5 = image.md5,
fileSize = image.input.size.toInt(), fileSize = image.input.size.toInt(),
fileName = image.md5.toUHexString("") + "." + ExternalImage.defaultFormatName, fileName = image.md5.toUHexString("") + "." + image.formatName,
imgOriginal = 1 imgOriginal = 1
) )
).sendAndExpect<LongConn.OffPicUp.Response>() ).sendAndExpect<LongConn.OffPicUp.Response>()
......
...@@ -237,7 +237,7 @@ internal class MemberImpl constructor( ...@@ -237,7 +237,7 @@ internal class MemberImpl constructor(
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
group.members.delegate.removeIf { it.id == this@MemberImpl.id } group.members.delegate.removeIf { it.id == this@MemberImpl.id }
this.cancel(CancellationException("Kicked by bot")) this@MemberImpl.cancel(CancellationException("Kicked by bot"))
MemberLeaveEvent.Kick(this@MemberImpl, null).broadcast() MemberLeaveEvent.Kick(this@MemberImpl, null).broadcast()
} }
} }
......
...@@ -96,14 +96,17 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo ...@@ -96,14 +96,17 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
private fun startHeartbeatJobOrKill(cancelCause: CancellationException? = null): Job { private fun startHeartbeatJobOrKill(cancelCause: CancellationException? = null): Job {
heartbeatJob?.cancel(cancelCause) heartbeatJob?.cancel(cancelCause)
return this@QQAndroidBotNetworkHandler.launch(CoroutineName("Heartbeat")) { return this@QQAndroidBotNetworkHandler.launch(CoroutineName("Heartbeat")) heartBeatJob@{
while (this.isActive) { while (this.isActive) {
delay(bot.configuration.heartbeatPeriodMillis) delay(bot.configuration.heartbeatPeriodMillis)
val failException = doHeartBeat() val failException = doHeartBeat()
if (failException != null) { if (failException != null) {
delay(bot.configuration.firstReconnectDelayMillis) delay(bot.configuration.firstReconnectDelayMillis)
bot.launch { BotOfflineEvent.Dropped(bot, failException).broadcast() }
return@launch bot.launch {
BotOfflineEvent.Dropped(bot, failException).broadcast()
}
return@heartBeatJob
} }
} }
}.also { heartbeatJob = it } }.also { heartbeatJob = it }
......
...@@ -99,9 +99,13 @@ internal class StGroupRankInfo( ...@@ -99,9 +99,13 @@ internal class StGroupRankInfo(
@JceId(4) @JvmField val dwGroupRankSeq: Long? = null, @JceId(4) @JvmField val dwGroupRankSeq: Long? = null,
@JceId(5) @JvmField val ownerName: String? = "", @JceId(5) @JvmField val ownerName: String? = "",
@JceId(6) @JvmField val adminName: String? = "", @JceId(6) @JvmField val adminName: String? = "",
@JceId(7) @JvmField val dwOfficeMode: Long? = null @JceId(7) @JvmField val dwOfficeMode: Long? = null,
@JceId(9) @JvmField val fuckIssue405: List<FuckIssue405?>? = null // fake
) : JceStruct ) : JceStruct
@Serializable
internal class FuckIssue405
@Serializable @Serializable
internal class StFavoriteGroup( internal class StFavoriteGroup(
@JceId(0) @JvmField val dwGroupCode: Long, @JceId(0) @JvmField val dwGroupCode: Long,
......
...@@ -148,22 +148,22 @@ internal class NewContact { ...@@ -148,22 +148,22 @@ internal class NewContact {
return struct?.msg?.run { return struct?.msg?.run {
//this.soutv("SystemMsg") //this.soutv("SystemMsg")
when (subType) { when (subType) {
1 -> { //管理员邀 1 -> { // 处理被邀请入群 或 处理成员入群申
when (c2cInviteJoinGroupFlag) { when (groupMsgType) {
1 -> { 1 -> {
// 被邀请入群
BotInvitedJoinGroupRequestEvent(
bot, struct.msgSeq, actionUin,
groupCode, groupName, actionUinNick
)
}
0 -> {
// 成员申请入群 // 成员申请入群
MemberJoinRequestEvent( MemberJoinRequestEvent(
bot, struct.msgSeq, msgAdditional, bot, struct.msgSeq, msgAdditional,
struct.reqUin, groupCode, groupName, reqUinNick struct.reqUin, groupCode, groupName, reqUinNick
) )
} }
2 -> {
// 被邀请入群
BotInvitedJoinGroupRequestEvent(
bot, struct.msgSeq, actionUin,
groupCode, groupName, actionUinNick
)
}
else -> throw contextualBugReportException( else -> throw contextualBugReportException(
"parse SystemMsgNewGroup, subType=1", "parse SystemMsgNewGroup, subType=1",
this._miraiContentToString(), this._miraiContentToString(),
...@@ -171,16 +171,14 @@ internal class NewContact { ...@@ -171,16 +171,14 @@ internal class NewContact {
) )
} }
} }
2 -> { 2 -> { // 被邀请入群, 自动同意, 不需处理
// 被邀请入群, 自动同意
val group = bot.getNewGroup(groupCode) ?: return null val group = bot.getNewGroup(groupCode) ?: return null
val invitor = group[actionUin] val invitor = group[actionUin]
BotJoinGroupEvent.Invite(invitor) BotJoinGroupEvent.Invite(invitor)
} }
3 -> { 3 -> { // 已被请他管理员处理
// 已被请他管理员处理
null null
} }
5 -> { 5 -> {
...@@ -223,7 +221,8 @@ internal class NewContact { ...@@ -223,7 +221,8 @@ internal class NewContact {
groupId: Long, groupId: Long,
isInvited: Boolean, isInvited: Boolean,
accept: Boolean?, accept: Boolean?,
blackList: Boolean = false blackList: Boolean = false,
message: String = ""
) = ) =
buildOutgoingUniPacket(client) { buildOutgoingUniPacket(client) {
writeProtoBuf( writeProtoBuf(
...@@ -236,7 +235,7 @@ internal class NewContact { ...@@ -236,7 +235,7 @@ internal class NewContact {
false -> 12 // reject false -> 12 // reject
}, },
groupCode = groupId, groupCode = groupId,
msg = "", msg = message,
remark = "", remark = "",
blacklist = blackList blacklist = blackList
), ),
......
...@@ -44,6 +44,7 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket ...@@ -44,6 +44,7 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.GroupInfoImpl import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.GroupInfoImpl
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.NewContact import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.NewContact
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.utils._miraiContentToString
import net.mamoe.mirai.qqandroid.utils.io.serialization.readProtoBuf import net.mamoe.mirai.qqandroid.utils.io.serialization.readProtoBuf
import net.mamoe.mirai.qqandroid.utils.io.serialization.toByteArray import net.mamoe.mirai.qqandroid.utils.io.serialization.toByteArray
import net.mamoe.mirai.qqandroid.utils.io.serialization.writeProtoBuf import net.mamoe.mirai.qqandroid.utils.io.serialization.writeProtoBuf
...@@ -190,6 +191,20 @@ internal object MessageSvcPbGetMsg : OutgoingPacketFactory<MessageSvcPbGetMsg.Re ...@@ -190,6 +191,20 @@ internal object MessageSvcPbGetMsg : OutgoingPacketFactory<MessageSvcPbGetMsg.Re
34 -> { // 与 33 重复 34 -> { // 与 33 重复
return@mapNotNull null return@mapNotNull null
} }
85 -> bot.groupListModifyLock.withLock { // 其他客户端入群
val group = bot.getGroupByUinOrNull(msg.msgHead.fromUin)
if (msg.msgHead.toUin == bot.id && group == null) {
val newGroup = bot.getNewGroup(Group.calculateGroupCodeByGroupUin(msg.msgHead.fromUin))
?: return@mapNotNull null
bot.groups.delegate.addLast(newGroup)
return@mapNotNull BotJoinGroupEvent.Active(newGroup)
} else {
// unknown
return@mapNotNull null
}
}
/* /*
34 -> { // 主动入群 34 -> { // 主动入群
......
...@@ -81,7 +81,9 @@ internal object OnlinePushReqPush : IncomingPacketFactory<OnlinePushReqPush.ReqP ...@@ -81,7 +81,9 @@ internal object OnlinePushReqPush : IncomingPacketFactory<OnlinePushReqPush.ReqP
val packets: Sequence<Packet> = reqPushMsg.vMsgInfos.deco(bot.client) { msgInfo -> val packets: Sequence<Packet> = reqPushMsg.vMsgInfos.deco(bot.client) { msgInfo ->
when (msgInfo.shMsgType.toInt()) { when (msgInfo.shMsgType.toInt()) {
732 -> { 732 -> {
val group = bot.getGroup(readUInt().toLong()) val group = bot.getGroupOrNull(readUInt().toLong())
?: return@deco emptySequence() // group has not been initialized
GroupImpl.checkIsInstance(group) GroupImpl.checkIsInstance(group)
val internalType = readByte().toInt() val internalType = readByte().toInt()
...@@ -257,6 +259,7 @@ private object Transformers732 : Map<Int, Lambda732> by mapOf( ...@@ -257,6 +259,7 @@ private object Transformers732 : Map<Int, Lambda732> by mapOf(
} }
} }
@Suppress("DEPRECATION")
if (group.settings.isConfessTalkEnabled == new) { if (group.settings.isConfessTalkEnabled == new) {
return@lambda732 emptySequence() return@lambda732 emptySequence()
} }
......
...@@ -182,7 +182,7 @@ internal class StatSvc { ...@@ -182,7 +182,7 @@ internal class StatSvc {
} }
internal object ReqMSFOffline : internal object ReqMSFOffline :
IncomingPacketFactory<BotOfflineEvent.Dropped>("StatSvc.ReqMSFOffline", "StatSvc.RspMSFForceOffline") { IncomingPacketFactory<BotOfflineEvent.MsfOffline>("StatSvc.ReqMSFOffline", "StatSvc.RspMSFForceOffline") {
internal data class MsfOfflineToken( internal data class MsfOfflineToken(
val uin: Long, val uin: Long,
...@@ -190,13 +190,13 @@ internal class StatSvc { ...@@ -190,13 +190,13 @@ internal class StatSvc {
val const: Byte val const: Byte
) : Packet, RuntimeException("dropped by StatSvc.ReqMSFOffline") ) : Packet, RuntimeException("dropped by StatSvc.ReqMSFOffline")
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): BotOfflineEvent.Dropped { override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): BotOfflineEvent.MsfOffline {
val decodeUniPacket = readUniPacket(RequestMSFForceOffline.serializer()) val decodeUniPacket = readUniPacket(RequestMSFForceOffline.serializer())
@Suppress("INVISIBLE_MEMBER") @Suppress("INVISIBLE_MEMBER")
return BotOfflineEvent.Dropped(bot, MsfOfflineToken(decodeUniPacket.uin, decodeUniPacket.iSeqno, 0)) return BotOfflineEvent.MsfOffline(bot, MsfOfflineToken(decodeUniPacket.uin, decodeUniPacket.iSeqno, 0))
} }
override suspend fun QQAndroidBot.handle(packet: BotOfflineEvent.Dropped, sequenceId: Int): OutgoingPacket? { override suspend fun QQAndroidBot.handle(packet: BotOfflineEvent.MsfOffline, sequenceId: Int): OutgoingPacket? {
val cause = packet.cause val cause = packet.cause
check(cause is MsfOfflineToken) { "internal error: handling $packet in StatSvc.ReqMSFOffline" } check(cause is MsfOfflineToken) { "internal error: handling $packet in StatSvc.ReqMSFOffline" }
return buildResponseUniPacket(client) { return buildResponseUniPacket(client) {
......
...@@ -34,7 +34,9 @@ kotlin { ...@@ -34,7 +34,9 @@ kotlin {
) )
} }
jvm() jvm() {
// withJava() // https://youtrack.jetbrains.com/issue/KT-39991
}
sourceSets { sourceSets {
all { all {
......
...@@ -291,11 +291,13 @@ abstract class Bot internal constructor( ...@@ -291,11 +291,13 @@ abstract class Bot internal constructor(
@Deprecated( @Deprecated(
"use member function.", "use member function.",
replaceWith = ReplaceWith("event.reject(blackList)"), replaceWith = ReplaceWith("event.reject(blackList)"),
level = DeprecationLevel.ERROR level = DeprecationLevel.HIDDEN
) )
@JvmSynthetic
abstract suspend fun rejectMemberJoinRequest(event: MemberJoinRequestEvent, blackList: Boolean = false) abstract suspend fun rejectMemberJoinRequest(event: MemberJoinRequestEvent, blackList: Boolean = false)
@JvmSynthetic
abstract suspend fun rejectMemberJoinRequest(event: MemberJoinRequestEvent, blackList: Boolean = false, message: String = "")
/** /**
* 忽略加群验证(需管理员权限) * 忽略加群验证(需管理员权限)
* *
......
...@@ -26,109 +26,109 @@ data class GroupActiveData( ...@@ -26,109 +26,109 @@ data class GroupActiveData(
val info: GInfo? = null, val info: GInfo? = null,
@SerialName("role") @SerialName("role")
val role: Int? val role: Int? = 0
) { ) {
@Serializable @Serializable
data class GInfo( data class GInfo(
@SerialName("g_act_num") @SerialName("g_act_num")
val actNum: List<GActNum?>?, //发言人数列表 val actNum: List<GActNum?>? = null, //发言人数列表
@SerialName("g_createtime") @SerialName("g_createtime")
val createTime: Int?, val createTime: Int? = 0,
@SerialName("g_exit_num") @SerialName("g_exit_num")
val exitNum: List<GExitNum?>?, //退群人数列表 val exitNum: List<GExitNum?>? = null, //退群人数列表
@SerialName("g_join_num") @SerialName("g_join_num")
val joinNum: List<GJoinNum?>?, val joinNum: List<GJoinNum?>? = null,
@SerialName("g_mem_num") @SerialName("g_mem_num")
val memNum: List<GMemNum?>?, //人数变化 val memNum: List<GMemNum?>? = null, //人数变化
@SerialName("g_most_act") @SerialName("g_most_act")
val mostAct: List<GMostAct?>?, //发言排行 val mostAct: List<GMostAct?>? = null, //发言排行
@SerialName("g_sentences") @SerialName("g_sentences")
val sentences: List<GSentence?>?, val sentences: List<GSentence?>? = null,
@SerialName("gc") @SerialName("gc")
val gc: Int?, val gc: Int? = null,
@SerialName("gn") @SerialName("gn")
val gn: String?, val gn: String? = null,
@SerialName("gowner") @SerialName("gowner")
val gowner: String?, val gowner: String? = null,
@SerialName("isEnd") @SerialName("isEnd")
val isEnd: Int? val isEnd: Int? = null
) { ) {
@Serializable @Serializable
data class GActNum( data class GActNum(
@SerialName("date") @SerialName("date")
val date: String?, val date: String? = null,
@SerialName("num") @SerialName("num")
val num: Int? val num: Int? = 0
) )
@Serializable @Serializable
data class GExitNum( data class GExitNum(
@SerialName("date") @SerialName("date")
val date: String?, val date: String? = null,
@SerialName("num") @SerialName("num")
val num: Int? val num: Int? = 0
) )
@Serializable @Serializable
data class GJoinNum( data class GJoinNum(
@SerialName("date") @SerialName("date")
val date: String?, val date: String? = null,
@SerialName("num") @SerialName("num")
val num: Int? val num: Int? = 0
) )
@Serializable @Serializable
data class GMemNum( data class GMemNum(
@SerialName("date") @SerialName("date")
val date: String?, val date: String? = null,
@SerialName("num") @SerialName("num")
val num: Int? val num: Int? = 0
) )
@Serializable @Serializable
data class GMostAct( data class GMostAct(
@SerialName("name") @SerialName("name")
val name: String?, // 名称 不完整 val name: String? = null, // 名称 不完整
@SerialName("sentences_num") @SerialName("sentences_num")
val sentencesNum: Int?, // 发言数 val sentencesNum: Int? = 0, // 发言数
@SerialName("sta") @SerialName("sta")
val sta: Int?, val sta: Int? = 0,
@SerialName("uin") @SerialName("uin")
val uin: Long? val uin: Long? = 0
) )
@Serializable @Serializable
data class GSentence( data class GSentence(
@SerialName("date") @SerialName("date")
val date: String?, val date: String? = null,
@SerialName("num") @SerialName("num")
val num: Int? val num: Int? = 0
) )
} }
} }
\ No newline at end of file
...@@ -16,6 +16,9 @@ package net.mamoe.mirai.event.events ...@@ -16,6 +16,9 @@ package net.mamoe.mirai.event.events
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.event.AbstractEvent import net.mamoe.mirai.event.AbstractEvent
import net.mamoe.mirai.qqandroid.network.Packet import net.mamoe.mirai.qqandroid.network.Packet
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.SinceMirai
import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
...@@ -35,7 +38,8 @@ sealed class BotOfflineEvent : BotEvent, AbstractEvent() { ...@@ -35,7 +38,8 @@ sealed class BotOfflineEvent : BotEvent, AbstractEvent() {
/** /**
* 主动离线. 主动广播这个事件也可以让 [Bot] 关闭. * 主动离线. 主动广播这个事件也可以让 [Bot] 关闭.
*/ */
data class Active(override val bot: Bot, val cause: Throwable?) : BotOfflineEvent(), BotActiveEvent data class Active(override val bot: Bot, override val cause: Throwable?) : BotOfflineEvent(), BotActiveEvent,
CauseAware
/** /**
* 被挤下线 * 被挤下线
...@@ -45,15 +49,31 @@ sealed class BotOfflineEvent : BotEvent, AbstractEvent() { ...@@ -45,15 +49,31 @@ sealed class BotOfflineEvent : BotEvent, AbstractEvent() {
BotPassiveEvent BotPassiveEvent
/** /**
* 被服务器断开或因网络问题而掉线 * 被服务器断开
*/ */
data class Dropped internal constructor(override val bot: Bot, val cause: Throwable?) : BotOfflineEvent(), Packet, @SinceMirai("1.1.0")
BotPassiveEvent @MiraiInternalAPI("This is very experimental and might be changed")
data class MsfOffline internal constructor(override val bot: Bot, override val cause: Throwable?) :
BotOfflineEvent(), Packet,
BotPassiveEvent, CauseAware
/**
* 因网络问题而掉线
*/
data class Dropped internal constructor(override val bot: Bot, override val cause: Throwable?) : BotOfflineEvent(),
Packet,
BotPassiveEvent, CauseAware
/** /**
* 服务器主动要求更换另一个服务器 * 服务器主动要求更换另一个服务器
*/ */
@MiraiInternalAPI
data class RequireReconnect internal constructor(override val bot: Bot) : BotOfflineEvent(), Packet, BotPassiveEvent data class RequireReconnect internal constructor(override val bot: Bot) : BotOfflineEvent(), Packet, BotPassiveEvent
@MiraiExperimentalAPI
interface CauseAware {
val cause: Throwable?
}
} }
/** /**
......
...@@ -355,7 +355,8 @@ data class MemberJoinRequestEvent internal constructor( ...@@ -355,7 +355,8 @@ data class MemberJoinRequestEvent internal constructor(
suspend fun accept() = bot.acceptMemberJoinRequest(this) suspend fun accept() = bot.acceptMemberJoinRequest(this)
@JvmSynthetic @JvmSynthetic
suspend fun reject(blackList: Boolean = false) = bot.rejectMemberJoinRequest(this, blackList) @JvmOverloads
suspend fun reject(blackList: Boolean = false, message: String = "") = bot.rejectMemberJoinRequest(this, blackList, message)
@JvmSynthetic @JvmSynthetic
suspend fun ignore(blackList: Boolean = false) = bot.ignoreMemberJoinRequest(this, blackList) suspend fun ignore(blackList: Boolean = false) = bot.ignoreMemberJoinRequest(this, blackList)
...@@ -368,8 +369,8 @@ data class MemberJoinRequestEvent internal constructor( ...@@ -368,8 +369,8 @@ data class MemberJoinRequestEvent internal constructor(
@JavaFriendlyAPI @JavaFriendlyAPI
@JvmOverloads @JvmOverloads
@JvmName("reject") @JvmName("reject")
fun __rejectBlockingForJava__(blackList: Boolean = false) = fun __rejectBlockingForJava__(blackList: Boolean = false, message: String = "") =
runBlocking { bot.rejectMemberJoinRequest(this@MemberJoinRequestEvent, blackList) } runBlocking { bot.rejectMemberJoinRequest(this@MemberJoinRequestEvent, blackList, message) }
@JavaFriendlyAPI @JavaFriendlyAPI
@JvmOverloads @JvmOverloads
......
...@@ -112,10 +112,12 @@ interface LowLevelBotAPIAccessor { ...@@ -112,10 +112,12 @@ interface LowLevelBotAPIAccessor {
/** /**
* 获取群活跃信息 * 获取群活跃信息
* 不传page可得到趋势图
* page从0开始传入可以得到发言列表
*/ */
@LowLevelAPI @LowLevelAPI
@MiraiExperimentalAPI @MiraiExperimentalAPI
suspend fun _lowLevelGetGroupActiveData(groupId: Long): GroupActiveData suspend fun _lowLevelGetGroupActiveData(groupId: Long, page: Int = -1): GroupActiveData
/** /**
...@@ -123,19 +125,38 @@ interface LowLevelBotAPIAccessor { ...@@ -123,19 +125,38 @@ interface LowLevelBotAPIAccessor {
*/ */
@LowLevelAPI @LowLevelAPI
@MiraiExperimentalAPI @MiraiExperimentalAPI
suspend fun _lowLevelSolveNewFriendRequestEvent(eventId: Long, fromId: Long, fromNick: String, accept: Boolean, blackList: Boolean) suspend fun _lowLevelSolveNewFriendRequestEvent(
eventId: Long,
fromId: Long,
fromNick: String,
accept: Boolean,
blackList: Boolean
)
/** /**
* 处理被邀请加入一个群请求事件 * 处理被邀请加入一个群请求事件
*/ */
@LowLevelAPI @LowLevelAPI
@MiraiExperimentalAPI @MiraiExperimentalAPI
suspend fun _lowLevelSolveBotInvitedJoinGroupRequestEvent(eventId: Long, invitorId: Long, groupId: Long, accept: Boolean) suspend fun _lowLevelSolveBotInvitedJoinGroupRequestEvent(
eventId: Long,
invitorId: Long,
groupId: Long,
accept: Boolean
)
/** /**
* 处理账号请求加入群事件 * 处理账号请求加入群事件
*/ */
@LowLevelAPI @LowLevelAPI
@MiraiExperimentalAPI @MiraiExperimentalAPI
suspend fun _lowLevelSolveMemberJoinRequestEvent(eventId: Long, fromId: Long, fromNick: String, groupId: Long, accept: Boolean?, blackList: Boolean) suspend fun _lowLevelSolveMemberJoinRequestEvent(
eventId: Long,
fromId: Long,
fromNick: String,
groupId: Long,
accept: Boolean?,
blackList: Boolean,
message: String = ""
)
} }
...@@ -73,7 +73,7 @@ expect interface Image : Message, MessageContent, CodableMessage { ...@@ -73,7 +73,7 @@ expect interface Image : Message, MessageContent, CodableMessage {
* *
* ### 格式 * ### 格式
* 群图片: * 群图片:
* - [GROUP_IMAGE_ID_REGEX], 示例: `{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.mirai` (后缀一定为 `".mirai"`) * - [GROUP_IMAGE_ID_REGEX], 示例: `{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.ext` (ext系扩展名)
* *
* 好友图片: * 好友图片:
* - [FRIEND_IMAGE_ID_REGEX_1], 示例: `/f8f1ab55-bf8e-4236-b55e-955848d7069f` * - [FRIEND_IMAGE_ID_REGEX_1], 示例: `/f8f1ab55-bf8e-4236-b55e-955848d7069f`
...@@ -125,7 +125,7 @@ abstract class FriendImage internal constructor() : AbstractImage() { // change ...@@ -125,7 +125,7 @@ abstract class FriendImage internal constructor() : AbstractImage() { // change
/** /**
* 群图片. * 群图片.
* *
* @property imageId 形如 `{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.mirai` (后缀一定为 `".mirai"`) * @property imageId 形如 `{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.ext` (ext系扩展名)
* @see Image 查看更多说明 * @see Image 查看更多说明
*/ */
// CustomFace // CustomFace
...@@ -156,11 +156,11 @@ val FRIEND_IMAGE_ID_REGEX_2 = Regex("""/[0-9]*-[0-9]*-[0-9a-fA-F]{32}""") ...@@ -156,11 +156,11 @@ val FRIEND_IMAGE_ID_REGEX_2 = Regex("""/[0-9]*-[0-9]*-[0-9a-fA-F]{32}""")
/** /**
* 群图片 ID 正则表达式 * 群图片 ID 正则表达式
* *
* `{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.mirai` * `{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.ext`
*/ */
@Suppress("RegExpRedundantEscape") // This is required on Android @Suppress("RegExpRedundantEscape") // This is required on Android
// Java: MessageUtils.GROUP_IMAGE_ID_REGEX // Java: MessageUtils.GROUP_IMAGE_ID_REGEX
val GROUP_IMAGE_ID_REGEX = Regex("""\{[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}\}\.mirai""") val GROUP_IMAGE_ID_REGEX = Regex("""\{[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}\}\..{3,5}""")
/** /**
* 通过 [Image.imageId] 构造一个 [Image] 以便发送. * 通过 [Image.imageId] 构造一个 [Image] 以便发送.
......
...@@ -51,7 +51,7 @@ annotation class MiraiExperimentalAPI( ...@@ -51,7 +51,7 @@ annotation class MiraiExperimentalAPI(
annotation class SinceMirai(val version: String) annotation class SinceMirai(val version: String)
/** /**
* 标记一个正计划在 [version] 版本时删除的 API. * 标记一个正计划在 [version] 版本时删除 (对外隐藏) 的 API.
*/ */
@Target(CLASS, PROPERTY, FIELD, CONSTRUCTOR, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER, TYPEALIAS) @Target(CLASS, PROPERTY, FIELD, CONSTRUCTOR, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER, TYPEALIAS)
@Retention(AnnotationRetention.SOURCE) @Retention(AnnotationRetention.SOURCE)
......
...@@ -16,10 +16,10 @@ import kotlinx.serialization.UnstableDefault ...@@ -16,10 +16,10 @@ import kotlinx.serialization.UnstableDefault
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration import kotlinx.serialization.json.JsonConfiguration
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.coroutineContext import kotlin.coroutines.coroutineContext
import kotlin.jvm.JvmField
import kotlin.jvm.JvmStatic import kotlin.jvm.JvmStatic
import kotlin.jvm.JvmSynthetic import kotlin.jvm.JvmSynthetic
...@@ -52,6 +52,30 @@ expect open class BotConfiguration() : BotConfigurationBase { ...@@ -52,6 +52,30 @@ expect open class BotConfiguration() : BotConfigurationBase {
@ConfigurationDsl @ConfigurationDsl
fun randomDeviceInfo() fun randomDeviceInfo()
/**
* 协议类型, 服务器仅允许使用不同协议同时登录.
*/
enum class MiraiProtocol {
/**
* Android 手机.
*/
ANDROID_PHONE,
/**
* Android 平板.
*/
ANDROID_PAD,
/**
* Android 手表.
* */
@SinceMirai("1.1.0")
ANDROID_WATCH;
internal val id: Long
}
companion object { companion object {
/** 默认的配置实例. 可以进行修改 */ /** 默认的配置实例. 可以进行修改 */
@JvmStatic @JvmStatic
...@@ -61,10 +85,8 @@ expect open class BotConfiguration() : BotConfigurationBase { ...@@ -61,10 +85,8 @@ expect open class BotConfiguration() : BotConfigurationBase {
fun copy(): BotConfiguration fun copy(): BotConfiguration
} }
@MiraiInternalAPI
@Suppress("PropertyName")
@SinceMirai("1.1.0") @SinceMirai("1.1.0")
internal open class BotConfigurationBase internal constructor() { open class BotConfigurationBase internal constructor() {
/** /**
* 日志记录器 * 日志记录器
* *
...@@ -131,27 +153,6 @@ internal open class BotConfigurationBase internal constructor() { ...@@ -131,27 +153,6 @@ internal open class BotConfigurationBase internal constructor() {
Json(JsonConfiguration(isLenient = true, ignoreUnknownKeys = true)) Json(JsonConfiguration(isLenient = true, ignoreUnknownKeys = true))
}.getOrElse { Json(JsonConfiguration.Stable) } }.getOrElse { Json(JsonConfiguration.Stable) }
enum class MiraiProtocol(
/** 协议模块使用的 ID */
@JvmField internal val id: Long
) {
/**
* Android 手机.
*
* - 与手机冲突
* - 与平板和电脑不冲突
*/
ANDROID_PHONE(537062845),
/**
* Android 平板.
*
* - 与平板冲突
* - 与手机和电脑不冲突
*/
ANDROID_PAD(537062409)
}
/** /**
* 不显示网络日志. 不推荐. * 不显示网络日志. 不推荐.
* @see networkLoggerSupplier 更多日志处理方式 * @see networkLoggerSupplier 更多日志处理方式
......
...@@ -11,15 +11,18 @@ ...@@ -11,15 +11,18 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlinx.io.core.readBytes
import kotlinx.io.core.use
import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.User import net.mamoe.mirai.contact.User
import net.mamoe.mirai.message.MessageReceipt import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.sendTo
import net.mamoe.mirai.message.data.toUHexString
import net.mamoe.mirai.utils.internal.DeferredReusableInput import net.mamoe.mirai.utils.internal.DeferredReusableInput
import net.mamoe.mirai.utils.internal.ReusableInput import net.mamoe.mirai.utils.internal.ReusableInput
import kotlin.jvm.JvmField import kotlin.jvm.JvmField
import kotlin.jvm.JvmName
import kotlin.jvm.JvmSynthetic import kotlin.jvm.JvmSynthetic
/** /**
...@@ -35,6 +38,12 @@ class ExternalImage internal constructor( ...@@ -35,6 +38,12 @@ class ExternalImage internal constructor(
internal val input: ReusableInput internal val input: ReusableInput
) { ) {
internal val md5: ByteArray get() = this.input.md5 internal val md5: ByteArray get() = this.input.md5
val formatName: String by lazy {
val hex = input.asInput().use {
it.readBytes(8).toUHexString("")
}
return@lazy hex.detectFormatName()
}
init { init {
if (input !is DeferredReusableInput) { if (input !is DeferredReusableInput) {
...@@ -67,6 +76,14 @@ class ExternalImage internal constructor( ...@@ -67,6 +76,14 @@ class ExternalImage internal constructor(
} }
internal fun calculateImageResourceId(): String = generateImageId(md5) internal fun calculateImageResourceId(): String = generateImageId(md5)
private fun String.detectFormatName(): String = when {
startsWith("FFD8") -> "jpg"
startsWith("89504E47") -> "png"
startsWith("47494638") -> "gif"
startsWith("424D") -> "bmp"
else -> defaultFormatName
}
} }
/* /*
......
...@@ -262,7 +262,7 @@ open class SimpleLogger( ...@@ -262,7 +262,7 @@ open class SimpleLogger(
enum class LogPriority( enum class LogPriority(
@MiraiExperimentalAPI val nameAligned: String, @MiraiExperimentalAPI val nameAligned: String,
@MiraiExperimentalAPI val simpleName: String, val simpleName: String,
@MiraiExperimentalAPI val correspondingFunction: MiraiLogger.(message: String?, e: Throwable?) -> Unit @MiraiExperimentalAPI val correspondingFunction: MiraiLogger.(message: String?, e: Throwable?) -> Unit
) { ) {
VERBOSE("VERBOSE", "V", MiraiLogger::verbose), VERBOSE("VERBOSE", "V", MiraiLogger::verbose),
......
...@@ -64,26 +64,22 @@ internal class ChunkedInput( ...@@ -64,26 +64,22 @@ internal class ChunkedInput(
* *
* 若 [ByteReadPacket.remaining] 小于 [sizePerPacket], 将会返回唯一元素 [this] 的 [Sequence] * 若 [ByteReadPacket.remaining] 小于 [sizePerPacket], 将会返回唯一元素 [this] 的 [Sequence]
*/ */
internal fun ByteReadPacket.chunkedFlow(sizePerPacket: Int): Flow<ChunkedInput> { internal fun ByteReadPacket.chunkedFlow(sizePerPacket: Int, buffer: ByteArray): Flow<ChunkedInput> {
ByteArrayPool.checkBufferSize(sizePerPacket) ByteArrayPool.checkBufferSize(sizePerPacket)
if (this.remaining <= sizePerPacket.toLong()) { if (this.remaining <= sizePerPacket.toLong()) {
ByteArrayPool.useInstance { buffer -> return flowOf(
return flowOf( ChunkedInput(
ChunkedInput( buffer,
buffer, this.readAvailable(buffer, 0, sizePerPacket)
this.readAvailable(buffer, 0, sizePerPacket)
)
) )
} )
} }
return flow { return flow {
ByteArrayPool.useInstance { buffer -> val chunkedInput = ChunkedInput(buffer, 0)
val chunkedInput = ChunkedInput(buffer, 0) do {
do { chunkedInput.size = this@chunkedFlow.readAvailable(buffer, 0, sizePerPacket)
chunkedInput.size = this@chunkedFlow.readAvailable(buffer, 0, sizePerPacket) emit(chunkedInput)
emit(chunkedInput) } while (this@chunkedFlow.isNotEmpty)
} while (this@chunkedFlow.isNotEmpty)
}
} }
} }
...@@ -93,19 +89,17 @@ internal fun ByteReadPacket.chunkedFlow(sizePerPacket: Int): Flow<ChunkedInput> ...@@ -93,19 +89,17 @@ internal fun ByteReadPacket.chunkedFlow(sizePerPacket: Int): Flow<ChunkedInput>
* 对于一个 1000 长度的 [ByteReadChannel] 和参数 [sizePerPacket] = 300, 将会产生含四个元素的 [Sequence], * 对于一个 1000 长度的 [ByteReadChannel] 和参数 [sizePerPacket] = 300, 将会产生含四个元素的 [Sequence],
* 其长度分别为: 300, 300, 300, 100. * 其长度分别为: 300, 300, 300, 100.
*/ */
internal fun ByteReadChannel.chunkedFlow(sizePerPacket: Int): Flow<ChunkedInput> { internal fun ByteReadChannel.chunkedFlow(sizePerPacket: Int, buffer: ByteArray): Flow<ChunkedInput> {
ByteArrayPool.checkBufferSize(sizePerPacket) ByteArrayPool.checkBufferSize(sizePerPacket)
if (this.isClosedForRead) { if (this.isClosedForRead) {
return flowOf() return flowOf()
} }
return flow { return flow {
ByteArrayPool.useInstance { buffer -> val chunkedInput = ChunkedInput(buffer, 0)
val chunkedInput = ChunkedInput(buffer, 0) do {
do { chunkedInput.size = this@chunkedFlow.readAvailable(buffer, 0, sizePerPacket)
chunkedInput.size = this@chunkedFlow.readAvailable(buffer, 0, sizePerPacket) emit(chunkedInput)
emit(chunkedInput) } while (!this@chunkedFlow.isClosedForRead)
} while (!this@chunkedFlow.isClosedForRead)
}
} }
} }
...@@ -117,7 +111,7 @@ internal fun ByteReadChannel.chunkedFlow(sizePerPacket: Int): Flow<ChunkedInput> ...@@ -117,7 +111,7 @@ internal fun ByteReadChannel.chunkedFlow(sizePerPacket: Int): Flow<ChunkedInput>
* 其长度分别为: 300, 300, 300, 100. * 其长度分别为: 300, 300, 300, 100.
*/ */
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
internal fun Input.chunkedFlow(sizePerPacket: Int): Flow<ChunkedInput> { internal fun Input.chunkedFlow(sizePerPacket: Int, buffer: ByteArray): Flow<ChunkedInput> {
ByteArrayPool.checkBufferSize(sizePerPacket) ByteArrayPool.checkBufferSize(sizePerPacket)
if (this.endOfInput) { if (this.endOfInput) {
...@@ -125,12 +119,10 @@ internal fun Input.chunkedFlow(sizePerPacket: Int): Flow<ChunkedInput> { ...@@ -125,12 +119,10 @@ internal fun Input.chunkedFlow(sizePerPacket: Int): Flow<ChunkedInput> {
} }
return flow { return flow {
ByteArrayPool.useInstance { buffer -> val chunkedInput = ChunkedInput(buffer, 0)
val chunkedInput = ChunkedInput(buffer, 0) while (!this@chunkedFlow.endOfInput) {
while (!this@chunkedFlow.endOfInput) { chunkedInput.size = this@chunkedFlow.readAvailable(buffer, 0, sizePerPacket)
chunkedInput.size = this@chunkedFlow.readAvailable(buffer, 0, sizePerPacket) emit(chunkedInput)
emit(chunkedInput)
}
} }
} }
} }
...@@ -144,16 +136,14 @@ internal fun Input.chunkedFlow(sizePerPacket: Int): Flow<ChunkedInput> { ...@@ -144,16 +136,14 @@ internal fun Input.chunkedFlow(sizePerPacket: Int): Flow<ChunkedInput> {
* 若 [ByteReadPacket.remaining] 小于 [sizePerPacket], 将会返回唯一元素 [this] 的 [Sequence] * 若 [ByteReadPacket.remaining] 小于 [sizePerPacket], 将会返回唯一元素 [this] 的 [Sequence]
*/ */
@OptIn(ExperimentalCoroutinesApi::class, InternalSerializationApi::class) @OptIn(ExperimentalCoroutinesApi::class, InternalSerializationApi::class)
internal fun InputStream.chunkedFlow(sizePerPacket: Int): Flow<ChunkedInput> { internal fun InputStream.chunkedFlow(sizePerPacket: Int, buffer: ByteArray): Flow<ChunkedInput> {
ByteArrayPool.checkBufferSize(sizePerPacket) require(sizePerPacket <= buffer.size) { "sizePerPacket is too large. Maximum buffer size=buffer.size=${buffer.size}" }
return flow { return flow {
ByteArrayPool.useInstance { buffer -> val chunkedInput = ChunkedInput(buffer, 0)
val chunkedInput = ChunkedInput(buffer, 0) while (this@chunkedFlow.available() != 0) {
while (this@chunkedFlow.available() != 0) { chunkedInput.size = this@chunkedFlow.read(buffer, 0, sizePerPacket)
chunkedInput.size = this@chunkedFlow.read(buffer, 0, sizePerPacket) emit(chunkedInput)
emit(chunkedInput)
}
} }
} }
} }
\ No newline at end of file
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * https://github.com/mamoe/mirai/blob/master/LICENSE
*/ */
@file:JvmMultifileClass
@file:JvmName("Events") @file:JvmName("Events")
@file:Suppress("unused", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "NOTHING_TO_INLINE") @file:Suppress("unused", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "NOTHING_TO_INLINE")
...@@ -37,18 +38,26 @@ import kotlin.reflect.jvm.kotlinFunction ...@@ -37,18 +38,26 @@ import kotlin.reflect.jvm.kotlinFunction
* *
* 支持的函数类型: * 支持的函数类型:
* ``` * ```
* // 所有函数参数, 函数返回值都不允许标记为可空 (带有 '?' 符号)
* // T 表示任何 Event 类型.
* suspend fun T.onEvent(T) * suspend fun T.onEvent(T)
* suspend fun T.onEvent(T): ListeningStatus * suspend fun T.onEvent(T): ListeningStatus
* suspend fun T.onEvent(T): Nothing
* suspend fun onEvent(T) * suspend fun onEvent(T)
* suspend fun onEvent(T): ListeningStatus * suspend fun onEvent(T): ListeningStatus
* suspend fun onEvent(T): Nothing
* suspend fun T.onEvent() * suspend fun T.onEvent()
* suspend fun T.onEvent(): ListeningStatus * suspend fun T.onEvent(): ListeningStatus
* suspend fun T.onEvent(): Nothing
* fun T.onEvent(T) * fun T.onEvent(T)
* fun T.onEvent(T): ListeningStatus * fun T.onEvent(T): ListeningStatus
* fun T.onEvent(T): Nothing
* fun onEvent(T) * fun onEvent(T)
* fun onEvent(T): ListeningStatus * fun onEvent(T): ListeningStatus
* fun onEvent(T): Nothing
* fun T.onEvent() * fun T.onEvent()
* fun T.onEvent(): ListeningStatus * fun T.onEvent(): ListeningStatus
* fun T.onEvent(): Nothing
* ``` * ```
* *
* Kotlin 使用示例: * Kotlin 使用示例:
...@@ -57,6 +66,8 @@ import kotlin.reflect.jvm.kotlinFunction ...@@ -57,6 +66,8 @@ import kotlin.reflect.jvm.kotlinFunction
* object MyEvents : ListenerHost { * object MyEvents : ListenerHost {
* override val coroutineContext = SupervisorJob() * override val coroutineContext = SupervisorJob()
* *
*
* // 可以抛出任何异常, 将在 this.coroutineContext 或 registerEvents 时提供的 CoroutineScope.coroutineContext 中的 CoroutineExceptionHandler 处理.
* @EventHandler * @EventHandler
* suspend fun MessageEvent.onMessage() { * suspend fun MessageEvent.onMessage() {
* reply("received") * reply("received")
...@@ -76,8 +87,17 @@ import kotlin.reflect.jvm.kotlinFunction ...@@ -76,8 +87,17 @@ import kotlin.reflect.jvm.kotlinFunction
* } * }
* *
* @EventHandler * @EventHandler
* suspend fun MessageEvent.onMessage() { * suspend fun MessageEvent.onMessage() { // 可以抛出任何异常, 将在 handleException 处理
* reply("received") * reply("received")
* // 无返回值 (或者返回 Unit), 表示一直监听事件.
* }
*
* @EventHandler
* suspend fun MessageEvent.onMessage(): ListeningStatus { // 可以抛出任何异常, 将在 handleException 处理
* reply("received")
*
* return ListeningStatus.LISTENING // 表示继续监听事件
* // return ListeningStatus.STOPPED // 表示停止监听事件
* } * }
* } * }
* *
...@@ -90,8 +110,10 @@ import kotlin.reflect.jvm.kotlinFunction ...@@ -90,8 +110,10 @@ import kotlin.reflect.jvm.kotlinFunction
* *
* 支持的方法类型 * 支持的方法类型
* ``` * ```
* // T 表示任何 Event 类型.
* void onEvent(T) * void onEvent(T)
* ListeningStatus onEvent(T) * Void onEvent(T)
* @NotNull ListeningStatus onEvent(T) // 返回 null 时将抛出异常
* ``` * ```
* *
* *
...@@ -99,13 +121,23 @@ import kotlin.reflect.jvm.kotlinFunction ...@@ -99,13 +121,23 @@ import kotlin.reflect.jvm.kotlinFunction
* ``` * ```
* public class MyEventHandlers extends SimpleListenerHost { * public class MyEventHandlers extends SimpleListenerHost {
* @Override * @Override
* public void handleException(CoroutineContext context, Throwable exception){ * public void handleException(@NotNull CoroutineContext context, @NotNull Throwable exception){
* // 处理事件处理时抛出的异常 * // 处理事件处理时抛出的异常
* } * }
* *
* @EventHandler * @EventHandler
* public void onMessage(MessageEvent event) throws Exception { * public void onMessage(@NotNull MessageEvent event) throws Exception { // 可以抛出任何异常, 将在 handleException 处理
* event.subject.sendMessage("received") * event.subject.sendMessage("received");
* // 无返回值, 表示一直监听事件.
* }
*
* @NotNull
* @EventHandler
* public ListeningStatus onMessage(@NotNull MessageEvent event) throws Exception { // 可以抛出任何异常, 将在 handleException 处理
* event.subject.sendMessage("received");
*
* return ListeningStatus.LISTENING; // 表示继续监听事件
* // return ListeningStatus.STOPPED; // 表示停止监听事件
* } * }
* } * }
* *
...@@ -152,7 +184,7 @@ abstract class SimpleListenerHost ...@@ -152,7 +184,7 @@ abstract class SimpleListenerHost
@JvmOverloads constructor(coroutineContext: CoroutineContext = EmptyCoroutineContext) : ListenerHost, CoroutineScope { @JvmOverloads constructor(coroutineContext: CoroutineContext = EmptyCoroutineContext) : ListenerHost, CoroutineScope {
final override val coroutineContext: CoroutineContext = final override val coroutineContext: CoroutineContext =
SupervisorJob(coroutineContext[Job]) + CoroutineExceptionHandler(::handleException) + coroutineContext CoroutineExceptionHandler(::handleException) + coroutineContext + SupervisorJob(coroutineContext[Job])
/** /**
* 处理事件处理中未捕获的异常. 在构造器中的 [coroutineContext] 未提供 [CoroutineExceptionHandler] 情况下必须继承此函数. * 处理事件处理中未捕获的异常. 在构造器中的 [coroutineContext] 未提供 [CoroutineExceptionHandler] 情况下必须继承此函数.
...@@ -260,6 +292,12 @@ private fun Method.registerEvent( ...@@ -260,6 +292,12 @@ private fun Method.registerEvent(
return ListeningStatus.STOPPED return ListeningStatus.STOPPED
} }
} }
require(!kotlinFunction.returnType.isMarkedNullable) {
"Kotlin event handlers cannot have nullable return type."
}
require(kotlinFunction.parameters.any { it.type.isMarkedNullable }) {
"Kotlin event handlers cannot have nullable parameter type."
}
when (kotlinFunction.returnType.classifier) { when (kotlinFunction.returnType.classifier) {
Unit::class, Nothing::class -> { Unit::class, Nothing::class -> {
scope.subscribeAlways( scope.subscribeAlways(
...@@ -299,7 +337,7 @@ private fun Method.registerEvent( ...@@ -299,7 +337,7 @@ private fun Method.registerEvent(
"Illegal method parameter. Required one exact Event subclass. found $paramType" "Illegal method parameter. Required one exact Event subclass. found $paramType"
} }
when (this.returnType) { when (this.returnType) {
Void::class.java, Void.TYPE -> { Void::class.java, Void.TYPE, Nothing::class.java -> {
scope.subscribeAlways( scope.subscribeAlways(
paramType.kotlin as KClass<out Event>, paramType.kotlin as KClass<out Event>,
priority = annotation.priority, priority = annotation.priority,
......
...@@ -127,6 +127,28 @@ actual open class BotConfiguration : BotConfigurationBase() { // open for Java ...@@ -127,6 +127,28 @@ actual open class BotConfiguration : BotConfigurationBase() { // open for Java
botLoggerSupplier = { SingleFileLogger(identity(it), file) } botLoggerSupplier = { SingleFileLogger(identity(it), file) }
} }
@Suppress("ACTUAL_WITHOUT_EXPECT")
actual enum class MiraiProtocol actual constructor(
/** 协议模块使用的 ID */
@JvmField actual internal val id: Long
) {
/**
* Android 手机.
*/
ANDROID_PHONE(537062845),
/**
* Android 平板.
*/
ANDROID_PAD(537062409),
/**
* Android 手表.
* */
@SinceMirai("1.1.0")
ANDROID_WATCH(537061176)
}
actual companion object { actual companion object {
/** 默认的配置实例. 可以进行修改 */ /** 默认的配置实例. 可以进行修改 */
@JvmStatic @JvmStatic
......
...@@ -53,50 +53,67 @@ actual open class PlatformLogger @JvmOverloads constructor( ...@@ -53,50 +53,67 @@ actual open class PlatformLogger @JvmOverloads constructor(
) : MiraiLoggerPlatformBase() { ) : MiraiLoggerPlatformBase() {
actual constructor(identity: String?) : this(identity, ::println) actual constructor(identity: String?) : this(identity, ::println)
private fun out(message: String?, priority: String, color: Color) { /**
if (isColored) output("$color$currentTimeFormatted $priority/$identity: $message") * 输出一条日志. [message] 末尾可能不带换行符.
else output("$currentTimeFormatted $priority/$identity: $message") */
@SinceMirai("1.1.0")
protected open fun printLog(message: String?, priority: SimpleLogger.LogPriority) {
if (isColored) output("${priority.color}$currentTimeFormatted ${priority.simpleName}/$identity: $message")
else output("$currentTimeFormatted ${priority.simpleName}/$identity: $message")
} }
override fun verbose0(message: String?) = out(message, "V", Color.RESET) /**
* 获取指定 [SimpleLogger.LogPriority] 的颜色
*/
@SinceMirai("1.1.0")
protected open val SimpleLogger.LogPriority.color: Color
get() = when (this) {
SimpleLogger.LogPriority.VERBOSE -> Color.RESET
SimpleLogger.LogPriority.INFO -> Color.LIGHT_GREEN
SimpleLogger.LogPriority.WARNING -> Color.LIGHT_RED
SimpleLogger.LogPriority.ERROR -> Color.RED
SimpleLogger.LogPriority.DEBUG -> Color.LIGHT_CYAN
}
override fun verbose0(message: String?) = printLog(message, SimpleLogger.LogPriority.VERBOSE)
override fun verbose0(message: String?, e: Throwable?) { override fun verbose0(message: String?, e: Throwable?) {
if (e != null) verbose((message ?: e.toString()) + "\n${e.stackTraceString}") if (e != null) verbose((message ?: e.toString()) + "\n${e.stackTraceString}")
else verbose(message.toString()) else verbose(message.toString())
} }
override fun info0(message: String?) = out(message, "I", Color.LIGHT_GREEN) override fun info0(message: String?) = printLog(message, SimpleLogger.LogPriority.INFO)
override fun info0(message: String?, e: Throwable?) { override fun info0(message: String?, e: Throwable?) {
if (e != null) info((message ?: e.toString()) + "\n${e.stackTraceString}") if (e != null) info((message ?: e.toString()) + "\n${e.stackTraceString}")
else info(message.toString()) else info(message.toString())
} }
override fun warning0(message: String?) = out(message, "W", Color.LIGHT_RED) override fun warning0(message: String?) = printLog(message, SimpleLogger.LogPriority.WARNING)
override fun warning0(message: String?, e: Throwable?) { override fun warning0(message: String?, e: Throwable?) {
if (e != null) warning((message ?: e.toString()) + "\n${e.stackTraceString}") if (e != null) warning((message ?: e.toString()) + "\n${e.stackTraceString}")
else warning(message.toString()) else warning(message.toString())
} }
override fun error0(message: String?) = out(message, "E", Color.RED) override fun error0(message: String?) = printLog(message, SimpleLogger.LogPriority.ERROR)
override fun error0(message: String?, e: Throwable?) { override fun error0(message: String?, e: Throwable?) {
if (e != null) error((message ?: e.toString()) + "\n${e.stackTraceString}") if (e != null) error((message ?: e.toString()) + "\n${e.stackTraceString}")
else error(message.toString()) else error(message.toString())
} }
override fun debug0(message: String?) = out(message, "D", Color.LIGHT_CYAN) override fun debug0(message: String?) = printLog(message, SimpleLogger.LogPriority.DEBUG)
override fun debug0(message: String?, e: Throwable?) { override fun debug0(message: String?, e: Throwable?) {
if (e != null) debug((message ?: e.toString()) + "\n${e.stackTraceString}") if (e != null) debug((message ?: e.toString()) + "\n${e.stackTraceString}")
else debug(message.toString()) else debug(message.toString())
} }
private val timeFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.SIMPLIFIED_CHINESE) @SinceMirai("1.1.0")
protected open val timeFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.SIMPLIFIED_CHINESE)
private val currentTimeFormatted get() = timeFormat.format(Date()) private val currentTimeFormatted get() = timeFormat.format(Date())
/** @MiraiExperimentalAPI("This is subject to change.")
* @author NaturalHG @SinceMirai("1.1.0")
*/ protected enum class Color(private val format: String) {
@Suppress("unused")
private enum class Color(private val format: String) {
RESET("\u001b[0m"), RESET("\u001b[0m"),
WHITE("\u001b[30m"), WHITE("\u001b[30m"),
......
...@@ -11,6 +11,8 @@ import net.mamoe.mirai.message.data.toLongUnsigned ...@@ -11,6 +11,8 @@ import net.mamoe.mirai.message.data.toLongUnsigned
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
internal const val DEFAULT_REUSABLE_INPUT_BUFFER_SIZE = 8192
internal actual fun ByteArray.asReusableInput(): ReusableInput { internal actual fun ByteArray.asReusableInput(): ReusableInput {
return object : ReusableInput { return object : ReusableInput {
override val md5: ByteArray = md5() override val md5: ByteArray = md5()
...@@ -18,9 +20,11 @@ internal actual fun ByteArray.asReusableInput(): ReusableInput { ...@@ -18,9 +20,11 @@ internal actual fun ByteArray.asReusableInput(): ReusableInput {
override fun chunkedFlow(sizePerPacket: Int): ChunkedFlowSession<ChunkedInput> { override fun chunkedFlow(sizePerPacket: Int): ChunkedFlowSession<ChunkedInput> {
return object : ChunkedFlowSession<ChunkedInput> { return object : ChunkedFlowSession<ChunkedInput> {
override val flow: Flow<ChunkedInput> = inputStream().chunkedFlow(sizePerPacket) private val stream = inputStream()
override val flow: Flow<ChunkedInput> = stream.chunkedFlow(sizePerPacket, ByteArray(DEFAULT_REUSABLE_INPUT_BUFFER_SIZE.coerceAtLeast(sizePerPacket)))
override fun close() { override fun close() {
stream.close()
// nothing to do // nothing to do
} }
} }
...@@ -46,7 +50,7 @@ internal fun File.asReusableInput(deleteOnClose: Boolean): ReusableInput { ...@@ -46,7 +50,7 @@ internal fun File.asReusableInput(deleteOnClose: Boolean): ReusableInput {
override fun chunkedFlow(sizePerPacket: Int): ChunkedFlowSession<ChunkedInput> { override fun chunkedFlow(sizePerPacket: Int): ChunkedFlowSession<ChunkedInput> {
val stream = inputStream() val stream = inputStream()
return object : ChunkedFlowSession<ChunkedInput> { return object : ChunkedFlowSession<ChunkedInput> {
override val flow: Flow<ChunkedInput> = stream.chunkedFlow(sizePerPacket) override val flow: Flow<ChunkedInput> = stream.chunkedFlow(sizePerPacket, ByteArray(DEFAULT_REUSABLE_INPUT_BUFFER_SIZE.coerceAtLeast(sizePerPacket)))
override fun close() { override fun close() {
stream.close() stream.close()
if (deleteOnClose) this@asReusableInput.delete() if (deleteOnClose) this@asReusableInput.delete()
...@@ -72,7 +76,7 @@ internal fun File.asReusableInput(deleteOnClose: Boolean, md5: ByteArray): Reusa ...@@ -72,7 +76,7 @@ internal fun File.asReusableInput(deleteOnClose: Boolean, md5: ByteArray): Reusa
override fun chunkedFlow(sizePerPacket: Int): ChunkedFlowSession<ChunkedInput> { override fun chunkedFlow(sizePerPacket: Int): ChunkedFlowSession<ChunkedInput> {
val stream = inputStream() val stream = inputStream()
return object : ChunkedFlowSession<ChunkedInput> { return object : ChunkedFlowSession<ChunkedInput> {
override val flow: Flow<ChunkedInput> = stream.chunkedFlow(sizePerPacket) override val flow: Flow<ChunkedInput> = stream.chunkedFlow(sizePerPacket, ByteArray(DEFAULT_REUSABLE_INPUT_BUFFER_SIZE.coerceAtLeast(sizePerPacket)))
override fun close() { override fun close() {
stream.close() stream.close()
if (deleteOnClose) this@asReusableInput.delete() if (deleteOnClose) this@asReusableInput.delete()
......
/*
* 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;
import org.junit.Test;
import java.util.concurrent.atomic.AtomicInteger;
import static kotlin.test.AssertionsKt.assertEquals;
public class JvmMethodEventsTestJava extends SimpleListenerHost {
private final AtomicInteger called = new AtomicInteger(0);
@EventHandler
public void ev(TestEvent event) {
called.incrementAndGet();
}
@EventHandler
public Void ev2(TestEvent event) {
called.incrementAndGet();
return null;
}
@EventHandler
public ListeningStatus ev3(TestEvent event) {
called.incrementAndGet();
return ListeningStatus.LISTENING;
}
@EventHandler
public void ev(TestEvent event, TestEvent event2) {
called.incrementAndGet();
}
@EventHandler
public Void ev2(TestEvent event, TestEvent event2) {
called.incrementAndGet();
return null;
}
@EventHandler
public ListeningStatus ev3(TestEvent event, TestEvent event2) {
called.incrementAndGet();
return ListeningStatus.LISTENING;
}
@Test
public void test() {
Events.registerEvents(this);
EventKt.broadcast(new TestEvent());
assertEquals(6, called.get(), null);
}
}
\ No newline at end of file
...@@ -11,12 +11,12 @@ ...@@ -11,12 +11,12 @@
package net.mamoe.mirai.event package net.mamoe.mirai.event
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import net.mamoe.mirai.utils.internal.runBlocking import net.mamoe.mirai.utils.internal.runBlocking
import org.junit.Test import org.junit.Test
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
import kotlin.test.assertEquals
internal class JvmMethodEventsTest { internal class JvmMethodEventsTest {
...@@ -46,6 +46,13 @@ internal class JvmMethodEventsTest { ...@@ -46,6 +46,13 @@ internal class JvmMethodEventsTest {
called.getAndIncrement() called.getAndIncrement()
} }
@Suppress("unused")
@EventHandler
suspend fun `suspend param Void`(event: TestEvent): Void? {
called.getAndIncrement()
return null
}
@EventHandler @EventHandler
@Suppress("unused") @Suppress("unused")
fun TestEvent.`receiver param Unit`(event: TestEvent) { fun TestEvent.`receiver param Unit`(event: TestEvent) {
...@@ -81,15 +88,15 @@ internal class JvmMethodEventsTest { ...@@ -81,15 +88,15 @@ internal class JvmMethodEventsTest {
} }
} }
TestClass().run { // TestClass().run {
this.registerEvents() // this.registerEvents()
//
runBlocking { // runBlocking {
TestEvent().broadcast() // TestEvent().broadcast()
} // }
//
assertEquals(8, this.getCalled()) // assertEquals(9, this.getCalled())
} // }
} }
@Test @Test
...@@ -114,14 +121,14 @@ internal class JvmMethodEventsTest { ...@@ -114,14 +121,14 @@ internal class JvmMethodEventsTest {
} }
} }
TestClass().run { // TestClass().run {
this.registerEvents() // this.registerEvents()
//
runBlocking { // runBlocking {
TestEvent().broadcast() // TestEvent().broadcast()
} // }
//
assertEquals(1, this.getCalled()) // assertEquals(1, this.getCalled())
} // }
} }
} }
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