[mock] Fix message recalling (#2421)

* Fix message recalling

Co-authored-by: Karlatemp <kar@kasukusakura.com>

* Message recalling tests

Co-authored-by: Karlatemp <kar@kasukusakura.com>
This commit is contained in:
Eritque arcus 2023-01-18 04:39:40 -05:00 committed by GitHub
parent aa84e6d337
commit 56dea84336
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 437 additions and 51 deletions

View File

@ -11,17 +11,11 @@ package net.mamoe.mirai.mock
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.Friend
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.contact.User
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.message.data.OnlineMessageSource
import net.mamoe.mirai.message.data.source
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.mock.contact.MockFriend
import net.mamoe.mirai.mock.contact.MockNormalMember
import net.mamoe.mirai.mock.contact.MockStranger
@ -125,42 +119,99 @@ public object MockActions {
*/
@JvmStatic
public suspend fun fireMessageRecalled(source: MessageSource, operator: User? = null) {
if (source is OnlineMessageSource) {
val from = source.sender
when (val target = source.target) {
is Group -> {
from.bot.mock().msgDatabase.removeMessageInfo(source)
MessageRecallEvent.GroupRecall(
source.bot,
from.id,
source.ids,
source.internalIds,
source.time,
operator?.cast(),
target,
when (from) {
is Bot -> target.botAsMember
else -> from.cast()
}
).broadcast()
return
fun notSupported(): Nothing = error("Unsupported message source kind: ${source.kind}: ${source.javaClass}")
val bot: MockBot = when {
source is OnlineMessageSource -> source.bot.mock()
operator != null -> operator.bot.mock()
else -> source.botOrNull?.mock() ?: error("Cannot find bot from source or operator")
}
val sourceKind = source.kind
fun target(): ContactOrBot = when {
source is OnlineMessageSource -> source.target
source.targetId == bot.id -> bot
sourceKind == MessageSourceKind.FRIEND -> bot.getFriendOrFail(source.targetId)
sourceKind == MessageSourceKind.STRANGER -> bot.getStrangerOrFail(source.targetId)
sourceKind == MessageSourceKind.TEMP -> error("Cannot detect message target from TEMP source kind")
sourceKind == MessageSourceKind.GROUP -> bot.getGroupOrFail(source.targetId)
else -> notSupported()
}
fun sender(): ContactOrBot = when {
source is OnlineMessageSource -> source.sender
source.fromId == bot.id -> bot
sourceKind == MessageSourceKind.FRIEND -> bot.getFriendOrFail(source.fromId)
sourceKind == MessageSourceKind.STRANGER -> bot.getStrangerOrFail(source.fromId)
sourceKind == MessageSourceKind.TEMP -> error("Cannot detect message sender from TEMP source kind")
sourceKind == MessageSourceKind.GROUP -> throw AssertionError("Message from group")
else -> notSupported()
}
fun subject(): Contact = when {
source is OnlineMessageSource -> source.subject
source.fromId == bot.id -> target() as Contact
sourceKind == MessageSourceKind.GROUP -> target() as Contact
else -> sender() as Contact
}
when (sourceKind) {
MessageSourceKind.GROUP -> {
val sender = sender()
val group = subject() as Group
val operator0 = when {
operator === bot -> null
operator === group.botAsMember -> null
operator == null -> sender.cast()
operator is Member -> operator
else -> error("Provided operator $operator(${operator.javaClass}) not a member")
}
is Friend -> {
from.bot.mock().msgDatabase.removeMessageInfo(source)
MessageRecallEvent.FriendRecall(
source.bot,
source.ids,
source.internalIds,
source.time,
from.id,
from.cast()
).broadcast()
return
}
bot.msgDatabase.removeMessageInfo(source)
MessageRecallEvent.GroupRecall(
bot, sender.id, source.ids, source.internalIds, source.time,
operator0,
group,
when (sender) {
is Bot -> group.botAsMember
else -> sender.cast()
}
).broadcast()
}
MessageSourceKind.FRIEND -> {
val subject = subject() as Friend
bot.msgDatabase.removeMessageInfo(source)
if (source.fromId == bot.id) {
return // no event
}
MessageRecallEvent.FriendRecall(bot, source.ids, source.internalIds, source.time, subject.id, subject)
.broadcast()
}
MessageSourceKind.TEMP -> {
bot.mock().msgDatabase.removeMessageInfo(source)
// TODO: event not available
}
MessageSourceKind.STRANGER -> {
bot.mock().msgDatabase.removeMessageInfo(source)
// TODO: event not available
}
else -> notSupported()
}
error("Unsupported message source type: ${source.javaClass}")
}
/**

View File

@ -24,9 +24,11 @@ import net.mamoe.mirai.internal.MiraiImpl
import net.mamoe.mirai.internal.network.components.EventDispatcher
import net.mamoe.mirai.message.action.Nudge
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.mock.MockActions
import net.mamoe.mirai.mock.MockBotFactory
import net.mamoe.mirai.mock.contact.MockGroup
import net.mamoe.mirai.mock.database.queryMessageInfo
import net.mamoe.mirai.mock.database.removeMessageInfo
import net.mamoe.mirai.mock.internal.contact.AQQ_RECALL_FAILED_MESSAGE
import net.mamoe.mirai.mock.internal.contact.MockFriendImpl
import net.mamoe.mirai.mock.internal.contact.MockImage
@ -178,6 +180,8 @@ internal class MockMiraiImpl : MiraiImpl() {
val canDelete = when (group.botPermission) {
MemberPermission.OWNER -> true
MemberPermission.ADMINISTRATOR -> kotlin.run w@{
if (info.sender == bot.id) return@w true
val member = group.getMember(info.sender) ?: return@w true
member.permission == MemberPermission.MEMBER
}
@ -241,9 +245,21 @@ internal class MockMiraiImpl : MiraiImpl() {
)
if (!resp) doFailed()
}
else -> {
is OnlineMessageSource.Incoming.FromStranger -> doFailed()
is OnlineMessageSource.Incoming.FromTemp -> doFailed()
is OnlineMessageSource.Outgoing.ToStranger -> {
bot.mock().msgDatabase.removeMessageInfo(source)
// TODO: No Event
}
is OnlineMessageSource.Outgoing.ToTemp -> {
bot.mock().msgDatabase.removeMessageInfo(source)
// TODO: No Event
}
else -> doFailed()
}
} else {
source as OfflineMessageSource
@ -267,12 +283,15 @@ internal class MockMiraiImpl : MiraiImpl() {
)
if (!resp) doFailed()
}
MessageSourceKind.TEMP -> {
// TODO: No Event
}
MessageSourceKind.STRANGER -> {
// TODO: No Event
MessageSourceKind.TEMP, MessageSourceKind.STRANGER -> {
if (source.fromId != bot.id) {
doFailed()
}
MockActions.fireMessageRecalled(source, bot.asFriend)
}
else -> doFailed()
}
}
}

View File

@ -11,7 +11,9 @@ package net.mamoe.mirai.mock.test
import net.mamoe.mirai.event.Event
import net.mamoe.mirai.event.GlobalEventChannel
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.mock.MockBotFactory
import net.mamoe.mirai.mock.database.queryMessageInfo
import net.mamoe.mirai.mock.internal.MockBotImpl
import net.mamoe.mirai.mock.utils.MockActionsScope
import net.mamoe.mirai.mock.utils.broadcastMockEvents
@ -19,6 +21,7 @@ import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.TestInstance
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.test.fail
@TestInstance(TestInstance.Lifecycle.PER_METHOD)
internal open class MockBotTestBase : TestBase() {
@ -55,4 +58,17 @@ internal open class MockBotTestBase : TestBase() {
return result
}
internal fun assertMessageNotAvailable(source: MessageSource) {
if (bot.msgDatabase.queryMessageInfo(source.ids, source.internalIds) != null) {
fail("Require message $source no longer available.")
}
}
internal fun assertMessageAvailable(source: MessageSource) {
if (bot.msgDatabase.queryMessageInfo(source.ids, source.internalIds) == null) {
fail("Require message $source available.")
}
}
}

View File

@ -15,6 +15,9 @@ import net.mamoe.mirai.event.Event
import net.mamoe.mirai.event.events.MessageEvent
import net.mamoe.mirai.event.events.MessagePostSendEvent
import net.mamoe.mirai.event.events.MessagePreSendEvent
import org.junit.jupiter.api.DynamicContainer
import org.junit.jupiter.api.DynamicNode
import org.junit.jupiter.api.DynamicTest
import org.junit.jupiter.api.fail
import java.net.URL
import kotlin.reflect.jvm.jvmName
@ -47,4 +50,15 @@ internal open class TestBase {
assertFails { block() }
}
internal fun dynamicTest(displayName: String, action: suspend CoroutineScope.() -> Unit): DynamicTest {
return DynamicTest.dynamicTest(displayName) { runBlocking(block = action) }
}
internal fun dynamicContainer(
displayName: String,
action: suspend CoroutineScope.() -> Iterable<DynamicNode>
): DynamicContainer {
return DynamicContainer.dynamicContainer(displayName, runBlocking(block = action))
}
}

View File

@ -16,13 +16,14 @@ import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.message.data.MessageSource.Key.recall
import net.mamoe.mirai.mock.MockActions.mockFireRecalled
import net.mamoe.mirai.mock.test.MockBotTestBase
import net.mamoe.mirai.mock.userprofile.MockMemberInfoBuilder
import net.mamoe.mirai.mock.utils.broadcastMockEvents
import net.mamoe.mirai.mock.utils.simpleMemberInfo
import org.junit.jupiter.api.DynamicNode
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails
import kotlin.test.assertNull
import kotlin.test.assertSame
import org.junit.jupiter.api.TestFactory
import kotlin.test.*
internal class MessagingTest : MockBotTestBase() {
@ -199,7 +200,7 @@ internal class MessagingTest : MockBotTestBase() {
}.dropMsgChat().let { events ->
assertEquals(5, events.size)
assertIsInstance<MessageRecallEvent.GroupRecall>(events[0]) {
assertNull(operator)
assertSame(sender, operator)
assertSame(sender, author)
}
assertIsInstance<MessageRecallEvent.GroupRecall>(events[1]) {
@ -236,4 +237,289 @@ internal class MessagingTest : MockBotTestBase() {
}
}
@Suppress("ComplexRedundantLet")
@Nested
internal inner class MessageRecalling {
@TestFactory
fun `friend messaging`(): Iterable<DynamicNode> {
val myFriend = bot.addFriend(1, "f")
return listOf<DynamicNode>(
dynamicTest("bot recalling") {
val msgBot = myFriend.sendMessage("2")
runAndReceiveEventBroadcast {
msgBot.recall()
}.let { events ->
assertEquals(0, events.size)
}
assertMessageNotAvailable(msgBot.source)
},
dynamicTest("friend recalling") {
val msgFriend = myFriend.says("1")
runAndReceiveEventBroadcast {
msgFriend.recalledBySender()
}.let { events ->
assertEquals(1, events.size)
assertIsInstance<MessageRecallEvent.FriendRecall>(events[0]) {
assertEquals(myFriend, this.operator)
assertContentEquals(msgFriend.source.ids, this.messageIds)
assertContentEquals(msgFriend.source.internalIds, this.messageInternalIds)
}
assertMessageNotAvailable(msgFriend.source)
}
},
dynamicTest("bot recall friend msg failed") {
val msg = myFriend.says("1")
assertFails { msg.recall() }
assertMessageAvailable(msg.source)
},
)
}
@TestFactory
fun `stranger messaging`(): Iterable<DynamicNode> {
val myStranger = bot.addStranger(2, "s")
return listOf<DynamicNode>(
dynamicTest("stranger recalling") {
val msg = myStranger.says("1")
runAndReceiveEventBroadcast {
msg.recalledBySender()
}.let { events ->
assertEquals(0, events.size)
}
assertMessageNotAvailable(msg.source)
},
dynamicTest("bot recalling") {
val msg = myStranger.sendMessage("1")
runAndReceiveEventBroadcast {
msg.recall()
}.let { events ->
assertEquals(0, events.size)
}
assertMessageNotAvailable(msg.source)
},
dynamicTest("bot recall stranger failed") {
val msg = myStranger.says("1")
assertFails { msg.recall() }
assertMessageAvailable(msg.source)
},
)
}
@TestFactory
fun `group messaging`(): Iterable<DynamicNode> = listOf(
dynamicContainer("Normal messaging test") {
val group = bot.addGroup(18451444229, "owner group")
val owner = group.addMember(MockMemberInfoBuilder.create {
uin(184554).permission(MemberPermission.OWNER)
})
val administrator = group.addMember(MockMemberInfoBuilder.create {
uin(184).permission(MemberPermission.ADMINISTRATOR)
})
val member = group.addMember(7777, "wapeog")
group.botAsMember.mockApi.permission = MemberPermission.MEMBER
return@dynamicContainer listOf<DynamicNode>(
dynamicTest("member self recalling") {
val msg = member.says("1")
runAndReceiveEventBroadcast { msg.recalledBySender() }.let { events ->
assertEquals(1, events.size)
assertIsInstance<MessageRecallEvent.GroupRecall>(events[0]) {
assertSame(member, this.author)
assertSame(member, operator)
assertContentEquals(msg.source.ids, this.messageIds)
assertContentEquals(msg.source.internalIds, this.messageInternalIds)
}
}
assertMessageNotAvailable(msg.source)
},
dynamicTest("member msg recalled by others") {
val msg = member.says("1")
runAndReceiveEventBroadcast { msg.recalledBy(administrator) }.let { events ->
assertEquals(1, events.size)
assertIsInstance<MessageRecallEvent.GroupRecall>(events[0]) {
assertSame(member, this.author)
assertSame(administrator, operator)
assertContentEquals(msg.source.ids, this.messageIds)
assertContentEquals(msg.source.internalIds, this.messageInternalIds)
}
}
assertMessageNotAvailable(msg.source)
},
dynamicTest("member msg forced recalled by bot") {
val msg = member.says("1")
runAndReceiveEventBroadcast { msg.recalledBy(group.botAsMember) }.let { events ->
assertEquals(1, events.size)
assertIsInstance<MessageRecallEvent.GroupRecall>(events[0]) {
assertSame(member, this.author)
assertSame(null, operator)
assertContentEquals(msg.source.ids, this.messageIds)
assertContentEquals(msg.source.internalIds, this.messageInternalIds)
}
}
assertMessageNotAvailable(msg.source)
},
)
},
dynamicContainer("bot is owner") {
val group = bot.addGroup(8412, "owner group")
val administrator = group.addMember(MockMemberInfoBuilder.create {
uin(184).permission(MemberPermission.ADMINISTRATOR)
})
val member = group.addMember(7777, "wapeog")
assertEquals(group.botPermission, MemberPermission.OWNER)
return@dynamicContainer listOf<DynamicNode>(
dynamicTest("Bot can recall itself message") {
val msg = group.sendMessage("1")
runAndReceiveEventBroadcast { msg.recall() }.let { events ->
assertEquals(1, events.size)
assertIsInstance<MessageRecallEvent.GroupRecall>(events[0]) {
assertSame(group.botAsMember, this.author)
assertSame(null, operator)
assertContentEquals(msg.source.ids, this.messageIds)
assertContentEquals(msg.source.internalIds, this.messageInternalIds)
}
}
assertMessageNotAvailable(msg.source)
},
dynamicTest("Bot can recall administrator message") {
val msg = administrator.says("1")
runAndReceiveEventBroadcast { msg.recall() }.let { events ->
assertEquals(1, events.size)
assertIsInstance<MessageRecallEvent.GroupRecall>(events[0]) {
assertSame(administrator, this.author)
assertSame(null, operator)
assertContentEquals(msg.source.ids, this.messageIds)
assertContentEquals(msg.source.internalIds, this.messageInternalIds)
}
}
assertMessageNotAvailable(msg.source)
},
dynamicTest("Bot can recall member message") {
val msg = member.says("1")
runAndReceiveEventBroadcast { msg.recall() }.let { events ->
assertEquals(1, events.size)
assertIsInstance<MessageRecallEvent.GroupRecall>(events[0]) {
assertSame(member, this.author)
assertSame(null, operator)
assertContentEquals(msg.source.ids, this.messageIds)
assertContentEquals(msg.source.internalIds, this.messageInternalIds)
}
}
assertMessageNotAvailable(msg.source)
},
)
},
dynamicContainer("bot is administrator") {
val group = bot.addGroup(7517, "owner group")
val owner = group.addMember(MockMemberInfoBuilder.create {
uin(96451).permission(MemberPermission.OWNER)
})
val administrator = group.addMember(MockMemberInfoBuilder.create {
uin(184).permission(MemberPermission.ADMINISTRATOR)
})
val member = group.addMember(7777, "wapeog")
group.botAsMember.mockApi.permission = MemberPermission.ADMINISTRATOR
return@dynamicContainer listOf<DynamicNode>(
dynamicTest("Bot can recall itself message") {
val msg = group.sendMessage("1")
runAndReceiveEventBroadcast { msg.recall() }.let { events ->
assertEquals(1, events.size)
assertIsInstance<MessageRecallEvent.GroupRecall>(events[0]) {
assertSame(group.botAsMember, this.author)
assertSame(null, operator)
assertContentEquals(msg.source.ids, this.messageIds)
assertContentEquals(msg.source.internalIds, this.messageInternalIds)
}
}
assertMessageNotAvailable(msg.source)
},
dynamicTest("Bot cannot recall owner message") {
val msg = owner.says("1")
assertFails { msg.recall() }
assertMessageAvailable(msg.source)
},
dynamicTest("Bot cannot recall administrator message") {
val msg = administrator.says("1")
assertFails { msg.recall() }
assertMessageAvailable(msg.source)
},
dynamicTest("Bot can recall member message") {
val msg = member.says("1")
runAndReceiveEventBroadcast { msg.recall() }.let { events ->
assertEquals(1, events.size)
assertIsInstance<MessageRecallEvent.GroupRecall>(events[0]) {
assertSame(member, this.author)
assertSame(null, operator)
assertContentEquals(msg.source.ids, this.messageIds)
assertContentEquals(msg.source.internalIds, this.messageInternalIds)
}
}
assertMessageNotAvailable(msg.source)
},
)
},
dynamicContainer("bot is member") {
val group = bot.addGroup(8441117, "owner group")
val owner = group.addMember(MockMemberInfoBuilder.create {
uin(98171).permission(MemberPermission.OWNER)
})
val administrator = group.addMember(MockMemberInfoBuilder.create {
uin(184).permission(MemberPermission.ADMINISTRATOR)
})
val member = group.addMember(7777, "wapeog")
group.botAsMember.mockApi.permission = MemberPermission.MEMBER
return@dynamicContainer listOf<DynamicNode>(
dynamicTest("Bot can recall itself message") {
val msg = group.sendMessage("1")
runAndReceiveEventBroadcast { msg.recall() }.let { events ->
assertEquals(1, events.size)
assertIsInstance<MessageRecallEvent.GroupRecall>(events[0]) {
assertSame(group.botAsMember, this.author)
assertSame(null, operator)
assertContentEquals(msg.source.ids, this.messageIds)
assertContentEquals(msg.source.internalIds, this.messageInternalIds)
}
}
assertMessageNotAvailable(msg.source)
},
dynamicTest("Bot cannot recall owner message") {
val msg = owner.says("1")
assertFails { msg.recall() }
assertMessageAvailable(msg.source)
},
dynamicTest("Bot cannot recall administrator message") {
val msg = administrator.says("1")
assertFails { msg.recall() }
assertMessageAvailable(msg.source)
},
dynamicTest("Bot cannot recall member message") {
val msg = member.says("1")
assertFails { msg.recall() }
assertMessageAvailable(msg.source)
},
)
},
)
}
}