Merge branch 'dev'

This commit is contained in:
Him188 2020-09-16 12:36:04 +08:00
commit 9eed2b94ae
38 changed files with 1264 additions and 238 deletions

View File

@ -1,5 +1,35 @@
# Version 1.x
## `1.3.0` 2020/9/16
### 新特性
- 支持群恢复相关事件: `MemberJoinEvent.Retrieve`, `BotJoinGroupEvent.Retrieve` (#531 by @Karlatemp)
- 群荣耀获取 (`Bot._lowLevelGetGroupHonorListData`) (#501 by @yyuueexxiinngg)
- 戳一戳事件: `FriendNudgedEvent`, `MemberNudgedEvent`, `BotNudgedEvent`.(#600 by @sandtechnology)
- 发送戳一戳: `Bot.nudge()`, `User.nudge()`
- 为 `BotFactory` 添加伴生对象. 在顶层方法不方便使用时可使用伴生对象的 `Bot` 构建方法
### 优化和修复
- **修复好友消息和事件同步相关问题: **
- 部分情况下无法同步好友消息 (#249)
- BotInvitedJoinGroupRequestEvent 重复执行两次 (#449)
- 群消息可能发送失败 (#527)
- 机器人启动后第二次被拉入群聊不会刷新群列表 (#580)
- 新群员入群事件只触发一次 (#590)
- 入群申请 MemberJoinRequestEvent 没有广播 (#542)
- 新成员入群未处理 (#567)
- 修复 `At` 之后的多余空格的问题 (#557)
- 修复 `QuoteReply` 前多余 `At` 和空格的问题 (#524)
- 修复群信息初始值 (`isMuteAll`, `isAllowMemberInvite`, ...) (#286)
- 修复 `Voice.url` 的域名缺失问题 (#584 by @Hieuzest)
- 修复日志颜色污染的问题 (#596 by @Karlatemp)
- 修复 `Bot.nick` 无法获取的问题 (#566)
- 修复登录时 `IndexOutOfBoundsException` 的问题 (#598)
- 修复 Bot 被踢出群收到的事件类型错误的问题 (#358) (#509 by @sandtechnology)
- 修复 `Bot.close` 后必须收到一个数据包才会关闭的问题 (#557)
- 优化 `PermissionDeniedException` 的消息内容
## `1.2.3` 2020/9/11
- 在同步事件失败时添加重试, 改善 #249, #482, #542, #567, #590
- 修复不断重连同一个服务器的问题 (#589)

View File

@ -9,7 +9,7 @@
object Versions {
object Mirai {
const val version = "1.2.3"
const val version = "1.3.0"
}
object Kotlin {

View File

@ -27,6 +27,7 @@ import net.mamoe.mirai.event.subscribeAlways
import net.mamoe.mirai.network.ForceOfflineException
import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.qqandroid.network.BotNetworkHandler
import net.mamoe.mirai.qqandroid.network.DefaultServerList
import net.mamoe.mirai.qqandroid.network.closeAndJoin
import net.mamoe.mirai.supervisorJob
import net.mamoe.mirai.utils.*
@ -91,7 +92,11 @@ internal abstract class BotImpl<N : BotNetworkHandler> constructor(
}
bot.logger.info { "Connection lost, retrying login" }
bot.asQQAndroidBot().client.serverList.removeAt(0)
bot.asQQAndroidBot().client.run {
if (serverList.isEmpty()) {
serverList.addAll(DefaultServerList)
} else serverList.removeAt(0)
}
var failed = false
val time = measureTime {

View File

@ -32,6 +32,7 @@ import net.mamoe.mirai.event.events.NewFriendRequestEvent
import net.mamoe.mirai.event.internal.MiraiAtomicBoolean
import net.mamoe.mirai.getGroupOrNull
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.action.Nudge
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.qqandroid.contact.FriendImpl
@ -42,6 +43,7 @@ import net.mamoe.mirai.qqandroid.message.*
import net.mamoe.mirai.qqandroid.network.QQAndroidBotNetworkHandler
import net.mamoe.mirai.qqandroid.network.QQAndroidClient
import net.mamoe.mirai.qqandroid.network.highway.HighwayHelper
import net.mamoe.mirai.qqandroid.network.protocol.data.jce.StTroopNum
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.LongMsg
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.*
@ -226,6 +228,30 @@ internal class QQAndroidBot constructor(
accept = accept
)
}
@Suppress("CANNOT_OVERRIDE_INVISIBLE_MEMBER")
override suspend fun sendNudge(nudge: Nudge, receiver: Contact): Boolean {
if (configuration.protocol != BotConfiguration.MiraiProtocol.ANDROID_PHONE) {
throw UnsupportedOperationException("nudge is supported only with protocol ANDROID_PHONE")
}
network.run {
return if (receiver is Group) {
receiver.checkIsGroupImpl()
NudgePacket.troopInvoke(
client = client,
messageReceiverGroupCode = receiver.id,
nudgeTargetId = nudge.target.id,
).sendAndExpect<NudgePacket.Response>().success
} else {
NudgePacket.friendInvoke(
client = client,
messageReceiverUin = receiver.id,
nudgeTargetId = nudge.target.id,
).sendAndExpect<NudgePacket.Response>().success
}
}
}
}
@ -250,10 +276,15 @@ internal abstract class QQAndroidBotBase constructor(
override val friends: ContactList<Friend> = ContactList(LockFreeLinkedList())
@JvmField internal var cachedNick: String? = null
override val nick: String get() = cachedNick ?: selfInfo.nick.also { cachedNick = it }
override lateinit var nick: String
internal lateinit var selfInfo: JceFriendInfo
internal var selfInfo: JceFriendInfo? = null
get() = field ?: error("selfInfo is not yet initialized")
set(it) {
checkNotNull(it)
field = it
nick = it.nick
}
override val selfQQ: Friend by lazy {
@OptIn(LowLevelAPI::class)
@ -311,12 +342,22 @@ internal abstract class QQAndroidBotBase constructor(
}.groups.asSequence().map { it.groupUin.shl(32) and it.groupCode }
}
@Suppress(
"DeprecatedCallableAddReplaceWith",
"FunctionName",
"RedundantSuspendModifier",
"unused",
"unused_parameter"
)
@Deprecated("")
@OptIn(LowLevelAPI::class)
override suspend fun _lowLevelQueryGroupInfo(groupCode: Long): GroupInfo = network.run {
suspend fun _lowLevelQueryGroupInfo(groupCode: Long, stTroopNum: StTroopNum): GroupInfo = network.run {
error("This should not be invoked")
/*
TroopManagement.GetGroupInfo(
client = bot.client,
groupCode = groupCode
).sendAndExpect<GroupInfoImpl>(retry = 3)
).sendAndExpect<GroupInfoImpl>(retry = 3).also { it.stTroopNum = stTroopNum }*/
}
@OptIn(LowLevelAPI::class)
@ -608,6 +649,30 @@ internal abstract class QQAndroidBotBase constructor(
return json.decodeFromString(GroupActiveData.serializer(), rep)
}
@LowLevelAPI
@MiraiExperimentalAPI
override suspend fun _lowLevelGetGroupHonorListData(groupId: Long, type: GroupHonorType): GroupHonorListData? {
val data = network.async {
MiraiPlatformUtils.Http.get<String> {
url("https://qun.qq.com/interactive/honorlist")
parameter("gc", groupId)
parameter("type", type.value)
headers {
append(
"cookie",
"uin=o${id};" +
" skey=${client.wLoginSigInfo.sKey.data.encodeToString()};" +
" p_uin=o${id};" +
" p_skey=${client.wLoginSigInfo.psKeyMap["qun.qq.com"]?.data?.encodeToString()}; "
)
}
}
}
val rep = data.await()
val jsonText = Regex("""window.__INITIAL_STATE__=(.+?)</script>""").find(rep)?.groupValues?.get(1)
return jsonText?.let { json.decodeFromString(GroupHonorListData.serializer(), it) }
}
@JvmSynthetic
@LowLevelAPI
@MiraiExperimentalAPI

View File

@ -124,6 +124,9 @@ internal class MemberImpl constructor(
@Suppress("PropertyName")
var _muteTimestamp: Int = memberInfo.muteTimestamp
@Suppress("PropertyName")
var _nudgeTimestamp: Long = 0L
override val muteTimeRemaining: Int
get() = if (_muteTimestamp == 0 || _muteTimestamp == 0xFFFFFFFF.toInt()) {
0
@ -197,7 +200,7 @@ internal class MemberImpl constructor(
private fun checkBotPermissionHigherThanThis(operationName: String) {
check(group.botPermission > this.permission) {
throw PermissionDeniedException(
"`$operationName` operation requires a higher permission, while" +
"`$operationName` operation requires a higher permission, while " +
"${group.botPermission} < ${this.permission}"
)
}
@ -219,7 +222,6 @@ internal class MemberImpl constructor(
net.mamoe.mirai.event.events.MemberUnmuteEvent(this@MemberImpl, null).broadcast()
}
@JvmSynthetic
override suspend fun kick(message: String) {
checkBotPermissionHigherThanThis("kick")

View File

@ -113,7 +113,9 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean, withGeneralFlags: B
}
is At -> {
elements.add(ImMsgBody.Elem(text = it.toJceData()))
elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = " ")))
// elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = " ")))
// removed by https://github.com/mamoe/mirai/issues/524
// 发送 QuoteReply 消息时无可避免的产生多余空格 #524
}
is PokeMessage -> {
elements.add(
@ -151,7 +153,9 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean, withGeneralFlags: B
when (val source = it.source) {
is OnlineMessageSource.Incoming.FromGroup -> {
transformOneMessage(At(source.sender))
transformOneMessage(PlainText(" "))
// transformOneMessage(PlainText(" "))
// removed by https://github.com/mamoe/mirai/issues/524
// 发送 QuoteReply 消息时无可避免的产生多余空格 #524
}
}
}
@ -297,6 +301,17 @@ private fun MessageChain.cleanupRubbishMessageElements(): MessageChain {
return@forEach
}
}
if (element is QuoteReply) {
// 客户端为兼容早期不支持 QuoteReply 的客户端而添加的 At
removeLastOrNull()?.let { rm ->
if ((rm as? PlainText)?.content != " ") add(rm)
else removeLastOrNull()?.let { rm2 ->
if (rm2 !is At) add(rm2)
}
}
}
add(element)
last = element
}
@ -316,7 +331,7 @@ internal val MIRAI_CUSTOM_ELEM_TYPE = "mirai".hashCode() // 103904510
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
internal fun List<ImMsgBody.Elem>.joinToMessageChain(groupIdOrZero: Long, bot: Bot, list: MessageChainBuilder) {
// (this._miraiContentToString())
// (this._miraiContentToString().soutv())
this.forEach { element ->
when {
element.srcMsg != null -> {

View File

@ -246,7 +246,6 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
// self info
data.selfInfo?.run {
bot.cachedNick = null
bot.selfInfo = this
// bot.remark = remark ?: ""
// bot.sex = sex
@ -272,28 +271,11 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
suspend fun StTroopNum.reloadGroup() {
retryCatching(3) {
bot.groups.delegate.addLast(
@Suppress("DuplicatedCode")
GroupImpl(
bot = bot,
coroutineContext = bot.coroutineContext,
id = groupCode,
groupInfo = bot._lowLevelQueryGroupInfo(groupCode).apply {
this as GroupInfoImpl
if (this.delegate.groupName == null) {
this.delegate.groupName = groupName
}
if (this.delegate.groupMemo == null) {
this.delegate.groupMemo = groupMemo
}
if (this.delegate.groupUin == null) {
this.delegate.groupUin = groupUin
}
this.delegate.groupCode = this@reloadGroup.groupCode
},
groupInfo = GroupInfoImpl(this),
members = bot._lowLevelQueryGroupMemberList(
groupUin,
groupCode,

View File

@ -20,6 +20,7 @@ import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.network.NoServerAvailableException
import net.mamoe.mirai.qqandroid.BotAccount
import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.network.protocol.SyncingCacheList
import net.mamoe.mirai.qqandroid.network.protocol.data.jce.FileStoragePushFSSvcListFuckKotlin
import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
import net.mamoe.mirai.qqandroid.network.protocol.packet.PacketLogger
@ -191,9 +192,6 @@ internal open class QQAndroidClient(
private val highwayDataTransSequenceIdForApplyUp: AtomicInt = atomic(77918)
internal fun nextHighwayDataTransSequenceIdForApplyUp(): Int = highwayDataTransSequenceIdForApplyUp.getAndAdd(2)
internal val onlinePushCacheList: AtomicResizeCacheList<Short> = AtomicResizeCacheList(20.secondsToMillis)
internal val pbPushTransMsgCacheList: AtomicResizeCacheList<Int> = AtomicResizeCacheList(20.secondsToMillis)
val appClientVersion: Int = 0
var networkType: NetworkType = NetworkType.WIFI
@ -205,16 +203,49 @@ internal open class QQAndroidClient(
*/
val protocolVersion: Short = 8001
class C2cMessageSyncData {
class MessageSvcSyncData {
val firstNotify: AtomicBoolean = atomic(true)
@Volatile
var syncCookie: ByteArray? = null
var pubAccountCookie = EMPTY_BYTE_ARRAY
var msgCtrlBuf: ByteArray = EMPTY_BYTE_ARRAY
internal data class PbGetMessageSyncId(
val uid: Long,
val sequence: Int,
val time: Int
)
val pbGetMessageCacheList = SyncingCacheList<PbGetMessageSyncId>()
internal data class SystemMsgNewGroupSyncId(
val sequence: Long,
val time: Long
)
val systemMsgNewGroupCacheList = SyncingCacheList<SystemMsgNewGroupSyncId>(10)
internal data class PbPushTransMsgSyncId(
val uid: Long,
val sequence: Int,
val time: Int
)
val pbPushTransMsgCacheList = SyncingCacheList<PbPushTransMsgSyncId>(10)
internal data class OnlinePushReqPushSyncId(
val uid: Long,
val sequence: Short,
val time: Long
)
val onlinePushReqPushCacheList = SyncingCacheList<OnlinePushReqPushSyncId>(50)
}
val c2cMessageSync = C2cMessageSyncData()
val syncingController = MessageSvcSyncData()
/*
* 以下登录使用

View File

@ -0,0 +1,27 @@
/*
*
* * 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.protocol
import net.mamoe.mirai.qqandroid.utils.LinkedList
import kotlin.jvm.Synchronized
internal class SyncingCacheList<E>(private val size: Int = 50) {
private val packetIdList = LinkedList<E>()
@Synchronized // faster than suspending Mutex
fun addCache(element: E): Boolean {
if (packetIdList.contains(element)) return false // duplicate
packetIdList.addLast(element)
if (packetIdList.size >= size) packetIdList.removeFirst()
return true
}
}

View File

@ -172,13 +172,13 @@ internal class Oidb0x88d : ProtoBuf {
@ProtoNumber(12) @JvmField val groupDefaultPage: Int? = null,
@ProtoNumber(13) @JvmField val groupInfoSeq: Int? = null,
@ProtoNumber(14) @JvmField val groupRoamingTime: Int? = null,
@ProtoNumber(15) var groupName: String? = null,
@ProtoNumber(16) var groupMemo: String? = null,
@ProtoNumber(15) @JvmField val groupName: String? = null,
@ProtoNumber(16) @JvmField val groupMemo: String? = null,
@ProtoNumber(17) @JvmField val ingGroupFingerMemo: String? = null,
@ProtoNumber(18) @JvmField val ingGroupClassText: String? = null,
@ProtoNumber(19) @JvmField val groupAllianceCode: List<Int>? = null,
@ProtoNumber(20) @JvmField val groupExtraAdmNum: Int? = null,
@ProtoNumber(21) var groupUin: Long? = null,
@ProtoNumber(21) @JvmField val groupUin: Long? = null,
@ProtoNumber(22) @JvmField val groupCurMsgSeq: Int? = null,
@ProtoNumber(23) @JvmField val groupLastMsgTime: Int? = null,
@ProtoNumber(24) @JvmField val ingGroupQuestion: String? = null,
@ -258,7 +258,6 @@ internal class Oidb0x88d : ProtoBuf {
@ProtoNumber(98) @JvmField val cmduinRingtoneId: Int? = null,
@ProtoNumber(99) @JvmField val groupFlagext4: Int? = null,
@ProtoNumber(100) @JvmField val groupFreezeReason: Int? = null,
@ProtoNumber(101) var groupCode: Long? = null // mirai 添加
) : ProtoBuf
@Serializable
@ -2423,3 +2422,17 @@ internal class Cmd0x6ce : ProtoBuf {
) : ProtoBuf
}
@Serializable
internal class Cmd0xed3 : ProtoBuf {
@Serializable
internal class RspBody : ProtoBuf
@Serializable
internal class ReqBody(
@ProtoNumber(1) @JvmField val toUin: Long = 0L,
@ProtoNumber(2) @JvmField val groupCode: Long = 0L,
@ProtoNumber(3) @JvmField val msgSeq: Int = 0,
@ProtoNumber(4) @JvmField val msgRandom: Int = 0,
@ProtoNumber(5) @JvmField val aioUin: Long = 0L
) : ProtoBuf
}

View File

@ -13,10 +13,7 @@ import kotlinx.io.core.*
import net.mamoe.mirai.event.Event
import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.network.Packet
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.MultiMsg
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.NewContact
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.PbMessageSvc
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.*
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.ImgStore
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.LongConn
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.*
@ -148,9 +145,10 @@ internal object KnownPacketFactories {
TroopManagement.EditSpecialTitle,
TroopManagement.Mute,
TroopManagement.GroupOperation,
TroopManagement.GetGroupInfo,
// TroopManagement.GetGroupInfo,
TroopManagement.EditGroupNametag,
TroopManagement.Kick,
NudgePacket,
Heartbeat.Alive,
PbMessageSvc.PbMsgWithDraw,
MultiMsg.ApplyUp,

View File

@ -58,7 +58,7 @@ internal class NewContact {
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): NewFriendRequestEvent? {
readBytes().loadAs(Structmsg.RspSystemMsgNew.serializer()).run {
val struct = friendmsgs?.firstOrNull()
val struct = friendmsgs?.firstOrNull()// 会有重复且无法过滤, 不要用 map
return struct?.msg?.run {
NewFriendRequestEvent(
bot,
@ -145,9 +145,16 @@ internal class NewContact {
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Packet? {
readBytes().loadAs(Structmsg.RspSystemMsgNew.serializer()).run {
val struct = groupmsgs?.firstOrNull()
val struct = groupmsgs?.firstOrNull() ?: return null // 会有重复且无法过滤, 不要用 map
return struct?.msg?.run {
if (!bot.client.syncingController.systemMsgNewGroupCacheList.addCache(
QQAndroidClient.MessageSvcSyncData.SystemMsgNewGroupSyncId(struct.msgSeq, struct.msgTime)
)
) { // duplicate
return null
}
return struct.msg?.run {
//this.soutv("SystemMsg")
when (subType) {
1 -> { // 处理被邀请入群 或 处理成员入群申请

View File

@ -0,0 +1,79 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.qqandroid.network.protocol.packet.chat
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.readBytes
import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.network.Packet
import net.mamoe.mirai.qqandroid.network.QQAndroidClient
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Cmd0xed3
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.OidbSso
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.buildOutgoingUniPacket
import net.mamoe.mirai.qqandroid.utils.io.serialization.loadAs
import net.mamoe.mirai.qqandroid.utils.io.serialization.toByteArray
import net.mamoe.mirai.qqandroid.utils.io.serialization.writeProtoBuf
internal object NudgePacket : OutgoingPacketFactory<NudgePacket.Response>("OidbSvc.0xed3") {
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
with(readBytes().loadAs(OidbSso.OIDBSSOPkg.serializer())) {
return Response(result == 0, result)
}
}
class Response(val success: Boolean, val code: Int) : Packet {
override fun toString(): String = "NudgeResponse(success=$success,code=$code)"
}
fun friendInvoke(
client: QQAndroidClient,
nudgeTargetId: Long,
messageReceiverUin: Long,
): OutgoingPacket {
return buildOutgoingUniPacket(client) {
writeProtoBuf(
OidbSso.OIDBSSOPkg.serializer(),
OidbSso.OIDBSSOPkg(
command = 3795,
serviceType = 1,
result = 0,
bodybuffer = Cmd0xed3.ReqBody(
toUin = nudgeTargetId,
aioUin = messageReceiverUin
).toByteArray(Cmd0xed3.ReqBody.serializer())
)
)
}
}
fun troopInvoke(
client: QQAndroidClient,
messageReceiverGroupCode: Long,
nudgeTargetId: Long,
): OutgoingPacket {
return buildOutgoingUniPacket(client) {
writeProtoBuf(
OidbSso.OIDBSSOPkg.serializer(),
OidbSso.OIDBSSOPkg(
command = 3795,
serviceType = 1,
result = 0,
bodybuffer = Cmd0xed3.ReqBody(
toUin = nudgeTargetId,
groupCode = messageReceiverGroupCode
).toByteArray(Cmd0xed3.ReqBody.serializer())
)
)
}
}
}

View File

@ -14,13 +14,13 @@ import kotlinx.io.core.buildPacket
import kotlinx.io.core.readBytes
import kotlinx.io.core.toByteArray
import net.mamoe.mirai.LowLevelAPI
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.network.Packet
import net.mamoe.mirai.qqandroid.network.QQAndroidClient
import net.mamoe.mirai.qqandroid.network.protocol.data.jce.ModifyGroupCardReq
import net.mamoe.mirai.qqandroid.network.protocol.data.jce.RequestPacket
import net.mamoe.mirai.qqandroid.network.protocol.data.jce.StTroopNum
import net.mamoe.mirai.qqandroid.network.protocol.data.jce.stUinInfo
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.*
import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket
@ -32,19 +32,19 @@ import net.mamoe.mirai.data.GroupInfo as MiraiGroupInfo
@OptIn(LowLevelAPI::class)
internal class GroupInfoImpl(
internal val delegate: Oidb0x88d.GroupInfo
private val stTroopNum: StTroopNum
) : MiraiGroupInfo, Packet, Packet.NoLog {
override val uin: Long get() = delegate.groupUin ?: error("cannot find groupUin")
override val owner: Long get() = delegate.groupOwner ?: error("cannot find groupOwner")
override val groupCode: Long get() = Group.calculateGroupCodeByGroupUin(uin)
override val memo: String get() = delegate.groupMemo ?: error("cannot find groupMemo")
override val name: String get() = delegate.groupName ?: delegate.longGroupName ?: error("cannot find groupName")
override val allowMemberInvite get() = delegate.groupFlagExt?.and(0x000000c0) != 0
override val allowAnonymousChat get() = delegate.groupFlagExt?.and(0x40000000) == 0
override val autoApprove get() = delegate.groupFlagext3?.and(0x00100000) == 0
override val confessTalk get() = delegate.groupFlagext3?.and(0x00002000) == 0
override val muteAll: Boolean get() = delegate.shutupTimestamp != 0
override val botMuteTimestamp: Int get() = delegate.shutupTimestampMe ?: 0
override val uin: Long get() = stTroopNum.groupUin
override val owner: Long get() = stTroopNum.dwGroupOwnerUin
override val groupCode: Long get() = stTroopNum.groupCode
override val memo: String get() = stTroopNum.groupMemo
override val name: String get() = stTroopNum.groupName
override val allowMemberInvite get() = stTroopNum.dwGroupFlagExt?.and(0x000000c0) != 0L
override val allowAnonymousChat get() = stTroopNum.dwGroupFlagExt?.and(0x40000000) == 0L
override val autoApprove get() = stTroopNum.dwGroupFlagExt3?.and(0x00100000) == 0L
override val confessTalk get() = stTroopNum.dwGroupFlagExt3?.and(0x00002000) == 0L
override val muteAll: Boolean get() = stTroopNum.dwShutUpTimestamp != 0L
override val botMuteTimestamp: Int get() = stTroopNum.dwMyShutUpTimestamp?.toInt() ?: 0
}
internal class TroopManagement {
@ -88,6 +88,7 @@ internal class TroopManagement {
internal object GetGroupInfo : OutgoingPacketFactory<GroupInfoImpl>("OidbSvc.0x88d_7") {
@Deprecated("")
operator fun invoke(
client: QQAndroidClient,
groupCode: Long
@ -107,8 +108,8 @@ internal class TroopManagement {
groupFlagExt = 0,
groupFlagext4 = 0,
groupFlag = 0,
groupFlagext3 = 0,//获取confess
noFingerOpenFlag = 0,
groupFlagext3 = 1,//获取confess
noFingerOpenFlag = 1,
cmduinFlagEx2 = 0,
groupTypeFlag = 0,
appPrivilegeFlag = 0,
@ -135,12 +136,14 @@ internal class TroopManagement {
}
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): GroupInfoImpl {
error("deprecated")
/*
with(
this.readBytes()
.loadAs(OidbSso.OIDBSSOPkg.serializer()).bodybuffer.loadAs(Oidb0x88d.RspBody.serializer()).stzrspgroupinfo!![0].stgroupinfo!!
) {
return GroupInfoImpl(this)
}
return GroupInfoImpl()
}*/
}
}

View File

@ -17,7 +17,6 @@ import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
@ -63,10 +62,6 @@ import kotlin.random.Random
internal object MessageSvcPbGetMsg : OutgoingPacketFactory<MessageSvcPbGetMsg.Response>("MessageSvc.PbGetMsg") {
private val msgUidQueue = ArrayDeque<Long>()
private val msgUidSet = hashSetOf<Long>()
private val msgQueueMutex = Mutex()
@Suppress("SpellCheckingInspection")
operator fun invoke(
client: QQAndroidClient,
@ -88,7 +83,7 @@ internal object MessageSvcPbGetMsg : OutgoingPacketFactory<MessageSvcPbGetMsg.Re
whisperSessionId = 0,
syncFlag = syncFlag,
// serverBuf = from.serverBuf ?: EMPTY_BYTE_ARRAY,
syncCookie = syncCookie ?: client.c2cMessageSync.syncCookie
syncCookie = syncCookie ?: client.syncingController.syncCookie
?: byteArrayOf()//.also { client.c2cMessageSync.syncCookie = it },
// syncFlag = client.c2cMessageSync.syncFlag,
//msgCtrlBuf = client.c2cMessageSync.msgCtrlBuf,
@ -137,6 +132,17 @@ internal object MessageSvcPbGetMsg : OutgoingPacketFactory<MessageSvcPbGetMsg.Re
}
}
private suspend fun QQAndroidBot.createGroupForBot(groupUin: Long): Group? {
val group = getGroupByUinOrNull(groupUin)
if (group != null) {
return null
}
return getNewGroup(Group.calculateGroupCodeByGroupUin(groupUin))?.apply {
groups.delegate.addLast(this)
}
}
@OptIn(FlowPreview::class)
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
// 00 00 01 0F 08 00 12 00 1A 34 08 FF C1 C4 F1 05 10 FF C1 C4 F1 05 18 E6 ED B9 C3 02 20 89 FE BE A4 06 28 8A CA 91 D1 0C 48 9B A5 BD 9B 0A 58 DE 9D 99 F8 08 60 1D 68 FF C1 C4 F1 05 70 00 20 02 2A 9D 01 08 F3 C1 C4 F1 05 10 A2 FF 8C F0 03 18 01 22 8A 01 0A 2A 08 A2 FF 8C F0 03 10 DD F1 92 B7 07 18 A6 01 20 0B 28 AE F9 01 30 F4 C1 C4 F1 05 38 A7 E3 D8 D4 84 80 80 80 01 B8 01 CD B5 01 12 08 08 01 10 00 18 00 20 00 1A 52 0A 50 0A 27 08 00 10 F4 C1 C4 F1 05 18 A7 E3 D8 D4 04 20 00 28 0C 30 00 38 86 01 40 22 4A 0C E5 BE AE E8 BD AF E9 9B 85 E9 BB 91 12 08 0A 06 0A 04 4E 4D 53 4C 12 15 AA 02 12 9A 01 0F 80 01 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 12 04 4A 02 08 00 30 01 2A 15 08 97 A2 C1 F1 05 10 95 A6 F5 E5 0C 18 01 30 01 40 01 48 81 01 2A 10 08 D3 F7 B5 F1 05 10 DD F1 92 B7 07 18 01 30 01 38 00 42 00 48 00
@ -155,14 +161,14 @@ internal object MessageSvcPbGetMsg : OutgoingPacketFactory<MessageSvcPbGetMsg.Re
}
when (resp.msgRspType) {
0 -> {
bot.client.c2cMessageSync.syncCookie = resp.syncCookie
bot.client.c2cMessageSync.pubAccountCookie = resp.pubAccountCookie
bot.client.syncingController.syncCookie = resp.syncCookie
bot.client.syncingController.pubAccountCookie = resp.pubAccountCookie
}
1 -> {
bot.client.c2cMessageSync.syncCookie = resp.syncCookie
bot.client.syncingController.syncCookie = resp.syncCookie
}
2 -> {
bot.client.c2cMessageSync.pubAccountCookie = resp.pubAccountCookie
bot.client.syncingController.pubAccountCookie = resp.pubAccountCookie
}
}
@ -170,7 +176,7 @@ internal object MessageSvcPbGetMsg : OutgoingPacketFactory<MessageSvcPbGetMsg.Re
// bot.logger.debug(resp.msgRspType._miraiContentToString())
// bot.logger.debug(resp.syncCookie._miraiContentToString())
bot.client.c2cMessageSync.msgCtrlBuf = resp.msgCtrlBuf
bot.client.syncingController.msgCtrlBuf = resp.msgCtrlBuf
if (resp.uinPairMsgs == null) {
return EmptyResponse
@ -184,37 +190,22 @@ internal object MessageSvcPbGetMsg : OutgoingPacketFactory<MessageSvcPbGetMsg.Re
}.also {
MessageSvcPbDeleteMsg.delete(bot, it) // 删除消息
}
.mapNotNull<MsgComm.Msg, Packet> { msg ->
msgQueueMutex.lock()
val msgUid = msg.msgHead.msgUid
if (msgUidSet.size > 50) {
msgUidSet.remove(msgUidQueue.removeFirst())
}
if (!msgUidSet.add(msgUid)) {
msgQueueMutex.unlock()
return@mapNotNull null
}
msgQueueMutex.unlock()
msgUidQueue.addLast(msgUid)
suspend fun createGroupForBot(groupUin: Long): Group? {
val group = bot.getGroupByUinOrNull(groupUin)
if (group != null) {
return null
}
return bot.getNewGroup(Group.calculateGroupCodeByGroupUin(groupUin))?.apply {
bot.groups.delegate.addLast(this)
}
}
.mapNotNull { msg ->
if (!bot.client.syncingController.pbGetMessageCacheList.addCache(
QQAndroidClient.MessageSvcSyncData.PbGetMessageSyncId(
uid = msg.msgHead.msgUid,
sequence = msg.msgHead.msgSeq,
time = msg.msgHead.msgTime
)
)
) return@mapNotNull null
when (msg.msgHead.msgType) {
33 -> bot.groupListModifyLock.withLock {
if (msg.msgHead.authUin == bot.id) {
// 邀请入群
return@mapNotNull createGroupForBot(msg.msgHead.fromUin)?.let {
return@mapNotNull bot.createGroupForBot(msg.msgHead.fromUin)?.let {
// package: 27 0B 60 E7 01 CA CC 69 8B 83 44 71 47 90 06 B9 DC C0 ED D4 B1 00 30 33 44 30 42 38 46 30 39 37 32 38 35 43 34 31 38 30 33 36 41 34 36 31 36 31 35 32 37 38 46 46 43 30 41 38 30 36 30 36 45 38 31 43 39 41 34 38 37
// package: groupUin + 01 CA CC 69 8B 83 + invitorUin + length(06) + string + magicKey
val invitorUin = msg.msgBody.msgContent.sliceArray(10..13).toInt().toLong()
@ -256,7 +247,7 @@ internal object MessageSvcPbGetMsg : OutgoingPacketFactory<MessageSvcPbGetMsg.Re
}
38 -> bot.groupListModifyLock.withLock { // 建群
return@mapNotNull createGroupForBot(msg.msgHead.fromUin)
return@mapNotNull bot.createGroupForBot(msg.msgHead.fromUin)
?.let { BotJoinGroupEvent.Active(it) }
}
@ -265,7 +256,7 @@ internal object MessageSvcPbGetMsg : OutgoingPacketFactory<MessageSvcPbGetMsg.Re
// msg.msgHead.authUin: 处理人
return@mapNotNull if (msg.msgHead.toUin == bot.id) {
createGroupForBot(msg.msgHead.fromUin)
bot.createGroupForBot(msg.msgHead.fromUin)
?.let { BotJoinGroupEvent.Active(it) }
} else {
null
@ -427,7 +418,7 @@ internal object MessageSvcPbGetMsg : OutgoingPacketFactory<MessageSvcPbGetMsg.Re
MessageSvcPbGetMsg(
client,
MsgSvc.SyncFlag.CONTINUE,
bot.client.c2cMessageSync.syncCookie
bot.client.syncingController.syncCookie
).sendAndExpect<Packet>()
}
return
@ -438,7 +429,7 @@ internal object MessageSvcPbGetMsg : OutgoingPacketFactory<MessageSvcPbGetMsg.Re
MessageSvcPbGetMsg(
client,
MsgSvc.SyncFlag.CONTINUE,
bot.client.c2cMessageSync.syncCookie
bot.client.syncingController.syncCookie
).sendAndExpect<Packet>()
}
return
@ -454,28 +445,11 @@ internal suspend fun QQAndroidBot.getNewGroup(groupCode: Long): Group? {
.sendAndExpect<FriendList.GetTroopListSimplify.Response>(timeoutMillis = 10_000, retry = 5)
}.groups.firstOrNull { it.groupCode == groupCode } ?: return null
@Suppress("DuplicatedCode")
return GroupImpl(
bot = this,
coroutineContext = coroutineContext,
id = groupCode,
groupInfo = _lowLevelQueryGroupInfo(troopNum.groupCode).apply {
this as GroupInfoImpl
if (this.delegate.groupName == null) {
this.delegate.groupName = troopNum.groupName
}
if (this.delegate.groupMemo == null) {
this.delegate.groupMemo = troopNum.groupMemo
}
if (this.delegate.groupUin == null) {
this.delegate.groupUin = troopNum.groupUin
}
this.delegate.groupCode = troopNum.groupCode
},
groupInfo = GroupInfoImpl(troopNum),
members = _lowLevelQueryGroupMemberList(
troopNum.groupUin,
troopNum.groupCode,

View File

@ -80,7 +80,7 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg.
),
msgSeq = source.sequenceId,
msgRand = source.internalId,
syncCookie = client.c2cMessageSync.syncCookie ?: byteArrayOf()
syncCookie = client.syncingController.syncCookie ?: byteArrayOf()
// msgVia = 1
)
)
@ -110,7 +110,7 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg.
),
msgSeq = source.sequenceId,
msgRand = source.internalId,
syncCookie = client.c2cMessageSync.syncCookie ?: byteArrayOf()
syncCookie = client.syncingController.syncCookie ?: byteArrayOf()
)
)
}

View File

@ -31,13 +31,13 @@ internal object MessageSvcPushNotify : IncomingPacketFactory<RequestPushNotify>(
override suspend fun QQAndroidBot.handle(packet: RequestPushNotify, sequenceId: Int): OutgoingPacket? {
client.c2cMessageSync.firstNotify.loop { firstNotify ->
client.syncingController.firstNotify.loop { firstNotify ->
network.run {
return MessageSvcPbGetMsg(
client,
MsgSvc.SyncFlag.START,
if (firstNotify) {
if (!client.c2cMessageSync.firstNotify.compareAndSet(firstNotify, false)) {
if (!client.syncingController.firstNotify.compareAndSet(firstNotify, false)) {
return@loop
}
null

View File

@ -20,15 +20,16 @@ import kotlinx.io.core.readUByte
import kotlinx.io.core.readUInt
import net.mamoe.mirai.JavaFriendlyAPI
import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.event.events.BotGroupPermissionChangeEvent
import net.mamoe.mirai.event.events.MemberLeaveEvent
import net.mamoe.mirai.event.events.MemberPermissionChangeEvent
import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.contact.GroupImpl
import net.mamoe.mirai.qqandroid.contact.MemberImpl
import net.mamoe.mirai.qqandroid.contact.checkIsMemberImpl
import net.mamoe.mirai.qqandroid.message.contextualBugReportException
import net.mamoe.mirai.qqandroid.network.MultiPacketByIterable
import net.mamoe.mirai.qqandroid.network.Packet
import net.mamoe.mirai.qqandroid.network.QQAndroidClient
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.OnlinePushTrans
import net.mamoe.mirai.qqandroid.network.protocol.packet.IncomingPacketFactory
import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket
@ -45,52 +46,156 @@ internal object OnlinePushPbPushTransMsg :
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): Packet? {
val content = this.readProtoBuf(OnlinePushTrans.PbMsgInfo.serializer())
if (!bot.client.pbPushTransMsgCacheList.ensureNoDuplication(content.msgSeq)) {
if (!bot.client.syncingController.pbPushTransMsgCacheList.addCache(
content.run {
QQAndroidClient.MessageSvcSyncData.PbPushTransMsgSyncId(msgUid, msgSeq, msgTime)
}
)
) {
return null
}
// bot.network.logger.debug { content._miraiContentToString() }
content.msgData.read<Unit> {
when (content.msgType) {
44 -> {
// 3D C4 33 DD 01 FF CD 76 F4 03 C3 7E 2E 34
// 群转让
// start with 3D C4 33 DD 01 FF
// 3D C4 33 DD 01 FF C3 7E 2E 34 CD 76 F4 03
// 权限变更
// 3D C4 33 DD 01 00/01 .....
// 3D C4 33 DD 01 01 C3 7E 2E 34 01
this.discardExact(5)
val var4 = readByte().toInt()
var var5 = 0L
val target = readUInt().toLong()
if (var4 != 0 && var4 != 1) {
var5 = readUInt().toLong()
}
val group = bot.getGroupByUin(content.fromUin) as GroupImpl
if (var5 == 0L && this.remaining == 1L) {//管理员变更
val newPermission =
if (this.readByte().toInt() == 1) MemberPermission.ADMINISTRATOR
else MemberPermission.MEMBER
if (target == bot.id) {
if (group.botPermission == newPermission) {
return null
when (val mode = readUByte().toInt()) {
0xFF -> {
// 群转让 / huifu.qq.com
// From -> to
val from = readUInt().toLong()
val to = readUInt().toLong()
val results = ArrayList<Packet>()
// println("$from -> $to")
if (to == bot.id) {
if (bot.getGroupByUinOrNull(content.fromUin) == null) {
MessageSvcPbGetMsg.run {
results.add(
BotJoinGroupEvent.Retrieve(
bot.createGroupForBot(content.fromUin)!!
)
)
}
}
}
val group = bot.getGroupByUin(content.fromUin) as GroupImpl
if (from == bot.id) {
if (group.botPermission != MemberPermission.MEMBER)
results.add(
BotGroupPermissionChangeEvent(
group, group.botPermission.also {
group.botAsMember.checkIsMemberImpl().permission =
MemberPermission.MEMBER
},
MemberPermission.MEMBER
)
)
} else {
val member = group[from] as MemberImpl
if (member.permission != MemberPermission.MEMBER) {
results.add(
MemberPermissionChangeEvent(
member,
member.permission.also { member.permission = MemberPermission.MEMBER },
MemberPermission.MEMBER
)
)
}
}
if (to == bot.id) {
if (group.botPermission != MemberPermission.OWNER) {
results.add(
BotGroupPermissionChangeEvent(
group,
group.botAsMember.permission.also {
group.botAsMember.checkIsMemberImpl().permission =
MemberPermission.OWNER
},
MemberPermission.OWNER
)
)
}
} else {
val newOwner = group.getOrNull(to) ?: group.newMember(object : MemberInfo {
override val nameCard: String
get() = ""
override val permission: MemberPermission
get() = MemberPermission.OWNER
override val specialTitle: String
get() = ""
override val muteTimestamp: Int
get() = 0
override val uin: Long
get() = to
override val nick: String
get() = ""
}).also { owner ->
owner.checkIsMemberImpl().permission = MemberPermission.OWNER
group.members.delegate.addLast(owner)
results.add(MemberJoinEvent.Retrieve(owner))
}
if (newOwner.permission != MemberPermission.OWNER) {
results.add(
MemberPermissionChangeEvent(
newOwner,
newOwner.permission.also {
newOwner.checkIsMemberImpl().permission = MemberPermission.OWNER
},
MemberPermission.OWNER
)
)
}
}
return MultiPacketByIterable(results)
}
else -> {
var var5 = 0L
val target = readUInt().toLong()
if (mode != 0 && mode != 1) {
var5 = readUInt().toLong()
}
return BotGroupPermissionChangeEvent(
group,
group.botPermission.also {
group.botAsMember.checkIsMemberImpl().permission = newPermission
},
newPermission
)
} else {
val member = group[target] as MemberImpl
if (member.permission == newPermission) {
return null
}
val group = bot.getGroupByUin(content.fromUin) as GroupImpl
return MemberPermissionChangeEvent(
member,
member.permission.also { member.permission = newPermission },
newPermission
)
if (var5 == 0L && this.remaining == 1L) {//管理员变更
val newPermission =
if (this.readByte().toInt() == 1) MemberPermission.ADMINISTRATOR
else MemberPermission.MEMBER
if (target == bot.id) {
if (group.botPermission == newPermission) {
return null
}
return BotGroupPermissionChangeEvent(
group,
group.botPermission.also {
group.botAsMember.checkIsMemberImpl().permission = newPermission
},
newPermission
)
} else {
val member = group[target] as MemberImpl
if (member.permission == newPermission) {
return null
}
return MemberPermissionChangeEvent(
member,
member.permission.also { member.permission = newPermission },
newPermission
)
}
}
}
}
}
@ -108,28 +213,41 @@ internal object OnlinePushPbPushTransMsg :
A8 32 51 A1
83 3E 03 3F A2 06 B4 B4 BD A8 D5 DF 00 30 39 32 46 45 30 36 31 41 33 37 36 43 44 35 37 35 37 39 45 37 32 34 44 37 37 30 36 46 39 39 43 35 35 33 33 31 34 44 32 44 46 35 45 42 43 31 31 36
*/
readUInt().toLong() // group, uin or code ?
discardExact(1)
readUInt().toLong() // groupUin
readByte().toInt() // follow type
val target = readUInt().toLong()
val type = readUByte().toInt()
val operator = readUInt().toLong()
val groupUin = content.fromUin
when (type) {
0x82 -> bot.getGroupByUinOrNull(groupUin)?.let { group ->
val member = group.getOrNull(target) as? MemberImpl ?: return null
return MemberLeaveEvent.Quit(member.also {
member.cancel(CancellationException("Leaved actively"))
group.members.delegate.remove(member)
})
2, 0x82 -> bot.getGroupByUinOrNull(groupUin)?.let { group ->
if (target == bot.id) {
return BotLeaveEvent.Active(group).also {
group.cancel(CancellationException("Leaved actively"))
bot.groups.delegate.remove(group)
}
} else {
val member = group.getOrNull(target) as? MemberImpl ?: return null
return MemberLeaveEvent.Quit(member.also {
member.cancel(CancellationException("Leaved actively"))
group.members.delegate.remove(member)
})
}
}
0x83 -> bot.getGroupByUin(groupUin).let { group ->
val member = group.getOrNull(target) as? MemberImpl ?: return null
return MemberLeaveEvent.Kick(member.also {
member.cancel(CancellationException("Leaved actively"))
group.members.delegate.remove(member)
}, group.members[operator])
3, 0x83 -> bot.getGroupByUin(groupUin).let { group ->
if (target == bot.id) {
return BotLeaveEvent.Kick(group.members[operator]).also {
group.cancel(CancellationException("Being kicked"))
bot.groups.delegate.remove(group)
}
} else {
val member = group.getOrNull(target) as? MemberImpl ?: return null
return MemberLeaveEvent.Kick(member.also {
member.cancel(CancellationException("Being kicked"))
group.members.delegate.remove(member)
}, group.members[operator])
}
}
}
}

View File

@ -14,8 +14,6 @@
package net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.cancel
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.io.core.readBytes
@ -23,6 +21,8 @@ import kotlinx.io.core.readUInt
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import net.mamoe.mirai.JavaFriendlyAPI
import net.mamoe.mirai.contact.Friend
import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.data.FriendInfo
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.getFriendOrNull
@ -37,8 +37,9 @@ import net.mamoe.mirai.qqandroid.network.protocol.data.jce.MsgType0x210
import net.mamoe.mirai.qqandroid.network.protocol.data.jce.OnlinePushPack
import net.mamoe.mirai.qqandroid.network.protocol.data.jce.RequestPacket
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Submsgtype0x115
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Submsgtype0x122
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Submsgtype0x27.SubMsgType0x27.*
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Submsgtype0x44
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Submsgtype0x44.Submsgtype0x44
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Submsgtype0xb3
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.TroopTips0x857
import net.mamoe.mirai.qqandroid.network.protocol.packet.IncomingPacketFactory
@ -66,7 +67,11 @@ internal object OnlinePushReqPush : IncomingPacketFactory<OnlinePushReqPush.ReqP
mapper: ByteReadPacket.(msgInfo: MsgInfo) -> Sequence<Packet>
): Sequence<Packet> {
return asSequence().filter { msg ->
client.onlinePushCacheList.ensureNoDuplication(msg.shMsgSeq)
!client.syncingController.onlinePushReqPushCacheList.addCache(
QQAndroidClient.MessageSvcSyncData.OnlinePushReqPushSyncId(
uid = msg.lMsgUid ?: 0, sequence = msg.shMsgSeq, time = msg.uMsgTime
)
)
}.flatMap { it.vMsg.read { mapper(it) } }
}
@ -101,7 +106,7 @@ internal object OnlinePushReqPush : IncomingPacketFactory<OnlinePushReqPush.ReqP
528 -> {
val notifyMsgBody = readJceStruct(MsgType0x210.serializer())
Transformers528[notifyMsgBody.uSubMsgType]
?.let { processor -> processor(notifyMsgBody, bot) }
?.let { processor -> processor(notifyMsgBody, bot, msgInfo) }
?: kotlin.run {
bot.network.logger.debug {
"unknown group 528 type 0x${notifyMsgBody.uSubMsgType.toUHexString("")}, data: " + notifyMsgBody.vProtobuf.toUHexString()
@ -231,6 +236,44 @@ private object Transformers732 : Map<Int, Lambda732> by mapOf(
return@lambda732 sequenceOf(GroupAllowAnonymousChatEvent(!new, new, group, operator))
},
//系统提示
0x14 to lambda732 { group: GroupImpl, bot: QQAndroidBot ->
discardExact(1)
val grayTip = readProtoBuf(TroopTips0x857.NotifyMsgBody.serializer()).optGeneralGrayTip
when (grayTip?.templId) {
//戳一戳
10043L, 1134L, 1135L -> {
//预置数据,服务器将不会提供己方已知消息
var action = ""
var from: Member = group.botAsMember
var target: Member = group.botAsMember
var suffix = ""
grayTip.msgTemplParam?.map {
Pair(it.name.decodeToString(), it.value.decodeToString())
}?.asSequence()?.forEach { (key, value) ->
run {
when (key) {
"action_str" -> action = value
"uin_str1" -> from = group[value.toLong()]
"uin_str2" -> target = group[value.toLong()]
"suffix_str" -> suffix = value
}
}
}
if (target.id == bot.id) {
return@lambda732 sequenceOf(BotNudgedEvent(from, action, suffix))
}
return@lambda732 sequenceOf(MemberNudgedEvent(from, target, action, suffix))
}
else -> {
bot.network.logger.debug {
"Unknown Transformers528 0x14 template\ntemplId=${grayTip?.templId}\nPermList=${grayTip?.msgTemplParam?._miraiContentToString()}"
}
return@lambda732 emptySequence()
}
}
},
// 传字符串信息
0x10 to lambda732 { group: GroupImpl, bot: QQAndroidBot ->
val dataBytes = readBytes(26)
@ -329,17 +372,28 @@ private object Transformers732 : Map<Int, Lambda732> by mapOf(
}
)
internal val ignoredLambda528: Lambda528 = lambda528 { emptySequence() }
internal val ignoredLambda528: Lambda528 = lambda528 { _, _ -> emptySequence() }
internal interface Lambda528 {
operator fun invoke(msg: MsgType0x210, bot: QQAndroidBot): Sequence<Packet>
operator fun invoke(msg: MsgType0x210, bot: QQAndroidBot, msgInfo: MsgInfo): Sequence<Packet>
}
@kotlin.internal.LowPriorityInOverloadResolution
internal inline fun lambda528(crossinline block: MsgType0x210.(QQAndroidBot) -> Sequence<Packet>): Lambda528 {
return object : Lambda528 {
override fun invoke(msg: MsgType0x210, bot: QQAndroidBot): Sequence<Packet> {
override fun invoke(msg: MsgType0x210, bot: QQAndroidBot, msgInfo: MsgInfo): Sequence<Packet> {
return block(msg, bot)
}
}
}
internal inline fun lambda528(crossinline block: MsgType0x210.(QQAndroidBot, MsgInfo) -> Sequence<Packet>): Lambda528 {
return object : Lambda528 {
override fun invoke(msg: MsgType0x210, bot: QQAndroidBot, msgInfo: MsgInfo): Sequence<Packet> {
return block(msg, bot, msgInfo)
}
}
}
@ -353,7 +407,7 @@ internal object Transformers528 : Map<Long, Lambda528> by mapOf(
0x8AL to lambda528 { bot ->
@Serializable
data class Sub8AMsgInfo(
class Sub8AMsgInfo(
@ProtoNumber(1) val fromUin: Long,
@ProtoNumber(2) val botUin: Long,
@ProtoNumber(3) val srcId: Int,
@ -367,7 +421,7 @@ internal object Transformers528 : Map<Long, Lambda528> by mapOf(
) : ProtoBuf
@Serializable
data class Sub8A(
class Sub8A(
@ProtoNumber(1) val msgInfo: List<Sub8AMsgInfo>,
@ProtoNumber(2) val appId: Int, // 1
@ProtoNumber(3) val instId: Int, // 1
@ -403,7 +457,7 @@ internal object Transformers528 : Map<Long, Lambda528> by mapOf(
bot.friends.delegate.addLast(new)
return@lambda528 sequenceOf(FriendAddEvent(new))
},
0xE2L to lambda528 {
0xE2L to lambda528 { _ ->
// TODO: unknown. maybe messages.
// 0A 35 08 00 10 A2 FF 8C F0 03 1A 1B E5 90 8C E6 84 8F E4 BD A0 E7 9A 84 E5 8A A0 E5 A5 BD E5 8F 8B E8 AF B7 E6 B1 82 22 0C E6 BD 9C E6 B1 9F E7 BE A4 E5 8F 8B 28 01
// vProtobuf.loadAs(Msgtype0x210.serializer())
@ -411,7 +465,7 @@ internal object Transformers528 : Map<Long, Lambda528> by mapOf(
return@lambda528 emptySequence()
},
0x44L to lambda528 { bot ->
val msg = vProtobuf.loadAs(Submsgtype0x44.Submsgtype0x44.MsgBody.serializer())
val msg = vProtobuf.loadAs(Submsgtype0x44.MsgBody.serializer())
when {
msg.msgCleanCountMsg != null -> {
@ -426,20 +480,55 @@ internal object Transformers528 : Map<Long, Lambda528> by mapOf(
return@lambda528 emptySequence()
},
// bot 在其他客户端被踢或主动退出而同步情况
0xD4L to lambda528 { bot ->
0xD4L to lambda528 { _ ->
// this.soutv("0x210")
@Serializable
data class SubD4(
// ok
val uin: Long
) : ProtoBuf
/* @Serializable
data class SubD4(
// ok
val uin: Long
) : ProtoBuf
val uin = vProtobuf.loadAs(SubD4.serializer()).uin
val group = bot.getGroupByUinOrNull(uin) ?: bot.getGroupOrNull(uin)
return@lambda528 if (group != null && bot.groups.delegate.remove(group)) {
group.cancel(CancellationException("Being kicked"))
sequenceOf(BotLeaveEvent.Active(group))
} else emptySequence()
val uin = vProtobuf.loadAs(SubD4.serializer()).uin
val group = bot.getGroupByUinOrNull(uin) ?: bot.getGroupOrNull(uin)
return@lambda528 if (group != null && bot.groups.delegate.remove(group)) {
group.cancel(CancellationException("Being kicked"))
sequenceOf(BotLeaveEvent.Active(group))
} else emptySequence()*/
//ignore
return@lambda528 emptySequence()
},
//戳一戳信息等
0x122L to lambda528 { bot, _ ->
val body = vProtobuf.loadAs(Submsgtype0x122.Submsgtype0x122.MsgBody.serializer())
when (body.templId) {
//戳一戳
1134L, 1135L, 1136L, 10043L -> {
//预置数据,服务器将不会提供己方已知消息
var from: Friend = bot.selfQQ
var action = ""
var target: Friend = bot.selfQQ
var suffix = ""
body.msgTemplParam?.asSequence()?.map {
it.name.decodeToString() to it.value.decodeToString()
}?.forEach { (key, value) ->
when (key) {
"action_str" -> action = value
"uin_str1" -> from = bot.getFriend(value.toLong())
"uin_str2" -> target = bot.getFriend(value.toLong())
"suffix_str" -> suffix = value
}
}
return@lambda528 sequenceOf(BotNudgedEvent(from, action, suffix))
}
else -> {
bot.logger.debug {
"Unknown Transformers528 0x122L template\ntemplId=${body.templId}\nPermList=${body.msgTemplParam?._miraiContentToString()}"
}
return@lambda528 emptySequence()
}
}
},
//好友输入状态
0x115L to lambda528 { bot ->
@ -579,12 +668,10 @@ internal object Transformers528 : Map<Long, Lambda528> by mapOf(
val to = value.encodeToString()
if (uin == bot.id) {
val from = bot.nick
bot.cachedNick = to
add(
BotNickChangedEvent(
bot, from, to
)
)
if (from != to) {
bot.nick = to
add(BotNickChangedEvent(bot, from, to))
}
} else {
val friend = (bot.getFriendOrNull(uin) ?: return@forEach) as FriendImpl
val info = friend.friendInfo

View File

@ -0,0 +1,42 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.qqandroid.utils
// We target JVM and Android only.
internal expect class LinkedList<E>() : List<E>, Queue<E>, Deque<E>
internal interface Queue<E> : MutableCollection<E> {
override fun add(element: E): Boolean
fun offer(element: E): Boolean
fun remove(): E
fun poll(): E
fun element(): E
fun peek(): E
}
internal interface Deque<E> : Queue<E> {
fun addFirst(e: E)
fun addLast(e: E)
fun offerFirst(e: E): Boolean
fun offerLast(e: E): Boolean
fun removeFirst(): E
fun removeLast(): E
fun pollFirst(): E
fun pollLast(): E
val first: E
val last: E
fun peekFirst(): E
fun peekLast(): E
fun removeFirstOccurrence(o: E): Boolean
fun removeLastOccurrence(o: E): Boolean
fun push(e: E)
fun pop(): E
fun descendingIterator(): Iterator<E>
}

View File

@ -0,0 +1,17 @@
/*
*
* * 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.utils
import java.util.LinkedList
@Suppress("ACTUAL_WITHOUT_EXPECT")
internal actual typealias LinkedList<E> = LinkedList<E>

View File

@ -10,6 +10,7 @@
package net.mamoe.mirai.qqandroid.utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.Closeable
@ -20,6 +21,7 @@ import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.net.Socket
import java.net.SocketException
import java.util.concurrent.Executors
import kotlin.coroutines.CoroutineContext
/**
@ -38,6 +40,7 @@ internal actual class PlatformSocket : Closeable {
if (::socket.isInitialized) {
socket.close()
}
thread.shutdownNow()
}
@PublishedApi
@ -67,15 +70,17 @@ internal actual class PlatformSocket : Closeable {
}
}
private val thread = Executors.newSingleThreadExecutor()
/**
* @throws ReadPacketInternalException
*/
actual suspend fun read(): ByteReadPacket {
return withContext(Dispatchers.IO) {
try {
actual suspend fun read(): ByteReadPacket = suspendCancellableCoroutine { cont ->
thread.submit {
kotlin.runCatching {
readChannel.readPacketAtMost(Long.MAX_VALUE)
} catch (e: IOException) {
throw ReadPacketInternalException(e)
}.let {
cont.resumeWith(it)
}
}
}

View File

@ -20,6 +20,9 @@ import net.mamoe.mirai.event.events.BotInvitedJoinGroupRequestEvent
import net.mamoe.mirai.event.events.MemberJoinRequestEvent
import net.mamoe.mirai.event.events.NewFriendRequestEvent
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.action.BotNudge
import net.mamoe.mirai.message.action.MemberNudge
import net.mamoe.mirai.message.action.Nudge
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.utils.*
@ -216,6 +219,15 @@ public abstract class Bot internal constructor(
@JvmSynthetic
public abstract suspend fun recall(source: MessageSource)
/**
* 创建一个 "戳一戳" 消息
*
* @see MemberNudge.sendTo 发送这个戳一戳消息
*/
@MiraiExperimentalAPI
@SinceMirai("1.3.0")
public fun nudge(): BotNudge = BotNudge(this)
/**
* 获取图片下载链接
*
@ -339,6 +351,14 @@ public abstract class Bot internal constructor(
@JvmSynthetic
public abstract suspend fun ignoreInvitedJoinGroupRequest(event: BotInvitedJoinGroupRequestEvent)
@Deprecated(
"use member function.",
replaceWith = ReplaceWith("nudge.sendTo(contact)"),
level = DeprecationLevel.ERROR
)
@SinceMirai("1.3.0")
public abstract suspend fun sendNudge(nudge: Nudge, receiver: Contact): Boolean
// endregion
/**

View File

@ -13,6 +13,7 @@ package net.mamoe.mirai
import net.mamoe.mirai.utils.BotConfiguration
import net.mamoe.mirai.utils.Context
import net.mamoe.mirai.utils.SinceMirai
import kotlin.jvm.JvmName
import kotlin.jvm.JvmSynthetic
@ -45,6 +46,9 @@ public expect interface BotFactory {
passwordMd5: ByteArray,
configuration: BotConfiguration = BotConfiguration.Default
): Bot
@SinceMirai("1.3.0")
public companion object INSTANCE : BotFactory
}
/**

View File

@ -18,10 +18,13 @@ import net.mamoe.mirai.event.events.FriendMessagePostSendEvent
import net.mamoe.mirai.event.events.FriendMessagePreSendEvent
import net.mamoe.mirai.message.FriendMessageEvent
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.action.FriendNudge
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.PlainText
import net.mamoe.mirai.message.data.isContentEmpty
import net.mamoe.mirai.message.recall
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.SinceMirai
import kotlin.jvm.JvmSynthetic
/**
@ -70,6 +73,15 @@ public abstract class Friend : User(), CoroutineScope {
@JvmSynthetic
abstract override suspend fun sendMessage(message: Message): MessageReceipt<Friend>
/**
* 创建一个 "戳一戳" 消息
*
* @see FriendNudge.sendTo 发送这个戳一戳消息
*/
@MiraiExperimentalAPI
@SinceMirai("1.3.0")
public final override fun nudge(): FriendNudge = FriendNudge(this)
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "VIRTUAL_MEMBER_HIDDEN", "OVERRIDE_BY_INLINE")
@kotlin.internal.InlineOnly
@JvmSynthetic

View File

@ -16,10 +16,14 @@ import net.mamoe.mirai.JavaFriendlyAPI
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.getFriendOrNull
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.action.MemberNudge
import net.mamoe.mirai.message.action.Nudge
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.PlainText
import net.mamoe.mirai.message.data.isContentEmpty
import net.mamoe.mirai.message.recall
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.SinceMirai
import net.mamoe.mirai.utils.WeakRefProperty
import kotlin.jvm.JvmSynthetic
import kotlin.time.Duration
@ -157,6 +161,15 @@ public abstract class Member : MemberJavaFriendlyAPI, User() {
@JvmSynthetic
public abstract override suspend fun sendMessage(message: Message): MessageReceipt<Member>
/**
* 创建一个 "戳一戳" 消息
*
* @see MemberNudge.sendTo 发送这个戳一戳消息
*/
@MiraiExperimentalAPI
@SinceMirai("1.3.0")
public final override fun nudge(): Nudge = MemberNudge(this)
/**
* @see sendMessage
*/

View File

@ -15,11 +15,17 @@ import kotlinx.coroutines.CoroutineScope
import net.mamoe.mirai.Bot
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.action.FriendNudge
import net.mamoe.mirai.message.action.Nudge
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.PlainText
import net.mamoe.mirai.message.data.isContentEmpty
import net.mamoe.mirai.message.recall
import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.OverFileSizeMaxException
import net.mamoe.mirai.utils.SinceMirai
import kotlin.jvm.JvmSynthetic
/**
@ -67,6 +73,15 @@ public abstract class User : Contact(), CoroutineScope {
@JvmSynthetic
public abstract override suspend fun sendMessage(message: Message): MessageReceipt<User>
/**
* 创建一个 "戳一戳" 消息
*
* @see FriendNudge.sendTo 发送这个戳一戳消息
*/
@MiraiExperimentalAPI
@SinceMirai("1.3.0")
public abstract fun nudge(): Nudge
/**
* @see sendMessage
*/

View File

@ -0,0 +1,237 @@
package net.mamoe.mirai.data
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.SinceMirai
import kotlin.jvm.JvmStatic
/**
* 群荣誉信息
*/
@MiraiExperimentalAPI
@SinceMirai("1.3.0")
public enum class GroupHonorType(public val value: Int) {
TALKATIVE(1), // 龙王
PERFORMER(2), // 群聊之火
LEGEND(3), // 群聊炽焰
STRONG_NEWBIE(5), // 冒尖小春笋
EMOTION(6), // 快乐源泉
ACTIVE(7), // 活跃头衔
EXCLUSIVE(8), // 特殊头衔
MANAGE(9); // 管理头衔
internal companion object {
@JvmStatic
internal fun deserializeFromInt(value: Int): GroupHonorType = values().first { it.value == value }
}
}
@MiraiExperimentalAPI
@SinceMirai("1.3.0")
@Serializable
public data class GroupHonorListData(
@SerialName("acceptLanguages")
val acceptLanguages: List<Language?>? = null,
@SerialName("gc")
val gc: String?,
@Serializable(with = GroupHonorTypeSerializer::class)
@SerialName("type")
val type: GroupHonorType?,
@SerialName("uin")
val uin: String?,
@SerialName("talkativeList")
val talkativeList: List<Talkative?>? = null,
@SerialName("currentTalkative")
val currentTalkative: CurrentTalkative? = null,
@SerialName("actorList")
val actorList: List<Actor?>? = null,
@SerialName("legendList")
val legendList: List<Actor?>? = null,
@SerialName("newbieList")
val newbieList: List<Actor?>? = null,
@SerialName("strongnewbieList")
val strongNewbieList: List<Actor?>? = null,
@SerialName("emotionList")
val emotionList: List<Actor?>? = null,
@SerialName("levelname")
val levelName: LevelName? = null,
@SerialName("manageList")
val manageList: List<Tag?>? = null,
@SerialName("exclusiveList")
val exclusiveList: List<Tag?>? = null,
@SerialName("activeObj")
val activeObj: Map<String, List<Tag?>?>? = null, // Key为活跃等级名, 如`冒泡`
@SerialName("showActiveObj")
val showActiveObj: Map<String, Boolean?>? = null,
@SerialName("myTitle")
val myTitle: String?,
@SerialName("myIndex")
val myIndex: Int? = 0,
@SerialName("myAvatar")
val myAvatar: String?,
@SerialName("hasServerError")
val hasServerError: Boolean?,
@SerialName("hwExcellentList")
val hwExcellentList: List<Actor?>? = null
) {
@Serializable
public data class Language(
@SerialName("code")
val code: String? = null,
@SerialName("script")
val script: String? = null,
@SerialName("region")
val region: String? = null,
@SerialName("quality")
val quality: Double? = null
)
@Serializable
public data class Actor(
@SerialName("uin")
val uin: Long? = 0,
@SerialName("avatar")
val avatar: String? = null,
@SerialName("name")
val name: String? = null,
@SerialName("desc")
val desc: String? = null,
@SerialName("btnText")
val btnText: String? = null,
@SerialName("text")
val text: String? = null,
@SerialName("icon")
val icon: Int?
)
@Serializable
public data class Talkative(
@SerialName("uin")
val uin: Long? = 0,
@SerialName("avatar")
val avatar: String? = null,
@SerialName("name")
val name: String? = null,
@SerialName("desc")
val desc: String? = null,
@SerialName("btnText")
val btnText: String? = null,
@SerialName("text")
val text: String? = null
)
@Serializable
public data class CurrentTalkative(
@SerialName("uin")
val uin: Long? = 0,
@SerialName("day_count")
val dayCount: Int? = null,
@SerialName("avatar")
val avatar: String? = null,
@SerialName("avatar_size")
val avatarSize: Int? = null,
@SerialName("nick")
val nick: String? = null
)
@Serializable
public data class LevelName(
@SerialName("lvln1")
val lv1: String? = null,
@SerialName("lvln2")
val lv2: String? = null,
@SerialName("lvln3")
val lv3: String? = null,
@SerialName("lvln4")
val lv4: String? = null,
@SerialName("lvln5")
val lv5: String? = null,
@SerialName("lvln6")
val lv6: String? = null
)
@Serializable
public data class Tag(
@SerialName("uin")
val uin: Long? = 0,
@SerialName("avatar")
val avatar: String? = null,
@SerialName("name")
val name: String? = null,
@SerialName("btnText")
val btnText: String? = null,
@SerialName("text")
val text: String? = null,
@SerialName("tag")
val tag: String? = null, // 头衔
@SerialName("tagColor")
val tagColor: String? = null
)
@Serializer(forClass = GroupHonorType::class)
public object GroupHonorTypeSerializer : KSerializer<GroupHonorType> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("GroupHonorTypeSerializer", PrimitiveKind.INT)
override fun serialize(encoder: Encoder, value: GroupHonorType) {
encoder.encodeInt(value.value)
}
override fun deserialize(decoder: Decoder): GroupHonorType {
return GroupHonorType.deserializeFromInt(decoder.decodeInt())
}
}
}

View File

@ -9,13 +9,10 @@
package net.mamoe.mirai.data
import net.mamoe.mirai.Bot
import net.mamoe.mirai.LowLevelAPI
/**
* 群资料.
*
* 通过 [Bot._lowLevelQueryGroupInfo] 得到
*/
@LowLevelAPI
public interface GroupInfo {

View File

@ -21,6 +21,8 @@
- 服务器主动要求更换另一个服务器: RequireReconnect
- Bot 重新登录: BotReloginEvent
- Bot 头像改变: BotAvatarChangedEvent
- (`1.2.0+`) Bot 昵称改变: BotNickChangedEvent
- (`1.3.0+`) Bot 被戳: BotNudgedEvent
### [消息](message.kt)
- (`1.1.0-`) 主动发送消息: MessageSendEvent
@ -81,13 +83,17 @@
##### 成员权限
- 成员权限改变: MemberPermissionChangeEvent
##### 禁言
##### 动作
- 群成员被禁言: MemberMuteEvent
- 群成员被取消禁言: MemberUnmuteEvent
- (`1.3.0+`) 群员被戳: MemberNudgedEvent
### [好友](friend.kt)
- 好友昵称改变: FriendRemarkChangeEvent
- 成功添加了一个新好友: FriendAddEvent
- 好友已被删除: FriendDeleteEvent
- 一个账号请求添加机器人为好友: NewFriendRequestEvent
- 好友头像改变: FriendAvatarChangedEvent
- 好友头像改变: FriendAvatarChangedEvent
- (`1.2.0+`) 好友昵称改变: FriendNickChangedEvent
- (`1.2.0+`) 好友输入状态改变: FriendInputStatusChangedEvent
- (`1.3.0+`) 好友被戳: FriendNudgedEvent

View File

@ -14,7 +14,9 @@
package net.mamoe.mirai.event.events
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.User
import net.mamoe.mirai.event.AbstractEvent
import net.mamoe.mirai.message.action.Nudge
import net.mamoe.mirai.qqandroid.network.Packet
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.MiraiInternalAPI
@ -124,6 +126,31 @@ public data class BotNickChangedEvent(
public val to: String
) : BotEvent, Packet, AbstractEvent()
/**
* [Bot] [][Nudge] 的事件.
*/
@MiraiExperimentalAPI
@SinceMirai("1.3.0")
public data class BotNudgedEvent internal constructor(
/**
* 戳一戳的发起人 [Bot] 的某一好友, 或某一群员
*/
public val from: User,
/**
* 戳一戳的动作名称
*/
public val action: String,
/**
* 戳一戳中设置的自定义后缀
*/
public val suffix: String,
) : BotEvent, Packet, AbstractEvent() {
/**
* 戳一戳的目标
*/
public override val bot: Bot get() = from.bot
}
// region 图片
// endregion

View File

@ -112,8 +112,6 @@ public data class FriendAvatarChangedEvent internal constructor(
public override val friend: Friend
) : FriendEvent, Packet, AbstractEvent()
/**
* [Friend] 昵称改变事件, 在此事件广播时好友已经完成改名
* @see BotNickChangedEvent
@ -124,7 +122,7 @@ public data class FriendNickChangedEvent internal constructor(
public val from: String,
public val to: String
) : FriendEvent, Packet, AbstractEvent()
/**
* 好友输入状态改变的事件当开始输入文字退出聊天窗口或清空输入框时会触发此事件
*/

View File

@ -22,8 +22,10 @@ import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.event.AbstractEvent
import net.mamoe.mirai.event.BroadcastControllable
import net.mamoe.mirai.event.internal.MiraiAtomicBoolean
import net.mamoe.mirai.message.action.Nudge
import net.mamoe.mirai.qqandroid.network.Packet
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.SinceMirai
import net.mamoe.mirai.utils.internal.runBlocking
import kotlin.internal.LowPriorityInOverloadResolution
import kotlin.jvm.*
@ -130,6 +132,18 @@ public sealed class BotJoinGroupEvent : GroupEvent, BotPassiveEvent, Packet, Abs
return "BotJoinGroupEvent.Invite(invitor=$invitor)"
}
}
/**
* 原群主通过 https://huifu.qq.com/ 恢复原来群主身份并入群,
* [Bot] 是原群主
*/
@MiraiExperimentalAPI
@SinceMirai("1.3.0")
public data class Retrieve internal constructor(
public override val group: Group
) : BotJoinGroupEvent() {
override fun toString(): String = "MemberJoinEvent.Retrieve(group=${group.id})"
}
}
// region 群设置
@ -260,6 +274,17 @@ public sealed class MemberJoinEvent(
) : MemberJoinEvent(member) {
public override fun toString(): String = "MemberJoinEvent.Active(member=${member.id})"
}
/**
* 原群主通过 https://huifu.qq.com/ 恢复原来群主身份并入群,
* 此时 [member] [Member.permission] 肯定是 [MemberPermission.OWNER]
*/
@SinceMirai("1.3.0")
public data class Retrieve internal constructor(
public override val member: Member
) : MemberJoinEvent(member) {
override fun toString(): String = "MemberJoinEvent.Retrieve(member=${member.id})"
}
}
/**
@ -487,4 +512,33 @@ public data class MemberUnmuteEvent internal constructor(
// endregion
// region 戳一戳
/**
* [Member] [][Nudge] 的事件.
*/
@MiraiExperimentalAPI
@SinceMirai("1.3.0")
public data class MemberNudgedEvent internal constructor(
/**
* 戳一戳的发起人, 不可能是 bot
*/
public val from: Member,
/**
* 戳一戳的目标 (被戳的群员), 不可能是 bot
*/
public override val member: Member,
/**
* 戳一戳的动作名称
*/
public val action: String,
/**
* 戳一戳中设置的自定义后缀
*/
public val suffix: String,
) : GroupMemberEvent, BotPassiveEvent, Packet, AbstractEvent()
// endregion
// endregion

View File

@ -55,13 +55,6 @@ public interface LowLevelBotAPIAccessor {
@LowLevelAPI
public suspend fun _lowLevelQueryGroupList(): Sequence<Long>
/**
* 向服务器查询群资料. 获得的仅为当前时刻的资料.
* 请优先使用 [Bot.getGroup] 然后查看群资料.
*/
@LowLevelAPI
public suspend fun _lowLevelQueryGroupInfo(groupCode: Long): GroupInfo
/**
* 向服务器查询群成员列表.
* 请优先使用 [Bot.getGroup], [Group.members] 查看群成员.
@ -122,6 +115,15 @@ public interface LowLevelBotAPIAccessor {
public suspend fun _lowLevelGetGroupActiveData(groupId: Long, page: Int = -1): GroupActiveData
/**
* 获取群荣誉信息
*/
@SinceMirai("1.3.0")
@LowLevelAPI
@MiraiExperimentalAPI
public suspend fun _lowLevelGetGroupHonorListData(groupId: Long, type: GroupHonorType): GroupHonorListData?
/**
* 处理一个账号请求添加机器人为好友的事件
*/

View File

@ -0,0 +1,118 @@
/*
*
* * 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.message.action
import net.mamoe.kjbb.JvmBlockingBridge
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.event.events.BotNudgedEvent
import net.mamoe.mirai.event.events.MemberNudgedEvent
import net.mamoe.mirai.utils.BotConfiguration
import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.SinceMirai
/**
* 一个 "戳一戳" 消息.
*
* 仅在手机 QQ 8.4.0 左右版本才受支持. 其他客户端会忽略这些消息.
*
* @see User.nudge 创建 [Nudge] 对象
* @see Bot.nudge 创建 [Nudge] 对象
*/
@MiraiExperimentalAPI
@SinceMirai("1.3.0")
public sealed class Nudge {
/**
* 戳的对象. "A 戳了 B" 中的 "B".
*/
public abstract val target: ContactOrBot // User or Bot
/**
* 发送戳一戳该成员的消息.
*
* 需要 [使用协议][BotConfiguration.protocol] [MiraiProtocol.ANDROID_PHONE].
*
* @param receiver 这条 "戳一戳" 消息的接收对象. (不是 "" 动作的对象, 而是接收 "A 戳了 B" 这条消息的对象)
*
* @see MemberNudgedEvent 成员被戳事件
* @see BotNudgedEvent [Bot] 被戳事件
*
* @throws UnsupportedOperationException 当未使用 [安卓协议][MiraiProtocol.ANDROID_PHONE] 时抛出
*
* @return 成功发送时为 `true`. 若对方禁用 "戳一戳" 功能, 返回 `false`.
*/
@JvmBlockingBridge
@MiraiExperimentalAPI
public suspend fun sendTo(receiver: Contact): Boolean {
@Suppress("DEPRECATION_ERROR")
return receiver.bot.sendNudge(this, receiver)
}
public companion object {
/**
* 发送戳一戳该成员的消息.
*
* 需要 [使用协议][BotConfiguration.protocol] [MiraiProtocol.ANDROID_PHONE].
*
* @see MemberNudgedEvent 成员被戳事件
* @see BotNudgedEvent [Bot] 被戳事件
*
* @throws UnsupportedOperationException 当未使用 [安卓协议][MiraiProtocol.ANDROID_PHONE] 时抛出
*
* @return 成功发送时为 `true`. 若对方禁用 "戳一戳" 功能, 返回 `false`.
*/
@MiraiExperimentalAPI
@JvmBlockingBridge
public suspend fun Contact.sendNudge(nudge: Nudge): Boolean = nudge.sendTo(this)
}
}
/**
* @see Bot.nudge
* @see Nudge
*/
@MiraiExperimentalAPI
@SinceMirai("1.3.0")
public data class BotNudge(
public override val target: Bot
) : Nudge()
/**
* @see User.nudge
* @see Nudge
*/
@MiraiExperimentalAPI
@SinceMirai("1.3.0")
public sealed class UserNudge : Nudge() {
public abstract override val target: User
}
/**
* @see Member.nudge
* @see Nudge
*/
@MiraiExperimentalAPI
@SinceMirai("1.3.0")
public data class MemberNudge(
public override val target: Member
) : UserNudge()
/**
* @see Friend.nudge
* @see Nudge
*/
@MiraiExperimentalAPI
@SinceMirai("1.3.0")
public data class FriendNudge(
public override val target: Friend
) : UserNudge()

View File

@ -47,9 +47,11 @@ public class Voice @MiraiInternalAPI constructor(
}
public val url: String?
get() =
if (_url.startsWith("http")) _url
else null
get() = when {
_url.isBlank() -> null
_url.startsWith("http") -> _url
else -> "http://grouptalk.c2c.qq.com$_url"
}
private var _stringValue: String? = null
get() = field ?: kotlin.run {

View File

@ -15,6 +15,7 @@ package net.mamoe.mirai
import net.mamoe.mirai.utils.BotConfiguration
import net.mamoe.mirai.utils.Context
import net.mamoe.mirai.utils.ContextImpl
import net.mamoe.mirai.utils.SinceMirai
/**
* 构造 [Bot] 的工厂. 这是 [Bot] 唯一的构造方式.
@ -52,6 +53,16 @@ public actual interface BotFactory {
configuration: BotConfiguration
): Bot
@SinceMirai("1.3.0")
public actual companion object INSTANCE : BotFactory {
override fun Bot(context: Context, qq: Long, password: String, configuration: BotConfiguration): Bot {
return factory.Bot(context, qq, password, configuration)
}
override fun Bot(context: Context, qq: Long, passwordMd5: ByteArray, configuration: BotConfiguration): Bot {
return factory.Bot(context, qq, passwordMd5, configuration)
}
}
}
/**
@ -61,7 +72,12 @@ public actual interface BotFactory {
*/
@JvmName("newBot")
@JvmOverloads
public fun Bot(context: Context, qq: Long, password: String, configuration: BotConfiguration = BotConfiguration.Default): Bot =
public fun Bot(
context: Context,
qq: Long,
password: String,
configuration: BotConfiguration = BotConfiguration.Default
): Bot =
factory.Bot(context, qq, password, configuration)
/**
@ -109,7 +125,12 @@ public fun Bot(
* 自动加载现有协议的 [BotFactory], 并使用指定的 [配置][configuration] 构造 [Bot] 实例
*/
@JvmSynthetic
public inline fun Bot(context: Context, qq: Long, passwordMd5: ByteArray, configuration: (BotConfiguration.() -> Unit)): Bot =
public inline fun Bot(
context: Context,
qq: Long,
passwordMd5: ByteArray,
configuration: (BotConfiguration.() -> Unit)
): Bot =
factory.Bot(context, qq, passwordMd5, BotConfiguration().apply(configuration))

View File

@ -59,7 +59,7 @@ public actual open class PlatformLogger @JvmOverloads constructor(
*/
@SinceMirai("1.1.0")
protected open fun printLog(message: String?, priority: SimpleLogger.LogPriority) {
if (isColored) output("${priority.color}$currentTimeFormatted ${priority.simpleName}/$identity: $message")
if (isColored) output("${priority.color}$currentTimeFormatted ${priority.simpleName}/$identity: $message${Color.RESET}")
else output("$currentTimeFormatted ${priority.simpleName}/$identity: $message")
}
@ -143,4 +143,4 @@ internal val Throwable.stackTraceString
get() = ByteArrayOutputStream().run {
printStackTrace(PrintStream(this))
String(this.toByteArray())
}
}