mirror of
https://github.com/mamoe/mirai.git
synced 2025-02-26 12:10:13 +08:00
Mock Testing Framework (#1521)
Co-authored-by: Eritque arcus <1930893235@qq.com> Co-authored-by: Him188 <Him188@mamoe.net>
This commit is contained in:
parent
2eb2c3acd0
commit
2db9804cf2
@ -63,6 +63,11 @@ object Versions {
|
||||
const val yamlkt = "0.12.0"
|
||||
const val intellijGradlePlugin = "1.7.0"
|
||||
|
||||
// https://github.com/google/jimfs
|
||||
// Java In Memory File System
|
||||
const val jimfs = "1.2"
|
||||
|
||||
|
||||
// don't update easily unless you want your disk space -= 1000 MB
|
||||
// (700 MB for IDEA, 150 MB for sources, 150 MB for JBR)
|
||||
const val intellij = "222.3345-EAP-CANDIDATE-SNAPSHOT"
|
||||
@ -115,6 +120,10 @@ val `ktor-client-logging` = ktor("client-logging", Versions.ktor)
|
||||
val `ktor-network` = ktor("network-jvm", Versions.ktor)
|
||||
val `ktor-client-serialization` = ktor("client-serialization", Versions.ktor)
|
||||
|
||||
val `ktor-server-core` = ktor("server-core", Versions.ktor)
|
||||
val `ktor-server-netty` = ktor("server-netty", Versions.ktor)
|
||||
const val `java-in-memory-file-system` = "com.google.jimfs:jimfs:" + Versions.jimfs
|
||||
|
||||
const val `logback-classic` = "ch.qos.logback:logback-classic:" + Versions.logback
|
||||
|
||||
const val `slf4j-api` = "org.slf4j:slf4j-api:" + Versions.slf4j
|
||||
|
@ -35,6 +35,7 @@ module.exports = {
|
||||
{text: "事件列表", link: "/EventList.html"},
|
||||
{text: "Debugging Network", link: "/DebuggingNetwork.html"},
|
||||
{text: "Using Dev Snapshots", link: "/UsingSnapshots.html"},
|
||||
{text: "mirai 模拟测试框架", link: "/mocking/Mocking.md"},
|
||||
]
|
||||
},
|
||||
],
|
||||
|
@ -21,6 +21,7 @@
|
||||
- [处理滑动验证码](#处理滑动验证码)
|
||||
- [常见登录失败原因](#常见登录失败原因)
|
||||
- [附录: 调试网络层](#附录-调试网络层)
|
||||
- [附录: 模拟测试框架](#附录-模拟测试框架)
|
||||
|
||||
## 1. 创建和配置 `Bot`
|
||||
|
||||
@ -284,6 +285,10 @@ contactListCache.setSaveIntervalMillis(60000) // 可选设置有更新时的保
|
||||
|
||||
参阅 [DebuggingNetwork.md](DebuggingNetwork.md)
|
||||
|
||||
## 附录: 模拟测试框架
|
||||
|
||||
参阅 [Mocking.md](mocking/Mocking.md)
|
||||
|
||||
> 下一步,[Contacts](Contacts.md)
|
||||
>
|
||||
> [回到 Mirai 文档索引](CoreAPI.md)
|
||||
|
49
docs/mocking/Mocking.md
Normal file
49
docs/mocking/Mocking.md
Normal file
@ -0,0 +1,49 @@
|
||||
# Mirai - Mocking
|
||||
|
||||
本章节介绍 mirai 模拟环境
|
||||
|
||||
> mirai 模拟环境从 `2.13` 开始支持
|
||||
>
|
||||
> 注:
|
||||
> - **不支持**同时运行模拟环境和真实环境
|
||||
> - **不支持**从模拟环境切换回真实环境
|
||||
|
||||
-----------------------------------
|
||||
|
||||
# 在非 console 中进行模拟
|
||||
|
||||
## 环境准备
|
||||
|
||||
要使用 mirai 模拟环境测试框架, 首先需要额外添加一项依赖
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
testImplementation("net.mamoe:mirai-core-mock:$VERSION")
|
||||
}
|
||||
```
|
||||
|
||||
并在本地的测试入口添加以下的代码
|
||||
|
||||
```kotlin
|
||||
internal fun main() {
|
||||
MockBotFactory.initialize()
|
||||
// .....
|
||||
}
|
||||
```
|
||||
|
||||
## 创建 Bot
|
||||
|
||||
对于创建 `MockBot`, 更好的方法是使用 `MockBotFactory.newMockBotBuilder()`
|
||||
|
||||
也可以使用原始的 `BotFactory` 来创建一个新的 `MockBot`, 系统会使用默认值填充相关的信息
|
||||
|
||||
## 使用
|
||||
|
||||
关于 `MockBot` 可以在 [这里](https://github.com/mamoe/mirai/tree/dev/mirai-core-mock/test/mock)
|
||||
找到 mirai-core-mock 的相关用法
|
||||
|
||||
----------------
|
||||
|
||||
# 在 console 中进行模拟
|
||||
|
||||
Work In Progress...
|
21
mirai-core-mock/README.md
Normal file
21
mirai-core-mock/README.md
Normal file
@ -0,0 +1,21 @@
|
||||
# mirai-core-mock
|
||||
|
||||
mirai 模拟环境测试框架
|
||||
|
||||
> 模拟环境目前仅支持 JVM
|
||||
|
||||
--------------
|
||||
|
||||
# src 架构
|
||||
|
||||
- `contact` - 与 `mirai-core-api` 架构一致
|
||||
- `database` - 数据库, 用于存储一些临时的零碎数据
|
||||
- `resserver` - 资源服务
|
||||
- `userprofile` - 与 `UserProfile` 相关的一些服务
|
||||
- `utils` - 工具类
|
||||
|
||||
# test 架构
|
||||
|
||||
- `<toplevel>` 与 mirai-core-api 关系不大或者一些独立的组件的测试
|
||||
- `.mock` 模拟的各个部分的测试, 每个测试都继承 `MockBotTestBase`
|
||||
|
37
mirai-core-mock/build.gradle.kts
Normal file
37
mirai-core-mock/build.gradle.kts
Normal file
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
kotlin("plugin.serialization")
|
||||
`maven-publish`
|
||||
id("me.him188.kotlin-jvm-blocking-bridge")
|
||||
}
|
||||
|
||||
version = Versions.project
|
||||
description = "Mirai core mock testing framework"
|
||||
|
||||
kotlin {
|
||||
explicitApiWarning()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(project(":mirai-core-api"))
|
||||
implementation(project(":mirai-core-utils"))
|
||||
implementation(project(":mirai-core"))
|
||||
|
||||
implementation(`ktor-server-core`)
|
||||
implementation(`ktor-server-netty`)
|
||||
implementation(`java-in-memory-file-system`)
|
||||
|
||||
}
|
||||
|
||||
configurePublishing("mirai-core-mock")
|
||||
tasks.named("shadowJar") { enabled = false }
|
187
mirai-core-mock/src/MockActions.kt
Normal file
187
mirai-core-mock/src/MockActions.kt
Normal file
@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
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.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.mock.contact.MockFriend
|
||||
import net.mamoe.mirai.mock.contact.MockNormalMember
|
||||
import net.mamoe.mirai.mock.contact.MockStranger
|
||||
import net.mamoe.mirai.mock.contact.MockUserOrBot
|
||||
import net.mamoe.mirai.mock.database.removeMessageInfo
|
||||
import net.mamoe.mirai.mock.utils.NudgeDsl
|
||||
import net.mamoe.mirai.mock.utils.mock
|
||||
import net.mamoe.mirai.mock.utils.nudged0
|
||||
import net.mamoe.mirai.utils.cast
|
||||
|
||||
@JvmBlockingBridge
|
||||
public object MockActions {
|
||||
|
||||
/**
|
||||
* 修改 [MockUserOrBot.nick] 并广播相关事件 (如 [FriendNickChangedEvent])
|
||||
*/
|
||||
@JvmStatic
|
||||
public suspend fun fireNickChanged(target: MockUserOrBot, value: String) {
|
||||
when (target) {
|
||||
is MockFriend -> {
|
||||
val ov = target.nick
|
||||
target.mockApi.nick = value
|
||||
FriendNickChangedEvent(target, ov, target.nick).broadcast()
|
||||
}
|
||||
|
||||
is MockStranger -> {
|
||||
target.mockApi.nick = value
|
||||
// TODO: StrangerNickChangedEvent
|
||||
}
|
||||
|
||||
is MockNormalMember -> {
|
||||
val friend0 = target.bot.getFriend(target.id)
|
||||
if (friend0 != null) {
|
||||
return fireNickChanged(friend0, value)
|
||||
}
|
||||
target.mockApi.nick = value
|
||||
}
|
||||
|
||||
is MockBot -> {
|
||||
target.nick = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改 [MockNormalMember.nameCard] 并广播 [MemberCardChangeEvent]
|
||||
*/
|
||||
@JvmStatic
|
||||
public suspend fun fireNameCardChanged(member: MockNormalMember, value: String) {
|
||||
val ov = member.nameCard
|
||||
member.mockApi.nameCard = value
|
||||
MemberCardChangeEvent(ov, value, member).broadcast()
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改 [MockNormalMember.specialTitle] 并广播 [MemberSpecialTitleChangeEvent]
|
||||
*/
|
||||
@JvmStatic
|
||||
public suspend fun fireSpecialTitleChanged(member: MockNormalMember, value: String) {
|
||||
val ov = member.specialTitle
|
||||
member.mockApi.specialTitle = value
|
||||
MemberSpecialTitleChangeEvent(
|
||||
ov,
|
||||
value,
|
||||
member,
|
||||
operator = member.group.owner.takeIf { it.id != member.bot.id },
|
||||
).broadcast()
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改一名成员的权限并广播 [MemberPermissionChangeEvent]
|
||||
*/
|
||||
@JvmStatic
|
||||
public suspend fun firePermissionChanged(member: MockNormalMember, perm: MemberPermission) {
|
||||
if (perm == MemberPermission.OWNER || member == member.group.owner) {
|
||||
error("Use group.changeOwner to modify group owner")
|
||||
}
|
||||
val ov = member.permission
|
||||
member.mockApi.permission = perm
|
||||
if (member.id == member.bot.id) {
|
||||
BotGroupPermissionChangeEvent(member.group, ov, perm)
|
||||
} else {
|
||||
MemberPermissionChangeEvent(member, ov, perm)
|
||||
}.broadcast()
|
||||
}
|
||||
|
||||
/**
|
||||
* 令 [operator] 撤回一条消息
|
||||
*
|
||||
* @param operator 当 [operator] 为 null 时代表是发送者自己撤回
|
||||
*/
|
||||
@JvmStatic
|
||||
public suspend fun fireMessageRecalled(chain: MessageChain, operator: User? = null) {
|
||||
return fireMessageRecalled(chain.source, operator)
|
||||
}
|
||||
|
||||
/**
|
||||
* 令 [operator] 撤回一条消息
|
||||
*
|
||||
* @param operator 当 [operator] 为 null 时代表是发送者自己撤回
|
||||
*/
|
||||
@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
|
||||
}
|
||||
|
||||
is Friend -> {
|
||||
from.bot.mock().msgDatabase.removeMessageInfo(source)
|
||||
MessageRecallEvent.FriendRecall(
|
||||
source.bot,
|
||||
source.ids,
|
||||
source.internalIds,
|
||||
source.time,
|
||||
from.id,
|
||||
from.cast()
|
||||
).broadcast()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
error("Unsupported message source type: ${source.javaClass}")
|
||||
}
|
||||
|
||||
/**
|
||||
* 令 [operator] 撤回一条消息
|
||||
*
|
||||
* @param operator 当 [operator] 为 null 时代表是发送者自己撤回
|
||||
*/
|
||||
@JvmStatic
|
||||
public suspend fun mockFireRecalled(receipt: MessageReceipt<*>, operator: User? = null) {
|
||||
return fireMessageRecalled(receipt.source, operator)
|
||||
}
|
||||
|
||||
/**
|
||||
* 令 [actor] 戳一下 [actee]
|
||||
*
|
||||
* @param actor 发起戳一戳的人
|
||||
* @param actee 被戳的人
|
||||
*/
|
||||
@JvmStatic
|
||||
public suspend fun fireNudge(actor: MockUserOrBot, actee: MockUserOrBot, dsl: NudgeDsl) {
|
||||
actor.nudged0(actee, dsl)
|
||||
}
|
||||
|
||||
}
|
158
mirai-core-mock/src/MockBot.kt
Normal file
158
mirai-core-mock/src/MockBot.kt
Normal file
@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||
|
||||
package net.mamoe.mirai.mock
|
||||
|
||||
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.contact.ContactList
|
||||
import net.mamoe.mirai.event.broadcast
|
||||
import net.mamoe.mirai.event.events.BotAvatarChangedEvent
|
||||
import net.mamoe.mirai.event.events.BotOfflineEvent
|
||||
import net.mamoe.mirai.event.events.NewFriendRequestEvent
|
||||
import net.mamoe.mirai.message.data.Image
|
||||
import net.mamoe.mirai.message.data.OnlineAudio
|
||||
import net.mamoe.mirai.mock.contact.*
|
||||
import net.mamoe.mirai.mock.database.MessageDatabase
|
||||
import net.mamoe.mirai.mock.resserver.TmpResourceServer
|
||||
import net.mamoe.mirai.mock.userprofile.UserProfileService
|
||||
import net.mamoe.mirai.mock.utils.NameGenerator
|
||||
import net.mamoe.mirai.utils.ExternalResource
|
||||
import net.mamoe.mirai.utils.cast
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* 一个虚拟的机器人对象. 继承于 [Bot]
|
||||
*
|
||||
* @see MockBotFactory 构造 [MockBot] 的工厂, [MockBot] 的唯一构造方式
|
||||
*/
|
||||
@Suppress("unused")
|
||||
@JvmBlockingBridge
|
||||
public interface MockBot : Bot, MockContactOrBot, MockUserOrBot {
|
||||
override val bot: MockBot get() = this
|
||||
|
||||
/**
|
||||
* bot 昵称, 访问此字段时与 [nick] 一致
|
||||
* 修改此字段时不会广播事件
|
||||
*/
|
||||
@MockBotDSL
|
||||
public var nickNoEvent: String
|
||||
|
||||
/**
|
||||
* bot 昵称
|
||||
*
|
||||
* 修改此字段时会广播事件
|
||||
*/
|
||||
override var nick: String
|
||||
|
||||
/**
|
||||
* Bot 头像, 可自定义, 修改时会广播 [BotAvatarChangedEvent]
|
||||
*/
|
||||
@set:MockBotDSL
|
||||
override var avatarUrl: String
|
||||
|
||||
/// Contact API override
|
||||
override fun getFriend(id: Long): MockFriend? = super.getFriend(id)?.cast()
|
||||
|
||||
override fun getFriendOrFail(id: Long): MockFriend = super.getFriendOrFail(id).cast()
|
||||
|
||||
override fun getGroup(id: Long): MockGroup? = super.getGroup(id)?.cast()
|
||||
|
||||
override fun getGroupOrFail(id: Long): MockGroup = super.getGroupOrFail(id).cast()
|
||||
|
||||
override fun getStranger(id: Long): MockStranger? = super.getStranger(id)?.cast()
|
||||
|
||||
override fun getStrangerOrFail(id: Long): MockStranger = super.getStrangerOrFail(id).cast()
|
||||
|
||||
override val groups: ContactList<MockGroup>
|
||||
override val friends: ContactList<MockFriend>
|
||||
override val strangers: ContactList<MockStranger>
|
||||
override val otherClients: ContactList<MockOtherClient>
|
||||
override val asFriend: MockFriend
|
||||
override val asStranger: MockStranger
|
||||
|
||||
/// All mock api will not broadcast event
|
||||
|
||||
public val nameGenerator: NameGenerator
|
||||
public val tmpResourceServer: TmpResourceServer
|
||||
public val msgDatabase: MessageDatabase
|
||||
public val userProfileService: UserProfileService
|
||||
|
||||
/// Mock Contact API
|
||||
|
||||
@MockBotDSL
|
||||
public fun addGroup(id: Long, name: String): MockGroup
|
||||
|
||||
@MockBotDSL
|
||||
public fun addGroup(id: Long, uin: Long, name: String): MockGroup
|
||||
|
||||
@MockBotDSL
|
||||
public fun addFriend(id: Long, name: String): MockFriend
|
||||
|
||||
@MockBotDSL
|
||||
public fun addStranger(id: Long, name: String): MockStranger
|
||||
|
||||
/**
|
||||
* 将 [resource] 上传到 [临时资源服务器][tmpResourceServer],
|
||||
* 并返回一个 [OnlineAudio] 对象, 可用于测试语音接收
|
||||
*
|
||||
* @see MockUser.says
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun uploadOnlineAudio(resource: ExternalResource): OnlineAudio
|
||||
|
||||
/**
|
||||
* 将 [resource] 上传到 [临时资源服务器][tmpResourceServer]
|
||||
* 并返回一个 [Image] 对象, 可用于测试图片接收
|
||||
*
|
||||
* @see MockUser.says
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun uploadMockImage(resource: ExternalResource): Image
|
||||
|
||||
/**
|
||||
* 广播 [Bot] 掉线事件
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun broadcastOfflineEvent() {
|
||||
BotOfflineEvent.Dropped(this, java.net.SocketException("socket closed")).broadcast()
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播 [Bot] 头像更新事件
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun broadcastAvatarChangeEvent() {
|
||||
BotAvatarChangedEvent(this).broadcast()
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播新好友添加事件
|
||||
*
|
||||
* @see NewFriendRequestEvent
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun broadcastNewFriendRequestEvent(
|
||||
requester: Long,
|
||||
requesterNick: String,
|
||||
fromGroup: Long,
|
||||
message: String
|
||||
): NewFriendRequestEvent {
|
||||
return NewFriendRequestEvent(
|
||||
this,
|
||||
eventId = Random.nextLong(),
|
||||
fromId = requester,
|
||||
fromGroupId = fromGroup,
|
||||
message = message,
|
||||
fromNick = requesterNick
|
||||
).broadcast()
|
||||
}
|
||||
}
|
13
mirai-core-mock/src/MockBotDSL.kt
Normal file
13
mirai-core-mock/src/MockBotDSL.kt
Normal file
@ -0,0 +1,13 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock
|
||||
|
||||
@DslMarker
|
||||
public annotation class MockBotDSL
|
62
mirai-core-mock/src/MockBotFactory.kt
Normal file
62
mirai-core-mock/src/MockBotFactory.kt
Normal file
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock
|
||||
|
||||
import net.mamoe.mirai.BotFactory
|
||||
import net.mamoe.mirai.Mirai
|
||||
import net.mamoe.mirai.mock.database.MessageDatabase
|
||||
import net.mamoe.mirai.mock.internal.MockBotFactoryImpl
|
||||
import net.mamoe.mirai.mock.internal.MockMiraiImpl
|
||||
import net.mamoe.mirai.mock.resserver.TmpResourceServer
|
||||
import net.mamoe.mirai.mock.userprofile.UserProfileService
|
||||
import net.mamoe.mirai.mock.utils.NameGenerator
|
||||
import net.mamoe.mirai.utils.BotConfiguration
|
||||
|
||||
public interface MockBotFactory : BotFactory {
|
||||
|
||||
public interface BotBuilder {
|
||||
public fun id(value: Long): BotBuilder
|
||||
|
||||
public fun nick(value: String): BotBuilder
|
||||
|
||||
public fun configuration(value: BotConfiguration): BotBuilder
|
||||
|
||||
public fun nameGenerator(value: NameGenerator): BotBuilder
|
||||
|
||||
public fun tmpResourceServer(server: TmpResourceServer): BotBuilder
|
||||
|
||||
public fun msgDatabase(db: MessageDatabase): BotBuilder
|
||||
|
||||
public fun userProfileService(service: UserProfileService): BotBuilder
|
||||
|
||||
public fun create(): MockBot
|
||||
|
||||
public fun createNoInstanceRegister(): MockBot
|
||||
}
|
||||
|
||||
public fun newMockBotBuilder(): BotBuilder
|
||||
|
||||
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||
public companion object : MockBotFactory by MockBotFactoryImpl() {
|
||||
init {
|
||||
Mirai
|
||||
net.mamoe.mirai._MiraiInstance.set(MockMiraiImpl())
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun initialize() {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public inline fun MockBotFactory.BotBuilder.configuration(
|
||||
block: BotConfiguration.() -> Unit
|
||||
): MockBotFactory.BotBuilder = configuration(BotConfiguration(block))
|
14
mirai-core-mock/src/contact/MockAnonymousMember.kt
Normal file
14
mirai-core-mock/src/contact/MockAnonymousMember.kt
Normal file
@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.contact
|
||||
|
||||
import net.mamoe.mirai.contact.AnonymousMember
|
||||
|
||||
public interface MockAnonymousMember : AnonymousMember, MockMember
|
33
mirai-core-mock/src/contact/MockContact.kt
Normal file
33
mirai-core-mock/src/contact/MockContact.kt
Normal file
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.contact
|
||||
|
||||
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
|
||||
import net.mamoe.mirai.contact.Contact
|
||||
import net.mamoe.mirai.mock.MockBotDSL
|
||||
|
||||
@JvmBlockingBridge
|
||||
public interface MockContact : Contact, MockContactOrBot {
|
||||
public interface MockApi {
|
||||
public var avatarUrl: String
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取直接修改字段内容的 API, 通过该 API 修改的值都不会触发广播
|
||||
*/
|
||||
@MockBotDSL
|
||||
public val mockApi: MockApi
|
||||
|
||||
/**
|
||||
* 修改 [avatarUrl] 的地址, 同时会广播相关事件 (如果有)
|
||||
*/
|
||||
@MockBotDSL
|
||||
public fun changeAvatarUrl(newAvatar: String)
|
||||
}
|
17
mirai-core-mock/src/contact/MockContactOrBot.kt
Normal file
17
mirai-core-mock/src/contact/MockContactOrBot.kt
Normal file
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.contact
|
||||
|
||||
import net.mamoe.mirai.contact.ContactOrBot
|
||||
import net.mamoe.mirai.mock.MockBot
|
||||
|
||||
public interface MockContactOrBot : ContactOrBot {
|
||||
override val bot: MockBot
|
||||
}
|
90
mirai-core-mock/src/contact/MockFriend.kt
Normal file
90
mirai-core-mock/src/contact/MockFriend.kt
Normal file
@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.contact
|
||||
|
||||
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
|
||||
import net.mamoe.mirai.contact.Friend
|
||||
import net.mamoe.mirai.event.broadcast
|
||||
import net.mamoe.mirai.event.events.BotInvitedJoinGroupRequestEvent
|
||||
import net.mamoe.mirai.event.events.FriendAddEvent
|
||||
import net.mamoe.mirai.event.events.FriendInputStatusChangedEvent
|
||||
import net.mamoe.mirai.mock.MockBotDSL
|
||||
import kotlin.random.Random
|
||||
|
||||
@JvmBlockingBridge
|
||||
public interface MockFriend : Friend, MockContact, MockUser, MockMsgSyncSupport {
|
||||
public interface MockApi : MockContact.MockApi {
|
||||
public val contact: MockFriend
|
||||
public var nick: String
|
||||
public var remark: String
|
||||
public var friendGroupId: Int
|
||||
public override var avatarUrl: String
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取直接修改字段内容的 API, 通过该 API 修改的值都不会触发广播
|
||||
*/
|
||||
@MockBotDSL
|
||||
public override val mockApi: MockApi
|
||||
|
||||
/**
|
||||
* 修改 nick 同时广播相关事件
|
||||
*/
|
||||
override var nick: String
|
||||
|
||||
/**
|
||||
* 修改 remark 同时广播相关事件
|
||||
*/
|
||||
override var remark: String
|
||||
|
||||
/**
|
||||
* 广播好友添加事件
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun broadcastFriendAddEvent(): FriendAddEvent {
|
||||
return FriendAddEvent(this).broadcast()
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播好友邀请 [bot] 加入一个群聊的事件
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun broadcastInviteBotJoinGroupRequestEvent(
|
||||
groupId: Long, groupName: String,
|
||||
): BotInvitedJoinGroupRequestEvent {
|
||||
return BotInvitedJoinGroupRequestEvent(
|
||||
bot,
|
||||
Random.nextLong(),
|
||||
id,
|
||||
groupId,
|
||||
groupName,
|
||||
nick
|
||||
).broadcast()
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播好友主动删除 [bot] 好友的事件
|
||||
*
|
||||
* 即使该函数体实现为 [delete], 也请使用该方法广播 **bot 被好友删除**,
|
||||
* 以确保不会受到未来的事件架构变更带来的影响
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun broadcastFriendDelete() {
|
||||
delete()
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播好友输入状态改变事件
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun broadcastFriendInputStateChange(inputting: Boolean) {
|
||||
FriendInputStatusChangedEvent(this, inputting).broadcast()
|
||||
}
|
||||
}
|
147
mirai-core-mock/src/contact/MockGroup.kt
Normal file
147
mirai-core-mock/src/contact/MockGroup.kt
Normal file
@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
|
||||
package net.mamoe.mirai.mock.contact
|
||||
|
||||
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
|
||||
import net.mamoe.mirai.contact.ContactList
|
||||
import net.mamoe.mirai.contact.Group
|
||||
import net.mamoe.mirai.contact.NormalMember
|
||||
import net.mamoe.mirai.data.GroupHonorType
|
||||
import net.mamoe.mirai.data.MemberInfo
|
||||
import net.mamoe.mirai.event.broadcast
|
||||
import net.mamoe.mirai.event.events.MemberHonorChangeEvent
|
||||
import net.mamoe.mirai.event.events.MemberJoinRequestEvent
|
||||
import net.mamoe.mirai.mock.MockBot
|
||||
import net.mamoe.mirai.mock.MockBotDSL
|
||||
import net.mamoe.mirai.mock.contact.announcement.MockAnnouncements
|
||||
import net.mamoe.mirai.mock.userprofile.MockMemberInfoBuilder
|
||||
import net.mamoe.mirai.utils.cast
|
||||
import kotlin.random.Random
|
||||
|
||||
@JvmBlockingBridge
|
||||
public interface MockGroup : Group, MockContact, MockMsgSyncSupport {
|
||||
/** @see net.mamoe.mirai.IMirai.getUin */
|
||||
public val uin: Long
|
||||
override val bot: MockBot
|
||||
override val members: ContactList<MockNormalMember>
|
||||
override val owner: MockNormalMember
|
||||
override val botAsMember: MockNormalMember
|
||||
override val avatarUrl: String
|
||||
override val announcements: MockAnnouncements
|
||||
|
||||
public interface MockApi : MockContact.MockApi {
|
||||
override var avatarUrl: String
|
||||
}
|
||||
|
||||
override val mockApi: MockApi
|
||||
|
||||
/**
|
||||
* 群荣耀, 可直接修改此属性, 修改此属性不会广播相关事件
|
||||
*
|
||||
* @see changeHonorMember
|
||||
*/
|
||||
@MockBotDSL
|
||||
public val honorMembers: MutableMap<GroupHonorType, MockNormalMember>
|
||||
|
||||
/**
|
||||
* 更改拥有群荣耀的群成员.
|
||||
*
|
||||
* 会自动广播 [MemberHonorChangeEvent.Achieve] 和 [MemberHonorChangeEvent.Lose] 等相关事件.
|
||||
*
|
||||
* 此外如果 [honorType] 是 [GroupHonorType.TALKATIVE],
|
||||
* 会额外广播 [net.mamoe.mirai.event.events.GroupTalkativeChangeEvent].
|
||||
*
|
||||
* 如果不需要广播事件, 可直接更改 [MockGroup.honorMembers]
|
||||
*/
|
||||
@MockBotDSL
|
||||
public fun changeHonorMember(member: MockNormalMember, honorType: GroupHonorType)
|
||||
|
||||
/**
|
||||
* 获取群控制面板
|
||||
*
|
||||
* 注, 通过本属性获取的控制面板为原始数据存储面板, 修改并不会广播相关事件, 如果需要广播事件,
|
||||
* 请使用 [MockGroupControlPane.withActor]
|
||||
*/
|
||||
@MockBotDSL
|
||||
public val controlPane: MockGroupControlPane
|
||||
|
||||
/** 添加一位成员, 该操作不会广播任何事件
|
||||
* @see MockMemberInfoBuilder
|
||||
*/
|
||||
@MockBotDSL
|
||||
public fun appendMember(mockMember: MemberInfo): MockGroup // chain call
|
||||
|
||||
/**
|
||||
* 添加一位成员, 该操作不会广播任何事件
|
||||
* @see MockMemberInfoBuilder
|
||||
*/
|
||||
@MockBotDSL
|
||||
public fun addMember(mockMember: MemberInfo): MockNormalMember
|
||||
|
||||
/** 添加一位成员, 该操作不会广播任何事件
|
||||
*/
|
||||
@MockBotDSL
|
||||
public fun appendMember(uin: Long, nick: String): MockGroup =
|
||||
appendMember(MockMemberInfoBuilder.create { uin(uin).nick(nick) })
|
||||
|
||||
/** 添加一位成员, 该操作不会广播任何事件
|
||||
* @see MockMemberInfoBuilder
|
||||
*/
|
||||
@MockBotDSL
|
||||
public fun addMember(uin: Long, nick: String): MockNormalMember =
|
||||
addMember(MockMemberInfoBuilder.create { uin(uin).nick(nick) })
|
||||
|
||||
|
||||
/**
|
||||
* 修改群主, 该操作会广播群转让的相关事件
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun changeOwner(member: NormalMember)
|
||||
|
||||
/**
|
||||
* 修改群主, 该操作不会广播任何事件
|
||||
*/
|
||||
@MockBotDSL
|
||||
public fun changeOwnerNoEventBroadcast(member: NormalMember)
|
||||
|
||||
/**
|
||||
* 创建新的匿名群成员.
|
||||
*
|
||||
* @param id 该匿名群成员的 id, 可自定义, 建议使用 ASCII 纯文本
|
||||
*/
|
||||
@MockBotDSL
|
||||
public fun newAnonymous(nick: String, id: String): MockAnonymousMember
|
||||
|
||||
override fun get(id: Long): MockNormalMember?
|
||||
override fun getOrFail(id: Long): MockNormalMember = super.getOrFail(id).cast()
|
||||
|
||||
/**
|
||||
* 主动广播有新成员申请加入的事件
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun broadcastNewMemberJoinRequestEvent(
|
||||
requester: Long,
|
||||
requesterName: String,
|
||||
message: String,
|
||||
invitor: Long = 0L,
|
||||
): MemberJoinRequestEvent {
|
||||
return MemberJoinRequestEvent(
|
||||
bot, Random.nextLong(),
|
||||
message,
|
||||
requester,
|
||||
this.id,
|
||||
this.name,
|
||||
requesterName,
|
||||
invitor.takeIf { it != 0L },
|
||||
).broadcast()
|
||||
}
|
||||
}
|
||||
|
43
mirai-core-mock/src/contact/MockGroupControlPane.kt
Normal file
43
mirai-core-mock/src/contact/MockGroupControlPane.kt
Normal file
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.contact
|
||||
|
||||
/**
|
||||
* 群设置面板, 如果是由 [withActor] 得到的面板在操作的同时会进行事件广播
|
||||
*
|
||||
* 与 [MockGroup.settings] 不同的是, 该控制面板不会进行权限校检
|
||||
*/
|
||||
public interface MockGroupControlPane {
|
||||
public val group: MockGroup
|
||||
|
||||
/**
|
||||
* 如果为 [MockGroup.controlPane] 获得的原始控制面板, 此属性为 [MockGroup.botAsMember]
|
||||
*
|
||||
* @see withActor
|
||||
*/
|
||||
public val currentActor: MockNormalMember
|
||||
|
||||
public var isAllowMemberInvite: Boolean
|
||||
|
||||
public var isMuteAll: Boolean
|
||||
|
||||
public var isAllowMemberFileUploading: Boolean
|
||||
|
||||
public var isAnonymousChatAllowed: Boolean
|
||||
|
||||
public var isAllowConfessTalk: Boolean
|
||||
|
||||
public var groupName: String
|
||||
|
||||
/**
|
||||
* 通过 [withActor] 得到的 [MockGroupControlPane] 在修改属性的同时会广播相关事件
|
||||
*/
|
||||
public fun withActor(actor: MockNormalMember): MockGroupControlPane
|
||||
}
|
33
mirai-core-mock/src/contact/MockMember.kt
Normal file
33
mirai-core-mock/src/contact/MockMember.kt
Normal file
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.contact
|
||||
|
||||
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
|
||||
import net.mamoe.mirai.contact.Member
|
||||
import net.mamoe.mirai.contact.MemberPermission
|
||||
import net.mamoe.mirai.mock.MockBotDSL
|
||||
|
||||
@JvmBlockingBridge
|
||||
public interface MockMember : Member, MockContact, MockUser {
|
||||
public interface MockApi : MockContact.MockApi {
|
||||
public val member: MockMember
|
||||
public var nick: String
|
||||
public var remark: String
|
||||
public var permission: MemberPermission
|
||||
}
|
||||
|
||||
override val group: MockGroup
|
||||
|
||||
/**
|
||||
* 获取直接修改字段内容的 API, 通过该 API 修改的值都不会触发广播
|
||||
*/
|
||||
@MockBotDSL
|
||||
public override val mockApi: MockApi
|
||||
}
|
21
mirai-core-mock/src/contact/MockMsgSyncSupport.kt
Normal file
21
mirai-core-mock/src/contact/MockMsgSyncSupport.kt
Normal file
@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.contact
|
||||
|
||||
import net.mamoe.mirai.message.data.MessageChain
|
||||
import net.mamoe.mirai.mock.MockBotDSL
|
||||
|
||||
public interface MockMsgSyncSupport : MockContact {
|
||||
/**
|
||||
* 广播消息同步事件
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun broadcastMsgSyncEvent(message: MessageChain, time: Int)
|
||||
}
|
107
mirai-core-mock/src/contact/MockNormalMember.kt
Normal file
107
mirai-core-mock/src/contact/MockNormalMember.kt
Normal file
@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.contact
|
||||
|
||||
import kotlinx.coroutines.cancel
|
||||
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
|
||||
import net.mamoe.mirai.contact.NormalMember
|
||||
import net.mamoe.mirai.event.broadcast
|
||||
import net.mamoe.mirai.event.events.BotLeaveEvent
|
||||
import net.mamoe.mirai.event.events.MemberJoinEvent
|
||||
import net.mamoe.mirai.event.events.MemberLeaveEvent
|
||||
import net.mamoe.mirai.mock.MockBotDSL
|
||||
import net.mamoe.mirai.mock.utils.broadcastBlocking
|
||||
import java.util.concurrent.CancellationException
|
||||
|
||||
@JvmBlockingBridge
|
||||
public interface MockNormalMember : NormalMember, MockMember {
|
||||
public interface MockApi : MockMember.MockApi {
|
||||
override val member: MockNormalMember
|
||||
public var lastSpeakTimestamp: Int
|
||||
public var joinTimestamp: Int
|
||||
public var nameCard: String
|
||||
public var specialTitle: String
|
||||
|
||||
/**
|
||||
* 单位 秒
|
||||
*/
|
||||
public var muteTimeEndTimestamp: Long
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取直接修改字段内容的 API, 通过该 API 修改的值都不会触发广播
|
||||
*/
|
||||
@MockBotDSL
|
||||
override val mockApi: MockApi
|
||||
|
||||
/**
|
||||
* 广播该成员加入了群
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun broadcastMemberJoinEvent() {
|
||||
broadcastMemberJoinEvent(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播该成员加入了群
|
||||
*
|
||||
* @param invitor 邀请者, 当邀请者不为 `null` 时广播 [MemberJoinEvent.Invite]
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun broadcastMemberJoinEvent(invitor: NormalMember?) {
|
||||
if (invitor == null) {
|
||||
MemberJoinEvent.Active(this)
|
||||
} else {
|
||||
MemberJoinEvent.Invite(this, invitor)
|
||||
}.broadcast()
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播该群员主动离开了群, 此方法同时会在 [group] 中移除此成员
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun broadcastMemberLeave() {
|
||||
if (group.members.delegate.remove(this)) {
|
||||
MemberLeaveEvent.Quit(this).broadcast()
|
||||
cancel(CancellationException("Member $id left"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播该群员将 [bot] 踢出了群聊, 并同时在 [bot] 的群聊列表里删除该群
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun broadcastKickBot() {
|
||||
if (bot.groups.delegate.remove(group)) {
|
||||
BotLeaveEvent.Kick(this).broadcast()
|
||||
cancel(CancellationException("Bot was kicked"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播 该群成员被 [actor] 踢出, 此方法同时会在 [group] 中移除此成员
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun broadcastKickedBy(actor: MockNormalMember) {
|
||||
if (group.members.delegate.remove(this)) {
|
||||
MemberLeaveEvent.Kick(this, actor).broadcastBlocking()
|
||||
cancel(CancellationException("Member $id kicked"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播该群员 禁言了 [target], 此方法没有权限校检
|
||||
*
|
||||
* @param durationSeconds 0 为取消禁言
|
||||
* @param target 被禁言群成员
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun broadcastMute(target: MockNormalMember, durationSeconds: Int)
|
||||
}
|
14
mirai-core-mock/src/contact/MockOtherClient.kt
Normal file
14
mirai-core-mock/src/contact/MockOtherClient.kt
Normal file
@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.contact
|
||||
|
||||
import net.mamoe.mirai.contact.OtherClient
|
||||
|
||||
public interface MockOtherClient : OtherClient, MockContact
|
62
mirai-core-mock/src/contact/MockStranger.kt
Normal file
62
mirai-core-mock/src/contact/MockStranger.kt
Normal file
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.contact
|
||||
|
||||
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
|
||||
import net.mamoe.mirai.contact.Stranger
|
||||
import net.mamoe.mirai.event.broadcast
|
||||
import net.mamoe.mirai.event.events.StrangerAddEvent
|
||||
import net.mamoe.mirai.event.events.StrangerRelationChangeEvent
|
||||
import net.mamoe.mirai.mock.MockBotDSL
|
||||
|
||||
@JvmBlockingBridge
|
||||
public interface MockStranger : Stranger, MockContact, MockUser {
|
||||
public interface MockApi : MockContact.MockApi {
|
||||
public val contact: MockStranger
|
||||
public var nick: String
|
||||
public var remark: String
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播陌生人加入
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun broadcastStrangerAddEvent(): StrangerAddEvent {
|
||||
return StrangerAddEvent(this).broadcast()
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加为好友
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun addAsFriend() {
|
||||
this.bot.addFriend(this.id, this.nick)
|
||||
bot.strangers.delegate.remove(this)
|
||||
StrangerRelationChangeEvent.Friended(this, bot.getFriend(this.id)!!).broadcast()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取直接修改字段内容的 API, 通过该 API 修改的值都不会触发广播
|
||||
*/
|
||||
@MockBotDSL
|
||||
public override val mockApi: MockApi
|
||||
|
||||
/**
|
||||
* 广播陌生人主动解除与 [bot] 的关系的事件
|
||||
*
|
||||
* @suppress
|
||||
* 即使该函数体实现为 [delete], 也请使用该方法广播 **bot 被陌生人删除**,
|
||||
* 以确保不会受到未来的事件架构变更带来的影响
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun broadcastStrangerDeleteEvent() {
|
||||
delete()
|
||||
}
|
||||
}
|
104
mirai-core-mock/src/contact/MockUser.kt
Normal file
104
mirai-core-mock/src/contact/MockUser.kt
Normal file
@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||
|
||||
package net.mamoe.mirai.mock.contact
|
||||
|
||||
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
|
||||
import net.mamoe.mirai.contact.User
|
||||
import net.mamoe.mirai.event.events.GroupMessageEvent
|
||||
import net.mamoe.mirai.message.MessageReceipt
|
||||
import net.mamoe.mirai.message.data.*
|
||||
import net.mamoe.mirai.mock.MockActions
|
||||
import net.mamoe.mirai.mock.MockActions.mockFireRecalled
|
||||
import net.mamoe.mirai.mock.MockBotDSL
|
||||
import net.mamoe.mirai.mock.utils.broadcastMockEvents
|
||||
import net.mamoe.mirai.utils.JavaFriendlyAPI
|
||||
import java.util.function.Consumer
|
||||
import java.util.function.Supplier
|
||||
import kotlin.internal.LowPriorityInOverloadResolution
|
||||
|
||||
@JvmBlockingBridge
|
||||
public interface MockUser : MockContact, MockUserOrBot, User {
|
||||
/**
|
||||
* 令 [MockUserOrBot] 撤回一条消息
|
||||
*
|
||||
* @see [mockFireRecalled]
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun recallMessage(message: MessageChain) {
|
||||
broadcastMockEvents {
|
||||
message.recalledBy(this@MockUser)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 令 [MockUserOrBot] 撤回一条消息
|
||||
*
|
||||
* @see [mockFireRecalled]
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun recallMessage(message: MessageSource) {
|
||||
broadcastMockEvents {
|
||||
message.recalledBy(this@MockUser)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 令 [MockUserOrBot] 撤回一条消息
|
||||
*
|
||||
* @see [mockFireRecalled]
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun recallMessage(message: MessageReceipt<*>) {
|
||||
mockFireRecalled(message, this)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 令 [MockContact] 发出一条信息, 并广播相关的消息事件 (如 [GroupMessageEvent])
|
||||
*
|
||||
* @return 返回 [MockContact] 发出的消息 (包含 [MessageSource]),
|
||||
* 可用于测试消息发出后马上撤回 `says().recall()`
|
||||
*
|
||||
* @see [MockActions.mockFireRecalled]
|
||||
* @see [MockUser.recallMessage]
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun says(message: MessageChain): MessageChain
|
||||
|
||||
|
||||
@MockBotDSL
|
||||
public suspend fun says(message: Message): MessageChain {
|
||||
return says(message.toMessageChain())
|
||||
}
|
||||
|
||||
@MockBotDSL
|
||||
public suspend fun says(message: String): MessageChain {
|
||||
return says(PlainText(message))
|
||||
}
|
||||
|
||||
@JavaFriendlyAPI
|
||||
@LowPriorityInOverloadResolution
|
||||
public suspend fun says(message: Consumer<MessageChainBuilder>): MessageChain {
|
||||
return says(buildMessageChain { message.accept(this) })
|
||||
}
|
||||
|
||||
|
||||
@JavaFriendlyAPI
|
||||
@LowPriorityInOverloadResolution
|
||||
public suspend fun says(message: Supplier<Message>): MessageChain {
|
||||
return says(message.get())
|
||||
}
|
||||
|
||||
public suspend fun says(message: suspend MessageChainBuilder.() -> Unit): MessageChain {
|
||||
return says(buildMessageChain { message(this) })
|
||||
}
|
||||
}
|
18
mirai-core-mock/src/contact/MockUserOrBot.kt
Normal file
18
mirai-core-mock/src/contact/MockUserOrBot.kt
Normal file
@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||
|
||||
package net.mamoe.mirai.mock.contact
|
||||
|
||||
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
|
||||
import net.mamoe.mirai.contact.UserOrBot
|
||||
|
||||
@JvmBlockingBridge
|
||||
public interface MockUserOrBot : MockContactOrBot, UserOrBot
|
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.contact.announcement
|
||||
|
||||
import net.mamoe.mirai.contact.Group
|
||||
import net.mamoe.mirai.contact.NormalMember
|
||||
import net.mamoe.mirai.contact.announcement.*
|
||||
import net.mamoe.mirai.mock.MockBotDSL
|
||||
import net.mamoe.mirai.utils.MiraiInternalApi
|
||||
|
||||
public interface MockAnnouncements : Announcements {
|
||||
/**
|
||||
* 直接以 [actor] 的身份推送一则公告
|
||||
*
|
||||
* @param events 当为 `true` 时会广播相关事件
|
||||
* @param announcement 见 [OfflineAnnouncement], [OfflineAnnouncement.create]
|
||||
*/
|
||||
@MockBotDSL
|
||||
public fun mockPublish(
|
||||
announcement: Announcement,
|
||||
actor: NormalMember,
|
||||
events: Boolean
|
||||
): OnlineAnnouncement
|
||||
|
||||
@MockBotDSL
|
||||
public fun mockPublish(
|
||||
announcement: Announcement,
|
||||
actor: NormalMember,
|
||||
): OnlineAnnouncement = mockPublish(announcement, actor, false)
|
||||
}
|
||||
|
||||
public class MockOnlineAnnouncement @MiraiInternalApi public constructor(
|
||||
override val content: String,
|
||||
override val parameters: AnnouncementParameters,
|
||||
override val senderId: Long,
|
||||
override val fid: String = "",
|
||||
override val allConfirmed: Boolean,
|
||||
override val confirmedMembersCount: Int,
|
||||
override val publicationTime: Long
|
||||
) : OnlineAnnouncement {
|
||||
|
||||
override lateinit var group: Group
|
||||
override val sender: NormalMember? get() = group[senderId]
|
||||
}
|
||||
|
||||
internal fun MockOnlineAnnouncement.copy(
|
||||
content: String = this.content,
|
||||
parameters: AnnouncementParameters = this.parameters,
|
||||
senderId: Long = this.senderId,
|
||||
fid: String = this.fid,
|
||||
allConfirmed: Boolean = this.allConfirmed,
|
||||
confirmedMembersCount: Int = this.confirmedMembersCount,
|
||||
publicationTime: Long = this.publicationTime,
|
||||
): MockOnlineAnnouncement = MockOnlineAnnouncement(
|
||||
content = content,
|
||||
parameters = parameters,
|
||||
senderId = senderId,
|
||||
fid = fid,
|
||||
allConfirmed = allConfirmed,
|
||||
confirmedMembersCount = confirmedMembersCount,
|
||||
publicationTime = publicationTime,
|
||||
)
|
104
mirai-core-mock/src/database/MessageDatabase.kt
Normal file
104
mirai-core-mock/src/database/MessageDatabase.kt
Normal file
@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.database
|
||||
|
||||
import net.mamoe.mirai.contact.Contact
|
||||
import net.mamoe.mirai.contact.roaming.RoamingMessageFilter
|
||||
import net.mamoe.mirai.message.data.MessageChain
|
||||
import net.mamoe.mirai.message.data.MessageSource
|
||||
import net.mamoe.mirai.message.data.MessageSourceKind
|
||||
import net.mamoe.mirai.mock.MockBot
|
||||
import net.mamoe.mirai.mock.internal.db.MsgDatabaseImpl
|
||||
import net.mamoe.mirai.utils.concatAsLong
|
||||
|
||||
/**
|
||||
* 一个消息数据库
|
||||
*
|
||||
* 该数据库用于存储发送者, 发送目标, 发送类型 等数据,
|
||||
* 用于支持 撤回/消息获取 等相关的功能的实现
|
||||
*
|
||||
* 一般在测试结束后销毁整个数据库
|
||||
*/
|
||||
public interface MessageDatabase {
|
||||
/**
|
||||
* implementation note: 该方法可能同时被多个线程同时调用
|
||||
*
|
||||
* @param time 单位秒
|
||||
*/
|
||||
public fun newMessageInfo(
|
||||
sender: Long, subject: Long, kind: MessageSourceKind,
|
||||
time: Long,
|
||||
message: MessageChain,
|
||||
): MessageInfo
|
||||
|
||||
public fun queryMessageInfo(msgId: Long): MessageInfo?
|
||||
|
||||
public fun queryMessageInfosBy(
|
||||
subject: Long, kind: MessageSourceKind,
|
||||
contact: Contact,
|
||||
timeStart: Long,
|
||||
timeEnd: Long,
|
||||
filter: RoamingMessageFilter
|
||||
): Sequence<MessageInfo>
|
||||
|
||||
/**
|
||||
* implementation note: 该方法可能同时被多个线程同时调用
|
||||
*/
|
||||
public fun removeMessageInfo(msgId: Long)
|
||||
|
||||
/**
|
||||
* 断开与数据库的连接, 在 [MockBot.close] 时会自动调用
|
||||
*/
|
||||
public fun disconnect()
|
||||
|
||||
/**
|
||||
* 建立与数据库的连接, 在 [MockBot] 构造后马上调用,
|
||||
* 抛出任何错误都会中断 [MockBot] 的初始化
|
||||
*/
|
||||
public fun connect()
|
||||
|
||||
public companion object {
|
||||
@JvmStatic
|
||||
public fun newDefaultDatabase(): MessageDatabase {
|
||||
return MsgDatabaseImpl()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public data class MessageInfo(
|
||||
public val mixinedMsgId: Long,
|
||||
public val sender: Long,
|
||||
public val subject: Long,
|
||||
public val kind: MessageSourceKind,
|
||||
public val time: Long, // seconds
|
||||
public val message: MessageChain,
|
||||
) {
|
||||
// ids
|
||||
public val id: Int get() = (mixinedMsgId shr 32).toInt()
|
||||
|
||||
// internalIds
|
||||
public val internal: Int get() = mixinedMsgId.toInt()
|
||||
}
|
||||
|
||||
public fun mockMsgDatabaseId(id: Int, internalId: Int): Long {
|
||||
return id.concatAsLong(internalId)
|
||||
}
|
||||
|
||||
public fun MessageDatabase.removeMessageInfo(id: Int, internalId: Int) {
|
||||
removeMessageInfo(mockMsgDatabaseId(id, internalId))
|
||||
}
|
||||
|
||||
public fun MessageDatabase.queryMessageInfo(ids: IntArray, internalIds: IntArray): MessageInfo? {
|
||||
return queryMessageInfo(mockMsgDatabaseId(ids[0], internalIds[0]))
|
||||
}
|
||||
|
||||
public fun MessageDatabase.removeMessageInfo(source: MessageSource) {
|
||||
removeMessageInfo(source.ids[0], source.internalIds[0])
|
||||
}
|
105
mirai-core-mock/src/internal/MockBotFactoryImpl.kt
Normal file
105
mirai-core-mock/src/internal/MockBotFactoryImpl.kt
Normal file
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.internal
|
||||
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.mock.MockBot
|
||||
import net.mamoe.mirai.mock.MockBotFactory
|
||||
import net.mamoe.mirai.mock.database.MessageDatabase
|
||||
import net.mamoe.mirai.mock.resserver.TmpResourceServer
|
||||
import net.mamoe.mirai.mock.userprofile.UserProfileService
|
||||
import net.mamoe.mirai.mock.utils.NameGenerator
|
||||
import net.mamoe.mirai.utils.BotConfiguration
|
||||
import net.mamoe.mirai.utils.lateinitMutableProperty
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.random.Random
|
||||
|
||||
internal class MockBotFactoryImpl : MockBotFactory {
|
||||
override fun newMockBotBuilder(): MockBotFactory.BotBuilder {
|
||||
return object : MockBotFactory.BotBuilder {
|
||||
var id: Long = Random.nextLong().absoluteValue
|
||||
var nick_: String by lateinitMutableProperty {
|
||||
"Mock Bot $id"
|
||||
}
|
||||
var configuration_: BotConfiguration by lateinitMutableProperty { BotConfiguration { } }
|
||||
var nameGenerator: NameGenerator = NameGenerator.getDefault()
|
||||
var tmpResourceServer_: TmpResourceServer by lateinitMutableProperty {
|
||||
TmpResourceServer.newInMemoryTmpResourceServer()
|
||||
}
|
||||
var msgDb: MessageDatabase by lateinitMutableProperty {
|
||||
MessageDatabase.newDefaultDatabase()
|
||||
}
|
||||
var userProfileService: UserProfileService by lateinitMutableProperty {
|
||||
UserProfileService.getInstance()
|
||||
}
|
||||
|
||||
override fun id(value: Long): MockBotFactory.BotBuilder = apply {
|
||||
this.id = value
|
||||
}
|
||||
|
||||
override fun nick(value: String): MockBotFactory.BotBuilder = apply {
|
||||
this.nick_ = value
|
||||
}
|
||||
|
||||
override fun configuration(value: BotConfiguration): MockBotFactory.BotBuilder = apply {
|
||||
this.configuration_ = value
|
||||
}
|
||||
|
||||
override fun nameGenerator(value: NameGenerator): MockBotFactory.BotBuilder = apply {
|
||||
this.nameGenerator = value
|
||||
}
|
||||
|
||||
override fun tmpResourceServer(server: TmpResourceServer): MockBotFactory.BotBuilder = apply {
|
||||
tmpResourceServer_ = server
|
||||
}
|
||||
|
||||
override fun msgDatabase(db: MessageDatabase): MockBotFactory.BotBuilder = apply {
|
||||
msgDb = db
|
||||
}
|
||||
|
||||
override fun userProfileService(service: UserProfileService): MockBotFactory.BotBuilder = apply {
|
||||
userProfileService = service
|
||||
}
|
||||
|
||||
override fun createNoInstanceRegister(): MockBot {
|
||||
return MockBotImpl(
|
||||
configuration_,
|
||||
id,
|
||||
nick_,
|
||||
nameGenerator,
|
||||
tmpResourceServer_,
|
||||
msgDb,
|
||||
userProfileService,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("INVISIBLE_MEMBER")
|
||||
override fun create(): MockBot {
|
||||
return createNoInstanceRegister().also {
|
||||
Bot._instances[id] = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun newBot(qq: Long, password: String, configuration: BotConfiguration): Bot {
|
||||
return newMockBotBuilder()
|
||||
.id(qq)
|
||||
.configuration(configuration)
|
||||
.create()
|
||||
}
|
||||
|
||||
override fun newBot(qq: Long, passwordMd5: ByteArray, configuration: BotConfiguration): Bot {
|
||||
return newMockBotBuilder()
|
||||
.id(qq)
|
||||
.configuration(configuration)
|
||||
.create()
|
||||
}
|
||||
}
|
189
mirai-core-mock/src/internal/MockBotImpl.kt
Normal file
189
mirai-core-mock/src/internal/MockBotImpl.kt
Normal file
@ -0,0 +1,189 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "CANNOT_OVERRIDE_INVISIBLE_MEMBER")
|
||||
|
||||
package net.mamoe.mirai.mock.internal
|
||||
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.isActive
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.Mirai
|
||||
import net.mamoe.mirai.contact.AvatarSpec
|
||||
import net.mamoe.mirai.contact.ContactList
|
||||
import net.mamoe.mirai.contact.ContactOrBot
|
||||
import net.mamoe.mirai.contact.MemberPermission
|
||||
import net.mamoe.mirai.contact.friendgroup.FriendGroups
|
||||
import net.mamoe.mirai.event.EventChannel
|
||||
import net.mamoe.mirai.event.GlobalEventChannel
|
||||
import net.mamoe.mirai.event.broadcast
|
||||
import net.mamoe.mirai.event.events.*
|
||||
import net.mamoe.mirai.internal.network.component.ComponentStorage
|
||||
import net.mamoe.mirai.internal.network.component.ConcurrentComponentStorage
|
||||
import net.mamoe.mirai.internal.network.components.EventDispatcher
|
||||
import net.mamoe.mirai.message.data.OnlineAudio
|
||||
import net.mamoe.mirai.mock.MockBot
|
||||
import net.mamoe.mirai.mock.contact.MockFriend
|
||||
import net.mamoe.mirai.mock.contact.MockGroup
|
||||
import net.mamoe.mirai.mock.contact.MockOtherClient
|
||||
import net.mamoe.mirai.mock.contact.MockStranger
|
||||
import net.mamoe.mirai.mock.database.MessageDatabase
|
||||
import net.mamoe.mirai.mock.internal.components.MockEventDispatcherImpl
|
||||
import net.mamoe.mirai.mock.internal.contact.*
|
||||
import net.mamoe.mirai.mock.internal.contact.friendfroup.MockFriendGroups
|
||||
import net.mamoe.mirai.mock.internal.serverfs.TmpResourceServerImpl
|
||||
import net.mamoe.mirai.mock.resserver.TmpResourceServer
|
||||
import net.mamoe.mirai.mock.userprofile.UserProfileService
|
||||
import net.mamoe.mirai.mock.utils.NameGenerator
|
||||
import net.mamoe.mirai.mock.utils.broadcastBlocking
|
||||
import net.mamoe.mirai.mock.utils.simpleMemberInfo
|
||||
import net.mamoe.mirai.utils.*
|
||||
import java.util.concurrent.CancellationException
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import net.mamoe.mirai.internal.utils.subLoggerImpl as subLog
|
||||
|
||||
internal class MockBotImpl(
|
||||
override val configuration: BotConfiguration,
|
||||
override val id: Long,
|
||||
nick: String,
|
||||
override val nameGenerator: NameGenerator,
|
||||
override val tmpResourceServer: TmpResourceServer,
|
||||
override val msgDatabase: MessageDatabase,
|
||||
override val userProfileService: UserProfileService,
|
||||
) : MockBot, Bot, ContactOrBot {
|
||||
private val loginBefore = AtomicBoolean(false)
|
||||
override var nickNoEvent: String = nick
|
||||
override var nick: String
|
||||
get() = nickNoEvent
|
||||
set(value) {
|
||||
val ov = nickNoEvent
|
||||
if (value == ov) return
|
||||
nickNoEvent = value
|
||||
BotNickChangedEvent(this, ov, value).broadcastBlocking()
|
||||
}
|
||||
|
||||
override var avatarUrl: String
|
||||
get() = asFriend.avatarUrl
|
||||
set(value) {
|
||||
asFriend.mockApi.avatarUrl = value
|
||||
BotAvatarChangedEvent(this).broadcastBlocking()
|
||||
}
|
||||
|
||||
override fun avatarUrl(spec: AvatarSpec): String {
|
||||
return avatarUrl
|
||||
}
|
||||
|
||||
override val logger: MiraiLogger by lazy {
|
||||
configuration.botLoggerSupplier(this)
|
||||
}
|
||||
|
||||
init {
|
||||
if (tmpResourceServer is TmpResourceServerImpl) {
|
||||
// Not using logger.subLogger caused by kotlin compile error
|
||||
tmpResourceServer.logger =
|
||||
subLog(this.logger, "TmpFsServer").takeUnless { it == this.logger } ?: kotlin.run {
|
||||
MiraiLogger.Factory.create(TmpResourceServerImpl::class.java, "TFS $id")
|
||||
}
|
||||
}
|
||||
tmpResourceServer.startupServer()
|
||||
msgDatabase.connect()
|
||||
}
|
||||
|
||||
val components: ComponentStorage by lazy {
|
||||
ConcurrentComponentStorage {
|
||||
set(EventDispatcher, MockEventDispatcherImpl(coroutineContext, logger))
|
||||
}
|
||||
}
|
||||
|
||||
@TestOnly
|
||||
internal suspend fun joinEventBroadcast() {
|
||||
components[EventDispatcher].joinBroadcast()
|
||||
}
|
||||
|
||||
override suspend fun login() {
|
||||
BotOnlineEvent(this).broadcast()
|
||||
if (!loginBefore.compareAndSet(false, true)) {
|
||||
BotReloginEvent(this, null).broadcast()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||
override fun close(cause: Throwable?) {
|
||||
tmpResourceServer.close()
|
||||
Bot._instances.remove(id, this)
|
||||
cancel(when (cause) {
|
||||
null -> CancellationException("Bot cancelled")
|
||||
else -> CancellationException(cause.message).also { it.initCause(cause) }
|
||||
})
|
||||
}
|
||||
|
||||
override val groups: ContactList<MockGroup> = ContactList()
|
||||
override val friends: ContactList<MockFriend> = ContactList()
|
||||
override val strangers: ContactList<MockStranger> = ContactList()
|
||||
override val otherClients: ContactList<MockOtherClient> = ContactList()
|
||||
override val friendGroups: FriendGroups = MockFriendGroups(this)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun addGroup(id: Long, name: String): MockGroup =
|
||||
addGroup(id, Mirai.calculateGroupUinByGroupCode(id), name)
|
||||
|
||||
override fun addGroup(id: Long, uin: Long, name: String): MockGroup {
|
||||
val group = MockGroupImpl(coroutineContext, this, id, uin, name)
|
||||
groups.delegate.add(group)
|
||||
group.appendMember(simpleMemberInfo(this.id, this.nick, permission = MemberPermission.OWNER))
|
||||
return group
|
||||
}
|
||||
|
||||
override fun addFriend(id: Long, name: String): MockFriend {
|
||||
val friend = MockFriendImpl(coroutineContext, this, id, name, "")
|
||||
friends.delegate.add(friend)
|
||||
return friend
|
||||
}
|
||||
|
||||
override fun addStranger(id: Long, name: String): MockStranger {
|
||||
val stranger = MockStrangerImpl(coroutineContext, this, id, "", name)
|
||||
strangers.delegate.add(stranger)
|
||||
return stranger
|
||||
}
|
||||
|
||||
override val isOnline: Boolean get() = isActive
|
||||
override val eventChannel: EventChannel<BotEvent> =
|
||||
GlobalEventChannel.filterIsInstance<BotEvent>().filter { it.bot === this@MockBotImpl }
|
||||
|
||||
override val asFriend: MockFriend by lazy {
|
||||
MockFriendImpl(coroutineContext, this, id, nick, "").also { basm ->
|
||||
@Suppress("QUALIFIED_SUPERTYPE_EXTENDED_BY_OTHER_SUPERTYPE", "RemoveExplicitSuperQualifier")
|
||||
basm.initAvatarUrl(super<ContactOrBot>.avatarUrl(spec = AvatarSpec.LARGEST))
|
||||
}
|
||||
}
|
||||
override val asStranger: MockStranger by lazy {
|
||||
MockStrangerImpl(coroutineContext, this, id, "", nick)
|
||||
}
|
||||
|
||||
override val coroutineContext: CoroutineContext by lazy {
|
||||
configuration.parentCoroutineContext.childScopeContext()
|
||||
}
|
||||
|
||||
override suspend fun uploadOnlineAudio(resource: ExternalResource): OnlineAudio {
|
||||
return resource.mockImplUploadAudioAsOnline(this)
|
||||
}
|
||||
|
||||
override suspend fun uploadMockImage(resource: ExternalResource): MockImage {
|
||||
val md5 = resource.md5
|
||||
val format = resource.formatName
|
||||
|
||||
return MockImage(generateImageId(md5, format), bot.tmpResourceServer.uploadResourceAsImage(resource).toString())
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "MockBot($id)"
|
||||
}
|
||||
}
|
339
mirai-core-mock/src/internal/MockMiraiImpl.kt
Normal file
339
mirai-core-mock/src/internal/MockMiraiImpl.kt
Normal file
@ -0,0 +1,339 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "CANNOT_OVERRIDE_INVISIBLE_MEMBER")
|
||||
|
||||
package net.mamoe.mirai.mock.internal
|
||||
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.BotFactory
|
||||
import net.mamoe.mirai.contact.*
|
||||
import net.mamoe.mirai.data.FriendInfo
|
||||
import net.mamoe.mirai.data.StrangerInfo
|
||||
import net.mamoe.mirai.data.UserProfile
|
||||
import net.mamoe.mirai.event.Event
|
||||
import net.mamoe.mirai.event.broadcast
|
||||
import net.mamoe.mirai.event.events.*
|
||||
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.MockBotFactory
|
||||
import net.mamoe.mirai.mock.contact.MockGroup
|
||||
import net.mamoe.mirai.mock.database.queryMessageInfo
|
||||
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
|
||||
import net.mamoe.mirai.mock.internal.contact.MockStrangerImpl
|
||||
import net.mamoe.mirai.mock.utils.mock
|
||||
import net.mamoe.mirai.mock.utils.simpleMemberInfo
|
||||
import net.mamoe.mirai.utils.currentTimeSeconds
|
||||
|
||||
internal class MockMiraiImpl : MiraiImpl() {
|
||||
override suspend fun solveBotInvitedJoinGroupRequestEvent(
|
||||
bot: Bot,
|
||||
eventId: Long,
|
||||
invitorId: Long,
|
||||
groupId: Long,
|
||||
accept: Boolean
|
||||
) {
|
||||
bot.mock()
|
||||
if (accept) {
|
||||
val group = bot.addGroup(groupId, bot.nameGenerator.nextGroupName())
|
||||
group.appendMember(
|
||||
simpleMemberInfo(
|
||||
uin = 111111111,
|
||||
permission = MemberPermission.OWNER,
|
||||
name = "MockMember - Owner",
|
||||
nameCard = "Custom NameCard",
|
||||
)
|
||||
).appendMember(
|
||||
simpleMemberInfo(
|
||||
uin = 222222222,
|
||||
permission = MemberPermission.ADMINISTRATOR,
|
||||
name = "MockMember - Administrator",
|
||||
nameCard = "root",
|
||||
)
|
||||
)
|
||||
|
||||
group.appendMember(
|
||||
simpleMemberInfo(
|
||||
uin = bot.id,
|
||||
permission = MemberPermission.MEMBER,
|
||||
name = bot.nick,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if (invitorId != 0L) {
|
||||
val invitor = group[invitorId] ?: kotlin.run {
|
||||
group.addMember(
|
||||
simpleMemberInfo(
|
||||
uin = invitorId,
|
||||
permission = MemberPermission.ADMINISTRATOR,
|
||||
name = bot.getFriend(invitorId)?.nick ?: "A random invitor",
|
||||
nameCard = "invitor",
|
||||
)
|
||||
)
|
||||
}
|
||||
BotJoinGroupEvent.Invite(invitor)
|
||||
} else {
|
||||
BotJoinGroupEvent.Active(group)
|
||||
}.broadcast()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun solveMemberJoinRequestEvent(
|
||||
bot: Bot,
|
||||
eventId: Long,
|
||||
fromId: Long,
|
||||
fromNick: String,
|
||||
groupId: Long,
|
||||
accept: Boolean?,
|
||||
blackList: Boolean,
|
||||
message: String
|
||||
) {
|
||||
if (accept == null || !accept) return // ignore
|
||||
|
||||
val member = bot.getGroupOrFail(groupId).mock().addMember(
|
||||
simpleMemberInfo(
|
||||
uin = fromId,
|
||||
name = fromNick,
|
||||
permission = MemberPermission.MEMBER
|
||||
)
|
||||
)
|
||||
MemberJoinEvent.Active(member).broadcast()
|
||||
}
|
||||
|
||||
override suspend fun solveNewFriendRequestEvent(
|
||||
bot: Bot,
|
||||
eventId: Long,
|
||||
fromId: Long,
|
||||
fromNick: String,
|
||||
accept: Boolean,
|
||||
blackList: Boolean
|
||||
) {
|
||||
if (!accept) return
|
||||
|
||||
// No event broadcast in mirai-core
|
||||
bot.mock().addFriend(fromId, fromNick)
|
||||
}
|
||||
|
||||
override fun getUin(contactOrBot: ContactOrBot): Long {
|
||||
if (contactOrBot is MockGroup) return contactOrBot.uin
|
||||
|
||||
return super.getUin(contactOrBot)
|
||||
}
|
||||
|
||||
override suspend fun muteAnonymousMember(
|
||||
bot: Bot,
|
||||
anonymousId: String,
|
||||
anonymousNick: String,
|
||||
groupId: Long,
|
||||
seconds: Int
|
||||
) {
|
||||
// noop
|
||||
}
|
||||
|
||||
override suspend fun recallFriendMessageRaw(
|
||||
bot: Bot,
|
||||
targetId: Long,
|
||||
messageIds: IntArray,
|
||||
messageInternalIds: IntArray,
|
||||
time: Int
|
||||
): Boolean {
|
||||
val info = bot.mock().msgDatabase.queryMessageInfo(messageIds, messageInternalIds) ?: return false
|
||||
if (info.kind != MessageSourceKind.FRIEND) return false
|
||||
if (info.sender != bot.id) return false
|
||||
if (currentTimeSeconds() - info.time > 120) return false
|
||||
bot.msgDatabase.removeMessageInfo(info.mixinedMsgId)
|
||||
|
||||
// MessageRecallEvent.FriendRecall() // TODO: Unknown Logic
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun recallGroupMessageRaw(
|
||||
bot: Bot,
|
||||
groupCode: Long,
|
||||
messageIds: IntArray,
|
||||
messageInternalIds: IntArray
|
||||
): Boolean {
|
||||
val info = bot.mock().msgDatabase.queryMessageInfo(messageIds, messageInternalIds) ?: return false
|
||||
if (info.kind != MessageSourceKind.GROUP) return false
|
||||
val group = bot.getGroup(info.subject) ?: return false
|
||||
val canDelete = when (group.botPermission) {
|
||||
MemberPermission.OWNER -> true
|
||||
MemberPermission.ADMINISTRATOR -> kotlin.run w@{
|
||||
val member = group.getMember(info.sender) ?: return@w true
|
||||
member.permission == MemberPermission.MEMBER
|
||||
}
|
||||
else -> kotlin.run w@{
|
||||
if (info.sender != bot.id) return@w false
|
||||
currentTimeSeconds() - info.time <= 120
|
||||
}
|
||||
}
|
||||
if (!canDelete) return false
|
||||
bot.msgDatabase.removeMessageInfo(info.mixinedMsgId)
|
||||
|
||||
MessageRecallEvent.GroupRecall(
|
||||
bot,
|
||||
info.sender,
|
||||
messageIds,
|
||||
messageInternalIds,
|
||||
info.time.toInt(),
|
||||
null,
|
||||
group,
|
||||
group[info.sender] ?: return true
|
||||
).broadcast()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun recallGroupTempMessageRaw(
|
||||
bot: Bot,
|
||||
groupUin: Long,
|
||||
targetId: Long,
|
||||
messageIds: IntArray,
|
||||
messageInternalIds: IntArray,
|
||||
time: Int
|
||||
): Boolean = false // TODO: No recall event
|
||||
|
||||
override suspend fun recallMessage(bot: Bot, source: MessageSource) {
|
||||
fun doFailed() {
|
||||
error("Failed to recall message #${source.ids.contentToString()}: $AQQ_RECALL_FAILED_MESSAGE")
|
||||
}
|
||||
if (source is OnlineMessageSource) {
|
||||
when (source) {
|
||||
is OnlineMessageSource.Incoming.FromFriend,
|
||||
is OnlineMessageSource.Outgoing.ToFriend,
|
||||
-> {
|
||||
val resp = recallFriendMessageRaw(
|
||||
bot,
|
||||
source.subject.id,
|
||||
source.ids,
|
||||
source.internalIds,
|
||||
source.time
|
||||
)
|
||||
if (!resp) doFailed()
|
||||
}
|
||||
is OnlineMessageSource.Incoming.FromGroup,
|
||||
is OnlineMessageSource.Outgoing.ToGroup,
|
||||
-> {
|
||||
val resp = recallGroupMessageRaw(
|
||||
bot,
|
||||
source.subject.id,
|
||||
source.ids,
|
||||
source.internalIds
|
||||
)
|
||||
if (!resp) doFailed()
|
||||
}
|
||||
else -> {
|
||||
// TODO: No Event
|
||||
}
|
||||
}
|
||||
} else {
|
||||
source as OfflineMessageSource
|
||||
when (source.kind) {
|
||||
MessageSourceKind.GROUP -> {
|
||||
val resp = recallGroupMessageRaw(
|
||||
bot,
|
||||
source.targetId,
|
||||
source.ids,
|
||||
source.internalIds
|
||||
)
|
||||
if (!resp) doFailed()
|
||||
}
|
||||
MessageSourceKind.FRIEND -> {
|
||||
val resp = recallFriendMessageRaw(
|
||||
bot,
|
||||
source.targetId,
|
||||
source.ids,
|
||||
source.internalIds,
|
||||
source.time
|
||||
)
|
||||
if (!resp) doFailed()
|
||||
}
|
||||
MessageSourceKind.TEMP -> {
|
||||
// TODO: No Event
|
||||
}
|
||||
MessageSourceKind.STRANGER -> {
|
||||
// TODO: No Event
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendNudge(bot: Bot, nudge: Nudge, receiver: Contact): Boolean {
|
||||
NudgeEvent(
|
||||
from = bot,
|
||||
target = nudge.target,
|
||||
subject = receiver,
|
||||
action = "戳了戳",
|
||||
suffix = ""
|
||||
).broadcast()
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun queryProfile(bot: Bot, targetId: Long): UserProfile {
|
||||
return bot.mock().userProfileService.doQueryUserProfile(targetId)
|
||||
}
|
||||
|
||||
override val BotFactory: BotFactory get() = MockBotFactory
|
||||
|
||||
/*override suspend fun getGroupVoiceDownloadUrl(bot: Bot, md5: ByteArray, groupId: Long, dstUin: Long): String {
|
||||
return super.getGroupVoiceDownloadUrl(bot, md5, groupId, dstUin)
|
||||
}*/
|
||||
|
||||
@Suppress("RETURN_TYPE_MISMATCH_ON_OVERRIDE")
|
||||
override fun newFriend(bot: Bot, friendInfo: FriendInfo): Friend {
|
||||
bot.mock()
|
||||
return MockFriendImpl(
|
||||
bot.coroutineContext,
|
||||
bot,
|
||||
friendInfo.uin,
|
||||
friendInfo.nick,
|
||||
friendInfo.remark,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("RETURN_TYPE_MISMATCH_ON_OVERRIDE")
|
||||
override fun newStranger(bot: Bot, strangerInfo: StrangerInfo): Stranger {
|
||||
bot.mock()
|
||||
return MockStrangerImpl(
|
||||
bot.coroutineContext,
|
||||
bot,
|
||||
strangerInfo.uin,
|
||||
strangerInfo.remark,
|
||||
strangerInfo.nick,
|
||||
)
|
||||
}
|
||||
|
||||
override fun createImage(imageId: String): Image {
|
||||
if (imageId matches Image.IMAGE_ID_REGEX) {
|
||||
return MockImage(imageId, "images/" + imageId.substring(1..36))
|
||||
}
|
||||
//imageId.substring(1..36)
|
||||
return super.createImage(imageId)
|
||||
}
|
||||
|
||||
override suspend fun broadcastEvent(event: Event) {
|
||||
if (event is BotEvent) {
|
||||
val bot = event.bot
|
||||
if (bot is MockBotImpl) {
|
||||
bot.components[EventDispatcher].broadcast(event)
|
||||
return
|
||||
}
|
||||
}
|
||||
super.broadcastEvent(event)
|
||||
}
|
||||
|
||||
override suspend fun refreshKeys(bot: Bot) {
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "CANNOT_OVERRIDE_INVISIBLE_MEMBER")
|
||||
|
||||
package net.mamoe.mirai.mock.internal.components
|
||||
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.launch
|
||||
import net.mamoe.mirai.event.Event
|
||||
import net.mamoe.mirai.internal.network.components.EventDispatcherImpl
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/*
|
||||
|
||||
Copied from:
|
||||
|
||||
mirai-core/src/commonTest/kotlin/network/framework/components/EventDispatcherImpl.kt
|
||||
|
||||
*/
|
||||
internal open class MockEventDispatcherImpl(
|
||||
lifecycleContext: CoroutineContext,
|
||||
logger: MiraiLogger,
|
||||
) : EventDispatcherImpl(lifecycleContext, logger) {
|
||||
override suspend fun broadcast(event: Event) {
|
||||
if (isActive) {
|
||||
// This requires the scope to be active, while the original one doesn't.
|
||||
|
||||
// so that [joinBroadcast] works.
|
||||
launch(
|
||||
start = CoroutineStart.UNDISPATCHED
|
||||
) {
|
||||
super.broadcast(event)
|
||||
}.join()
|
||||
} else {
|
||||
// Scope closed, typically when broadcasting `BotOfflineEvent` by StateObserver from `bot.close`
|
||||
super.broadcast(event)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun joinBroadcast() {
|
||||
for (child in coroutineContext.job.children) {
|
||||
child.join()
|
||||
}
|
||||
}
|
||||
}
|
76
mirai-core-mock/src/internal/contact/AbstractMockContact.kt
Normal file
76
mirai-core-mock/src/internal/contact/AbstractMockContact.kt
Normal file
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "CANNOT_OVERRIDE_INVISIBLE_MEMBER")
|
||||
|
||||
package net.mamoe.mirai.mock.internal.contact
|
||||
|
||||
import net.mamoe.mirai.contact.Contact
|
||||
import net.mamoe.mirai.event.events.MessagePreSendEvent
|
||||
import net.mamoe.mirai.internal.contact.broadcastMessagePreSendEvent
|
||||
import net.mamoe.mirai.internal.contact.replaceMagicCodes
|
||||
import net.mamoe.mirai.message.MessageReceipt
|
||||
import net.mamoe.mirai.message.data.Image
|
||||
import net.mamoe.mirai.message.data.Message
|
||||
import net.mamoe.mirai.message.data.MessageChain
|
||||
import net.mamoe.mirai.message.data.OnlineMessageSource
|
||||
import net.mamoe.mirai.mock.MockBot
|
||||
import net.mamoe.mirai.mock.contact.MockContact
|
||||
import net.mamoe.mirai.utils.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
internal abstract class AbstractMockContact(
|
||||
parentCoroutineContext: CoroutineContext,
|
||||
override val bot: MockBot,
|
||||
override val id: Long
|
||||
) : MockContact {
|
||||
|
||||
override val coroutineContext: CoroutineContext = parentCoroutineContext.childScopeContext()
|
||||
|
||||
/**
|
||||
* @return isCancelled
|
||||
*/
|
||||
protected abstract fun newMessagePreSend(message: Message): MessagePreSendEvent
|
||||
protected abstract suspend fun postMessagePreSend(message: MessageChain, receipt: MessageReceipt<*>)
|
||||
|
||||
protected abstract fun newMessageSource(message: MessageChain): OnlineMessageSource.Outgoing
|
||||
|
||||
override suspend fun sendMessage(message: Message): MessageReceipt<Contact> {
|
||||
val msg = broadcastMessagePreSendEvent(message, false) { _, _ -> newMessagePreSend(message) }
|
||||
|
||||
val source = newMessageSource(msg)
|
||||
val response = source.withMessage(msg)
|
||||
|
||||
bot.logger.verbose("$this <- $msg".replaceMagicCodes())
|
||||
|
||||
|
||||
@Suppress("DEPRECATION_ERROR")
|
||||
return MessageReceipt(source, this).also {
|
||||
postMessagePreSend(response, it)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override suspend fun uploadImage(resource: ExternalResource): Image {
|
||||
return bot.uploadMockImage(resource)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "$id"
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend inline fun <T : ExternalResource, R> T.inResource(action: () -> R): R {
|
||||
return useAutoClose {
|
||||
runBIO {
|
||||
inputStream().dropContent(close = true)
|
||||
}
|
||||
action()
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.internal.contact
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import net.mamoe.mirai.contact.Group
|
||||
import net.mamoe.mirai.contact.NormalMember
|
||||
import net.mamoe.mirai.contact.PermissionDeniedException
|
||||
import net.mamoe.mirai.contact.announcement.Announcement
|
||||
import net.mamoe.mirai.contact.announcement.AnnouncementImage
|
||||
import net.mamoe.mirai.contact.announcement.OnlineAnnouncement
|
||||
import net.mamoe.mirai.contact.isOperator
|
||||
import net.mamoe.mirai.event.events.GroupEntranceAnnouncementChangeEvent
|
||||
import net.mamoe.mirai.mock.contact.announcement.MockAnnouncements
|
||||
import net.mamoe.mirai.mock.contact.announcement.MockOnlineAnnouncement
|
||||
import net.mamoe.mirai.mock.contact.announcement.copy
|
||||
import net.mamoe.mirai.mock.utils.broadcastBlocking
|
||||
import net.mamoe.mirai.utils.ExternalResource
|
||||
import net.mamoe.mirai.utils.currentTimeSeconds
|
||||
import net.mamoe.mirai.utils.generateImageId
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.stream.Stream
|
||||
|
||||
internal class MockAnnouncementsImpl(
|
||||
val group: Group,
|
||||
) : MockAnnouncements {
|
||||
val announcements = ConcurrentHashMap<String, OnlineAnnouncement>()
|
||||
|
||||
override suspend fun asFlow(): Flow<OnlineAnnouncement> = announcements.values.asFlow()
|
||||
|
||||
override fun asStream(): Stream<OnlineAnnouncement> = announcements.values.toList().stream()
|
||||
|
||||
override suspend fun delete(fid: String): Boolean = announcements.remove(fid) != null
|
||||
|
||||
override suspend fun get(fid: String): OnlineAnnouncement? = announcements[fid]
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
internal fun putDirect(announcement: MockOnlineAnnouncement) {
|
||||
val annoc = if (announcement.fid.isEmpty()) {
|
||||
announcement.copy(fid = UUID.randomUUID().toString())
|
||||
} else announcement
|
||||
if (annoc.parameters.sendToNewMember) {
|
||||
announcements.entries.removeIf { (_, v) -> v.parameters.sendToNewMember }
|
||||
}
|
||||
announcements[annoc.fid] = annoc
|
||||
annoc.group = group
|
||||
}
|
||||
|
||||
override fun mockPublish(announcement: Announcement, actor: NormalMember, events: Boolean): OnlineAnnouncement {
|
||||
val old = if (announcement.parameters.sendToNewMember)
|
||||
announcements.elements().toList().firstOrNull { oa -> oa.parameters.sendToNewMember }
|
||||
else null
|
||||
val onac = MockOnlineAnnouncement(
|
||||
content = announcement.content,
|
||||
parameters = announcement.parameters,
|
||||
senderId = actor.id,
|
||||
fid = UUID.randomUUID().toString(),
|
||||
allConfirmed = false,
|
||||
confirmedMembersCount = 0,
|
||||
publicationTime = currentTimeSeconds()
|
||||
)
|
||||
putDirect(onac)
|
||||
if (!events) return onac
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
GroupEntranceAnnouncementChangeEvent(
|
||||
origin = old?.content.orEmpty(),
|
||||
new = onac.content,
|
||||
group = group,
|
||||
operator = actor.takeUnless { it.id == group.bot.id }
|
||||
).broadcastBlocking()
|
||||
|
||||
// TODO: mirai-core no other events about announcements
|
||||
return onac
|
||||
}
|
||||
|
||||
override suspend fun publish(announcement: Announcement): OnlineAnnouncement {
|
||||
if (!group.botPermission.isOperator()) {
|
||||
throw PermissionDeniedException("Failed to publish a new announcement because bot don't have admin permission to perform it.")
|
||||
}
|
||||
return mockPublish(announcement, this.group.botAsMember, true)
|
||||
}
|
||||
|
||||
override suspend fun uploadImage(resource: ExternalResource): AnnouncementImage = resource.inResource {
|
||||
AnnouncementImage.create(generateImageId(resource.md5), 500, 500)
|
||||
}
|
||||
}
|
119
mirai-core-mock/src/internal/contact/MockAnonymousMemberImpl.kt
Normal file
119
mirai-core-mock/src/internal/contact/MockAnonymousMemberImpl.kt
Normal file
@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.internal.contact
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.mamoe.mirai.contact.AvatarSpec
|
||||
import net.mamoe.mirai.contact.MemberPermission
|
||||
import net.mamoe.mirai.contact.nameCardOrNick
|
||||
import net.mamoe.mirai.event.broadcast
|
||||
import net.mamoe.mirai.event.events.GroupMessageEvent
|
||||
import net.mamoe.mirai.event.events.MessagePreSendEvent
|
||||
import net.mamoe.mirai.message.MessageReceipt
|
||||
import net.mamoe.mirai.message.data.Image
|
||||
import net.mamoe.mirai.message.data.Message
|
||||
import net.mamoe.mirai.message.data.MessageChain
|
||||
import net.mamoe.mirai.message.data.OnlineMessageSource
|
||||
import net.mamoe.mirai.mock.MockBot
|
||||
import net.mamoe.mirai.mock.contact.MockAnonymousMember
|
||||
import net.mamoe.mirai.mock.contact.MockGroup
|
||||
import net.mamoe.mirai.mock.contact.MockMember
|
||||
import net.mamoe.mirai.mock.internal.msgsrc.OnlineMsgSrcFromGroup
|
||||
import net.mamoe.mirai.mock.internal.msgsrc.newMsgSrc
|
||||
import net.mamoe.mirai.utils.ExternalResource
|
||||
import net.mamoe.mirai.utils.lateinitMutableProperty
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
internal class MockAnonymousMemberImpl(
|
||||
parentCoroutineContext: CoroutineContext,
|
||||
bot: MockBot, id: Long,
|
||||
|
||||
override val anonymousId: String,
|
||||
override val group: MockGroup,
|
||||
nameCard: String
|
||||
) : AbstractMockContact(parentCoroutineContext, bot, id), MockAnonymousMember {
|
||||
override fun newMessagePreSend(message: Message): MessagePreSendEvent {
|
||||
throw AssertionError()
|
||||
}
|
||||
|
||||
override fun avatarUrl(spec: AvatarSpec): String {
|
||||
return avatarUrl
|
||||
}
|
||||
|
||||
override suspend fun postMessagePreSend(message: MessageChain, receipt: MessageReceipt<*>) {
|
||||
throw AssertionError()
|
||||
}
|
||||
|
||||
override fun newMessageSource(message: MessageChain): OnlineMessageSource.Outgoing {
|
||||
throw AssertionError()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION_ERROR")
|
||||
override suspend fun sendMessage(message: Message): Nothing = super<MockAnonymousMember>.sendMessage(message)
|
||||
override suspend fun uploadImage(resource: ExternalResource): Image =
|
||||
super<AbstractMockContact>.uploadImage(resource)
|
||||
|
||||
override var permission: MemberPermission
|
||||
get() = MemberPermission.MEMBER
|
||||
set(value) {
|
||||
error("Modifying permission of AnonymousMember")
|
||||
}
|
||||
override val specialTitle: String
|
||||
get() = "匿名"
|
||||
|
||||
override suspend fun mute(durationSeconds: Int) {
|
||||
}
|
||||
|
||||
override var remark: String
|
||||
get() = ""
|
||||
set(_) {}
|
||||
override var nick: String
|
||||
get() = nameCard
|
||||
set(_) {}
|
||||
|
||||
override val nameCard: String
|
||||
get() = mockApi.nick
|
||||
|
||||
override val mockApi: MockMember.MockApi = object : MockMember.MockApi {
|
||||
override val member: MockMember
|
||||
get() = this@MockAnonymousMemberImpl
|
||||
|
||||
override var nick: String = nameCard
|
||||
|
||||
override var remark: String
|
||||
get() = ""
|
||||
set(_) {}
|
||||
|
||||
override var permission: MemberPermission
|
||||
get() = MemberPermission.MEMBER
|
||||
set(_) {}
|
||||
override var avatarUrl: String by lateinitMutableProperty { runBlocking { MockImage.random(bot).getUrl(bot) } }
|
||||
}
|
||||
|
||||
// TODO
|
||||
override val avatarUrl: String by mockApi::avatarUrl
|
||||
override fun changeAvatarUrl(newAvatar: String) {
|
||||
mockApi.avatarUrl = newAvatar
|
||||
}
|
||||
|
||||
|
||||
override suspend fun says(message: MessageChain): MessageChain {
|
||||
val src = newMsgSrc(true, message) { ids, internalIds, time ->
|
||||
OnlineMsgSrcFromGroup(ids, internalIds, time, message, bot, this)
|
||||
}
|
||||
val msg = src.withMessage(message)
|
||||
GroupMessageEvent(nameCardOrNick, permission, this, msg, src.time).broadcast()
|
||||
return msg
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "AnonymousMember($nameCard, $anonymousId)"
|
||||
}
|
||||
}
|
168
mirai-core-mock/src/internal/contact/MockFriendImpl.kt
Normal file
168
mirai-core-mock/src/internal/contact/MockFriendImpl.kt
Normal file
@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "CANNOT_OVERRIDE_INVISIBLE_MEMBER")
|
||||
|
||||
package net.mamoe.mirai.mock.internal.contact
|
||||
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.mamoe.mirai.contact.AvatarSpec
|
||||
import net.mamoe.mirai.contact.Friend
|
||||
import net.mamoe.mirai.contact.friendgroup.FriendGroup
|
||||
import net.mamoe.mirai.contact.roaming.RoamingMessages
|
||||
import net.mamoe.mirai.event.broadcast
|
||||
import net.mamoe.mirai.event.events.*
|
||||
import net.mamoe.mirai.message.MessageReceipt
|
||||
import net.mamoe.mirai.message.data.Message
|
||||
import net.mamoe.mirai.message.data.MessageChain
|
||||
import net.mamoe.mirai.message.data.OfflineAudio
|
||||
import net.mamoe.mirai.message.data.OnlineMessageSource
|
||||
import net.mamoe.mirai.mock.MockBot
|
||||
import net.mamoe.mirai.mock.contact.MockFriend
|
||||
import net.mamoe.mirai.mock.internal.contact.friendfroup.MockFriendGroups
|
||||
import net.mamoe.mirai.mock.internal.contact.roaming.MockRoamingMessages
|
||||
import net.mamoe.mirai.mock.internal.msgsrc.OnlineMsgSrcFromFriend
|
||||
import net.mamoe.mirai.mock.internal.msgsrc.OnlineMsgSrcToFriend
|
||||
import net.mamoe.mirai.mock.internal.msgsrc.newMsgSrc
|
||||
import net.mamoe.mirai.mock.utils.broadcastBlocking
|
||||
import net.mamoe.mirai.utils.ExternalResource
|
||||
import net.mamoe.mirai.utils.cast
|
||||
import net.mamoe.mirai.utils.lateinitMutableProperty
|
||||
import java.util.concurrent.CancellationException
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
internal class MockFriendImpl(
|
||||
parentCoroutineContext: CoroutineContext,
|
||||
bot: MockBot,
|
||||
id: Long,
|
||||
nick: String,
|
||||
remark: String
|
||||
) : AbstractMockContact(
|
||||
parentCoroutineContext,
|
||||
bot, id
|
||||
), MockFriend {
|
||||
override val mockApi: MockFriend.MockApi = object : MockFriend.MockApi {
|
||||
override val contact: MockFriend get() = this@MockFriendImpl
|
||||
|
||||
override var nick: String = nick
|
||||
override var remark: String = remark
|
||||
override var avatarUrl: String
|
||||
get() = this@MockFriendImpl._avatarUrl
|
||||
set(value) {
|
||||
this@MockFriendImpl._avatarUrl = value
|
||||
bot.groups.forEach { g ->
|
||||
val mems = if (this@MockFriendImpl.id == bot.id) {
|
||||
sequenceOf(g.botAsMember)
|
||||
} else g.members.asSequence().filter {
|
||||
it.id == this@MockFriendImpl.id
|
||||
}
|
||||
mems.forEach { m ->
|
||||
m.cast<MockNormalMemberImpl>().avatarUrl = value
|
||||
}
|
||||
}
|
||||
if (this@MockFriendImpl.id == bot.id) {
|
||||
sequenceOf(bot.asStranger)
|
||||
} else {
|
||||
bot.strangers.asSequence().filter { s ->
|
||||
s.id == this@MockFriendImpl.id
|
||||
}
|
||||
}.forEach { it.cast<MockStrangerImpl>().avatarUrl = value }
|
||||
}
|
||||
|
||||
override var friendGroupId: Int = 0
|
||||
}
|
||||
|
||||
override val friendGroup: FriendGroup
|
||||
get() = bot.friendGroups.cast<MockFriendGroups>().findOrDefault(mockApi.friendGroupId)
|
||||
|
||||
private var _avatarUrl: String by lateinitMutableProperty { runBlocking { MockImage.random(bot).getUrl(bot) } }
|
||||
override val avatarUrl: String get() = _avatarUrl
|
||||
internal fun initAvatarUrl(v: String) {
|
||||
_avatarUrl = v
|
||||
}
|
||||
|
||||
override fun changeAvatarUrl(newAvatar: String) {
|
||||
mockApi.avatarUrl = newAvatar
|
||||
FriendAvatarChangedEvent(this).broadcastBlocking()
|
||||
}
|
||||
|
||||
override fun avatarUrl(spec: AvatarSpec): String {
|
||||
return avatarUrl
|
||||
}
|
||||
|
||||
override var nick: String
|
||||
get() = mockApi.nick
|
||||
set(value) {
|
||||
val ov = mockApi.nick
|
||||
if (ov == value) return
|
||||
mockApi.nick = value
|
||||
FriendNickChangedEvent(this, ov, value).broadcastBlocking()
|
||||
}
|
||||
|
||||
override var remark: String
|
||||
get() = mockApi.remark
|
||||
set(value) {
|
||||
val ov = mockApi.remark
|
||||
if (ov == value) return
|
||||
mockApi.remark = value
|
||||
FriendRemarkChangeEvent(this, ov, value).broadcastBlocking()
|
||||
}
|
||||
|
||||
override fun newMessagePreSend(message: Message): MessagePreSendEvent {
|
||||
return FriendMessagePreSendEvent(this, message)
|
||||
}
|
||||
|
||||
override suspend fun postMessagePreSend(message: MessageChain, receipt: MessageReceipt<*>) {
|
||||
FriendMessagePostSendEvent(this, message, null, receipt.cast()).broadcast()
|
||||
}
|
||||
|
||||
override fun newMessageSource(message: MessageChain): OnlineMessageSource.Outgoing {
|
||||
return newMsgSrc(false, message) { ids, internalIds, time ->
|
||||
OnlineMsgSrcToFriend(ids, internalIds, time, message, bot, bot, this)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendMessage(message: Message): MessageReceipt<Friend> {
|
||||
return super<AbstractMockContact>.sendMessage(message).cast()
|
||||
}
|
||||
|
||||
override suspend fun delete() {
|
||||
if (bot.friends.delegate.remove(this)) {
|
||||
FriendDeleteEvent(this).broadcast()
|
||||
cancel(CancellationException("Friend deleted"))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun uploadAudio(resource: ExternalResource): OfflineAudio =
|
||||
resource.mockUploadAudio(bot)
|
||||
|
||||
override val roamingMessages: RoamingMessages = MockRoamingMessages(this)
|
||||
|
||||
override suspend fun says(message: MessageChain): MessageChain {
|
||||
val src = newMsgSrc(true, message) { ids, internalIds, time ->
|
||||
OnlineMsgSrcFromFriend(ids, internalIds, time, message, bot, this)
|
||||
}
|
||||
val msg = src.withMessage(message)
|
||||
FriendMessageEvent(this, msg, src.time).broadcast()
|
||||
return msg
|
||||
}
|
||||
|
||||
override suspend fun broadcastMsgSyncEvent(message: MessageChain, time: Int) {
|
||||
val src = newMsgSrc(true, message, time.toLong()) { ids, internalIds, time0 ->
|
||||
OnlineMsgSrcToFriend(ids, internalIds, time0, message, bot, bot, this)
|
||||
}
|
||||
val msg = src.withMessage(message)
|
||||
FriendMessageSyncEvent(this, msg, time).broadcast()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "Friend($id)"
|
||||
}
|
||||
}
|
353
mirai-core-mock/src/internal/contact/MockGroupImpl.kt
Normal file
353
mirai-core-mock/src/internal/contact/MockGroupImpl.kt
Normal file
@ -0,0 +1,353 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "CANNOT_OVERRIDE_INVISIBLE_MEMBER")
|
||||
|
||||
package net.mamoe.mirai.mock.internal.contact
|
||||
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.mamoe.mirai.contact.*
|
||||
import net.mamoe.mirai.contact.announcement.OfflineAnnouncement
|
||||
import net.mamoe.mirai.contact.announcement.buildAnnouncementParameters
|
||||
import net.mamoe.mirai.contact.file.RemoteFiles
|
||||
import net.mamoe.mirai.data.GroupHonorType
|
||||
import net.mamoe.mirai.data.MemberInfo
|
||||
import net.mamoe.mirai.event.broadcast
|
||||
import net.mamoe.mirai.event.events.*
|
||||
import net.mamoe.mirai.internal.contact.uin
|
||||
import net.mamoe.mirai.message.MessageReceipt
|
||||
import net.mamoe.mirai.message.data.*
|
||||
import net.mamoe.mirai.mock.MockBot
|
||||
import net.mamoe.mirai.mock.contact.MockAnonymousMember
|
||||
import net.mamoe.mirai.mock.contact.MockGroup
|
||||
import net.mamoe.mirai.mock.contact.MockGroupControlPane
|
||||
import net.mamoe.mirai.mock.contact.MockNormalMember
|
||||
import net.mamoe.mirai.mock.internal.msgsrc.OnlineMsgSrcToGroup
|
||||
import net.mamoe.mirai.mock.internal.msgsrc.newMsgSrc
|
||||
import net.mamoe.mirai.mock.utils.broadcastBlocking
|
||||
import net.mamoe.mirai.mock.utils.mock
|
||||
import net.mamoe.mirai.utils.*
|
||||
import java.util.*
|
||||
import java.util.concurrent.CancellationException
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
internal class MockGroupImpl(
|
||||
parentCoroutineContext: CoroutineContext,
|
||||
bot: MockBot,
|
||||
id: Long,
|
||||
override val uin: Long,
|
||||
name: String,
|
||||
) : AbstractMockContact(
|
||||
parentCoroutineContext, bot, id
|
||||
), MockGroup {
|
||||
override val honorMembers: MutableMap<GroupHonorType, MockNormalMember> = EnumMap(GroupHonorType::class.java)
|
||||
private val txFileSystem by lazy { bot.mock().tmpResourceServer.mockServerFileDisk.newFsSystem() }
|
||||
|
||||
override fun avatarUrl(spec: AvatarSpec): String {
|
||||
return avatarUrl
|
||||
}
|
||||
|
||||
override fun changeHonorMember(member: MockNormalMember, honorType: GroupHonorType) {
|
||||
val onm = honorMembers[honorType]
|
||||
honorMembers[honorType] = member
|
||||
// reference net.mamoe.mirai.internal.network.notice.group.NoticePipelineContext.processGeneralGrayTip, GroupNotificationProcessor.kt#361L
|
||||
if (honorType == GroupHonorType.TALKATIVE) {
|
||||
if (onm != null) GroupTalkativeChangeEvent(this, member, onm).broadcastBlocking()
|
||||
}
|
||||
if (onm != null) MemberHonorChangeEvent.Lose(onm, honorType).broadcastBlocking()
|
||||
MemberHonorChangeEvent.Achieve(member, honorType).broadcastBlocking()
|
||||
}
|
||||
|
||||
override fun appendMember(mockMember: MemberInfo): MockGroup {
|
||||
addMember(mockMember)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun addMember(mockMember: MemberInfo): MockNormalMember {
|
||||
val nMember = MockNormalMemberImpl(
|
||||
this.coroutineContext,
|
||||
bot,
|
||||
mockMember.uin,
|
||||
this,
|
||||
mockMember.permission,
|
||||
mockMember.remark,
|
||||
mockMember.nick,
|
||||
mockMember.muteTimestamp,
|
||||
mockMember.joinTimestamp,
|
||||
mockMember.lastSpeakTimestamp,
|
||||
mockMember.specialTitle,
|
||||
mockMember.nameCard
|
||||
)
|
||||
|
||||
if (nMember.id == bot.id) {
|
||||
botAsMember = nMember
|
||||
} else {
|
||||
members.delegate.removeAll { it.uin == nMember.id }
|
||||
members.delegate.add(nMember)
|
||||
}
|
||||
|
||||
if (nMember.permission == MemberPermission.OWNER) {
|
||||
if (::owner.isInitialized) {
|
||||
owner.mock().mockApi.permission = MemberPermission.MEMBER
|
||||
}
|
||||
owner = nMember
|
||||
}
|
||||
return nMember
|
||||
}
|
||||
|
||||
override suspend fun changeOwner(member: NormalMember) {
|
||||
val oldOwner = owner
|
||||
val oldPerm = member.permission
|
||||
member.mock().mockApi.permission = MemberPermission.OWNER
|
||||
oldOwner.mock().mockApi.permission = MemberPermission.MEMBER
|
||||
owner = member
|
||||
|
||||
if (member === botAsMember) {
|
||||
BotGroupPermissionChangeEvent(this, oldPerm, MemberPermission.OWNER)
|
||||
} else {
|
||||
MemberPermissionChangeEvent(member, oldPerm, MemberPermission.OWNER)
|
||||
}.broadcast()
|
||||
|
||||
if (oldOwner === botAsMember) {
|
||||
BotGroupPermissionChangeEvent(this, MemberPermission.OWNER, MemberPermission.MEMBER)
|
||||
} else {
|
||||
MemberPermissionChangeEvent(oldOwner, MemberPermission.OWNER, MemberPermission.MEMBER)
|
||||
}.broadcast()
|
||||
}
|
||||
|
||||
override fun changeOwnerNoEventBroadcast(member: NormalMember) {
|
||||
val oldOwner = owner
|
||||
member.mock().mockApi.permission = MemberPermission.OWNER
|
||||
oldOwner.mockApi.permission = MemberPermission.MEMBER
|
||||
owner = member
|
||||
}
|
||||
|
||||
override fun newAnonymous(nick: String, id: String): MockAnonymousMember {
|
||||
return MockAnonymousMemberImpl(
|
||||
coroutineContext, bot, 80000000, id, this, nick
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private val rawGroupControlPane = object : MockGroupControlPane {
|
||||
override val group: MockGroup get() = this@MockGroupImpl
|
||||
override val currentActor: MockNormalMember get() = group.botAsMember
|
||||
override var isAllowMemberInvite: Boolean = false
|
||||
override var isMuteAll: Boolean = false
|
||||
override var isAllowMemberFileUploading: Boolean = false
|
||||
override var isAnonymousChatAllowed: Boolean = false
|
||||
override var isAllowConfessTalk: Boolean = false
|
||||
override var groupName: String = name
|
||||
|
||||
override fun withActor(actor: MockNormalMember): MockGroupControlPane {
|
||||
return GroupControlPaneImpl(actor)
|
||||
}
|
||||
}
|
||||
|
||||
internal inner class GroupControlPaneImpl(
|
||||
override val currentActor: MockNormalMember
|
||||
) : MockGroupControlPane {
|
||||
override val group: MockGroup get() = this@MockGroupImpl
|
||||
private val actorNullIfBot: MockNormalMember?
|
||||
get() = currentActor.takeIf { it.id != bot.id }
|
||||
|
||||
override var groupName: String
|
||||
get() = rawGroupControlPane.groupName
|
||||
set(value) {
|
||||
val ov = rawGroupControlPane.groupName
|
||||
if (ov == value) return
|
||||
rawGroupControlPane.groupName = value
|
||||
GroupNameChangeEvent(ov, value, group, actorNullIfBot).broadcastBlocking()
|
||||
}
|
||||
|
||||
override var isMuteAll: Boolean
|
||||
get() = rawGroupControlPane.isMuteAll
|
||||
set(value) {
|
||||
val ov = rawGroupControlPane.isMuteAll
|
||||
if (ov == value) return
|
||||
rawGroupControlPane.isMuteAll = value
|
||||
GroupMuteAllEvent(ov, value, group, actorNullIfBot).broadcastBlocking()
|
||||
}
|
||||
|
||||
override var isAllowMemberFileUploading: Boolean
|
||||
get() = rawGroupControlPane.isAllowMemberFileUploading
|
||||
set(value) {
|
||||
// TODO: core-api no event
|
||||
rawGroupControlPane.isAllowMemberFileUploading = value
|
||||
}
|
||||
|
||||
override var isAllowMemberInvite: Boolean
|
||||
get() = rawGroupControlPane.isAllowMemberInvite
|
||||
set(value) {
|
||||
val ov = rawGroupControlPane.isAllowMemberInvite
|
||||
if (ov == value) return
|
||||
rawGroupControlPane.isAllowMemberInvite = value
|
||||
GroupAllowMemberInviteEvent(ov, value, group, actorNullIfBot).broadcastBlocking()
|
||||
}
|
||||
|
||||
override var isAnonymousChatAllowed: Boolean
|
||||
get() = rawGroupControlPane.isAnonymousChatAllowed
|
||||
set(value) {
|
||||
val ov = rawGroupControlPane.isAnonymousChatAllowed
|
||||
if (ov == value) return
|
||||
rawGroupControlPane.isAnonymousChatAllowed = value
|
||||
GroupAllowAnonymousChatEvent(ov, value, group, actorNullIfBot).broadcastBlocking()
|
||||
}
|
||||
|
||||
override var isAllowConfessTalk: Boolean
|
||||
get() = rawGroupControlPane.isAllowConfessTalk
|
||||
set(value) {
|
||||
val ov = rawGroupControlPane.isAllowConfessTalk
|
||||
if (ov == value) return
|
||||
rawGroupControlPane.isAllowConfessTalk = value
|
||||
GroupAllowConfessTalkEvent(ov, value, group, currentActor.id == bot.id).broadcastBlocking()
|
||||
}
|
||||
|
||||
override fun withActor(actor: MockNormalMember): MockGroupControlPane {
|
||||
return GroupControlPaneImpl(actor)
|
||||
}
|
||||
}
|
||||
|
||||
override val controlPane: MockGroupControlPane get() = rawGroupControlPane
|
||||
|
||||
override var name: String
|
||||
get() = controlPane.groupName
|
||||
set(value) {
|
||||
checkBotPermission(MemberPermission.ADMINISTRATOR)
|
||||
controlPane.withActor(botAsMember).groupName = value
|
||||
}
|
||||
|
||||
override val mockApi: MockGroup.MockApi = object : MockGroup.MockApi {
|
||||
override var avatarUrl: String by lateinitMutableProperty {
|
||||
runBlocking { MockImage.random(bot).getUrl(bot) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun changeAvatarUrl(newAvatar: String) {
|
||||
mockApi.avatarUrl = newAvatar
|
||||
}
|
||||
|
||||
override val avatarUrl: String by mockApi::avatarUrl
|
||||
|
||||
override lateinit var owner: MockNormalMember
|
||||
override lateinit var botAsMember: MockNormalMember
|
||||
override val members: ContactList<MockNormalMember> = ContactList()
|
||||
override fun get(id: Long): MockNormalMember? {
|
||||
if (id == bot.id) return botAsMember
|
||||
return members[id]
|
||||
}
|
||||
|
||||
override fun contains(id: Long): Boolean = members.any { it.id == id }
|
||||
|
||||
|
||||
override suspend fun quit(): Boolean {
|
||||
return if (bot.groups.delegate.remove(this)) {
|
||||
BotLeaveEvent.Active(this).broadcast()
|
||||
cancel(CancellationException("Bot quited group $id"))
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override val announcements = MockAnnouncementsImpl(this)
|
||||
|
||||
@Suppress("OverridingDeprecatedMember", "OVERRIDE_DEPRECATION")
|
||||
override val settings: GroupSettings = object : GroupSettings {
|
||||
override var entranceAnnouncement: String
|
||||
get() = announcements.announcements.values.asSequence()
|
||||
.filter { it.parameters.sendToNewMember }
|
||||
.firstOrNull()?.content ?: ""
|
||||
set(value) {
|
||||
checkBotPermission(MemberPermission.ADMINISTRATOR)
|
||||
announcements.mockPublish(OfflineAnnouncement.create(value, buildAnnouncementParameters {
|
||||
sendToNewMember = true
|
||||
}), this@MockGroupImpl.botAsMember)
|
||||
}
|
||||
|
||||
override var isMuteAll: Boolean
|
||||
get() = rawGroupControlPane.isMuteAll
|
||||
set(value) {
|
||||
checkBotPermission(MemberPermission.ADMINISTRATOR)
|
||||
rawGroupControlPane.withActor(botAsMember).isMuteAll = value
|
||||
}
|
||||
|
||||
override var isAllowMemberInvite: Boolean
|
||||
get() = rawGroupControlPane.isAllowMemberInvite
|
||||
set(value) {
|
||||
checkBotPermission(MemberPermission.ADMINISTRATOR)
|
||||
rawGroupControlPane.withActor(botAsMember).isAllowMemberInvite = value
|
||||
}
|
||||
|
||||
@MiraiExperimentalApi
|
||||
override val isAutoApproveEnabled: Boolean
|
||||
get() = false // TODO
|
||||
|
||||
override var isAnonymousChatEnabled: Boolean
|
||||
get() = rawGroupControlPane.isAnonymousChatAllowed
|
||||
set(value) {
|
||||
checkBotPermission(MemberPermission.ADMINISTRATOR)
|
||||
rawGroupControlPane.withActor(botAsMember).isAnonymousChatAllowed = value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun newMessagePreSend(message: Message): MessagePreSendEvent =
|
||||
GroupMessagePreSendEvent(this, message)
|
||||
|
||||
|
||||
override suspend fun postMessagePreSend(message: MessageChain, receipt: MessageReceipt<*>) {
|
||||
GroupMessagePostSendEvent(this, message, null, receipt = receipt.cast())
|
||||
.broadcast()
|
||||
}
|
||||
|
||||
override fun newMessageSource(message: MessageChain): OnlineMessageSource.Outgoing {
|
||||
return newMsgSrc(false, message) { ids, internalIds, time ->
|
||||
OnlineMsgSrcToGroup(ids, internalIds, time, message, bot, bot, this)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun broadcastMsgSyncEvent(message: MessageChain, time: Int) {
|
||||
val src = newMsgSrc(true, message, time.toLong()) { ids, internalIds, time0 ->
|
||||
OnlineMsgSrcToGroup(ids, internalIds, time0, message, bot, bot, this)
|
||||
}
|
||||
val msg = src.withMessage(message)
|
||||
GroupMessageSyncEvent(this, msg, botAsMember, bot.nick, time).broadcast()
|
||||
}
|
||||
|
||||
override suspend fun sendMessage(message: Message): MessageReceipt<Group> {
|
||||
return super<AbstractMockContact>.sendMessage(message).cast()
|
||||
}
|
||||
|
||||
@Suppress("OverridingDeprecatedMember", "DEPRECATION", "DEPRECATION_ERROR", "OVERRIDE_DEPRECATION")
|
||||
override suspend fun uploadVoice(resource: ExternalResource): net.mamoe.mirai.message.data.Voice =
|
||||
resource.mockUploadVoice(bot)
|
||||
|
||||
override suspend fun setEssenceMessage(source: MessageSource): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
@Deprecated("Please use files instead.", replaceWith = ReplaceWith("files.root"))
|
||||
@Suppress("OverridingDeprecatedMember", "DEPRECATION")
|
||||
override val filesRoot: RemoteFile by lazy {
|
||||
net.mamoe.mirai.mock.internal.remotefile.remotefile.RootRemoteFile(txFileSystem, this)
|
||||
}
|
||||
|
||||
override val files: RemoteFiles by lazy {
|
||||
net.mamoe.mirai.mock.internal.remotefile.absolutefile.MockRemoteFiles(this, txFileSystem)
|
||||
}
|
||||
|
||||
override suspend fun uploadAudio(resource: ExternalResource): OfflineAudio =
|
||||
resource.mockUploadAudio(bot)
|
||||
|
||||
override fun toString(): String {
|
||||
return "Group($id)"
|
||||
}
|
||||
}
|
237
mirai-core-mock/src/internal/contact/MockNormalMemberImpl.kt
Normal file
237
mirai-core-mock/src/internal/contact/MockNormalMemberImpl.kt
Normal file
@ -0,0 +1,237 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.internal.contact
|
||||
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.runBlocking
|
||||
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.Message
|
||||
import net.mamoe.mirai.message.data.MessageChain
|
||||
import net.mamoe.mirai.message.data.OnlineMessageSource
|
||||
import net.mamoe.mirai.mock.MockBot
|
||||
import net.mamoe.mirai.mock.contact.MockFriend
|
||||
import net.mamoe.mirai.mock.contact.MockGroup
|
||||
import net.mamoe.mirai.mock.contact.MockNormalMember
|
||||
import net.mamoe.mirai.mock.internal.msgsrc.OnlineMsgSrcFromGroup
|
||||
import net.mamoe.mirai.mock.internal.msgsrc.OnlineMsgSrcToTemp
|
||||
import net.mamoe.mirai.mock.internal.msgsrc.newMsgSrc
|
||||
import net.mamoe.mirai.mock.utils.broadcastBlocking
|
||||
import net.mamoe.mirai.utils.cast
|
||||
import net.mamoe.mirai.utils.currentTimeSeconds
|
||||
import net.mamoe.mirai.utils.lateinitMutableProperty
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.math.max
|
||||
|
||||
internal class MockNormalMemberImpl(
|
||||
parentCoroutineContext: CoroutineContext,
|
||||
bot: MockBot,
|
||||
id: Long,
|
||||
override val group: MockGroup,
|
||||
permission: MemberPermission,
|
||||
remark: String,
|
||||
nick: String,
|
||||
muteTimeRemaining: Int,
|
||||
joinTimestamp: Int,
|
||||
lastSpeakTimestamp: Int,
|
||||
specialTitle: String,
|
||||
nameCard: String,
|
||||
) : AbstractMockContact(
|
||||
parentCoroutineContext, bot,
|
||||
id
|
||||
), MockNormalMember {
|
||||
override var avatarUrl: String by lateinitMutableProperty {
|
||||
bot.getFriend(id)?.let { return@lateinitMutableProperty it.avatarUrl }
|
||||
runBlocking { MockImage.random(bot).getUrl(bot) }
|
||||
}
|
||||
|
||||
override fun avatarUrl(spec: AvatarSpec): String {
|
||||
return avatarUrl
|
||||
}
|
||||
|
||||
override fun changeAvatarUrl(newAvatar: String) {
|
||||
bot.getFriend(id)?.let { return it.changeAvatarUrl(newAvatar) }
|
||||
this.avatarUrl = newAvatar
|
||||
}
|
||||
|
||||
private inline fun <T> crossFriendAccess(
|
||||
ifExists: (MockFriend) -> T,
|
||||
ifNotExists: () -> T,
|
||||
): T {
|
||||
val f = bot.getFriend(id) ?: return ifNotExists()
|
||||
return ifExists(f)
|
||||
}
|
||||
|
||||
override val mockApi: MockNormalMember.MockApi = object : MockNormalMember.MockApi {
|
||||
override val member: MockNormalMember get() = this@MockNormalMemberImpl
|
||||
override var lastSpeakTimestamp: Int = lastSpeakTimestamp
|
||||
override var joinTimestamp: Int = joinTimestamp
|
||||
override var muteTimeEndTimestamp: Long = currentTimeSeconds() + muteTimeRemaining
|
||||
|
||||
override var nick: String = nick
|
||||
get() = crossFriendAccess(ifExists = { it.nick }, ifNotExists = { field })
|
||||
set(value) {
|
||||
crossFriendAccess(ifExists = { it.mockApi.nick = value }, ifNotExists = { field = value })
|
||||
}
|
||||
|
||||
override var remark: String = remark
|
||||
get() = crossFriendAccess(ifExists = { it.remark }, ifNotExists = { field })
|
||||
set(value) {
|
||||
crossFriendAccess(ifExists = { it.mockApi.remark = value }, ifNotExists = { field = value })
|
||||
}
|
||||
|
||||
override var permission: MemberPermission = permission
|
||||
override var nameCard: String = nameCard
|
||||
override var specialTitle: String = specialTitle
|
||||
override var avatarUrl: String
|
||||
get() = this@MockNormalMemberImpl.avatarUrl
|
||||
set(value) {
|
||||
this@MockNormalMemberImpl.avatarUrl = value
|
||||
|
||||
bot.getFriend(this@MockNormalMemberImpl.id)?.let { f ->
|
||||
f.mockApi.avatarUrl = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val permission: MemberPermission
|
||||
get() = mockApi.permission
|
||||
|
||||
override val joinTimestamp: Int
|
||||
get() = mockApi.joinTimestamp
|
||||
|
||||
override val lastSpeakTimestamp: Int
|
||||
get() = mockApi.lastSpeakTimestamp
|
||||
|
||||
override val muteTimeRemaining: Int
|
||||
get() = max((mockApi.muteTimeEndTimestamp - currentTimeSeconds()).toInt(), 0)
|
||||
|
||||
override val remark: String
|
||||
get() = mockApi.remark
|
||||
|
||||
override var nameCard: String
|
||||
get() = mockApi.nameCard
|
||||
set(value) {
|
||||
if (!group.botPermission.isOperator()) {
|
||||
throw PermissionDeniedException("Bot don't have permission to change the namecard of $this")
|
||||
}
|
||||
MemberCardChangeEvent(mockApi.nameCard, value, this).broadcastBlocking()
|
||||
mockApi.nameCard = value
|
||||
}
|
||||
|
||||
override var specialTitle: String
|
||||
get() = mockApi.specialTitle
|
||||
set(value) {
|
||||
if (group.botPermission != MemberPermission.OWNER) {
|
||||
throw PermissionDeniedException("Bot is not the owner of $group so bot cannot change the specialTitle of $this")
|
||||
}
|
||||
MemberSpecialTitleChangeEvent(mockApi.specialTitle, value, this, group.botAsMember).broadcastBlocking()
|
||||
mockApi.specialTitle = value
|
||||
}
|
||||
|
||||
override val nick: String
|
||||
get() = mockApi.nick
|
||||
|
||||
override suspend fun unmute() {
|
||||
requireBotPermissionHigherThanThis("unmute")
|
||||
mockApi.muteTimeEndTimestamp = 0
|
||||
MemberUnmuteEvent(this, null)
|
||||
}
|
||||
|
||||
override suspend fun kick(message: String, block: Boolean) {
|
||||
kick(message)
|
||||
}
|
||||
|
||||
override suspend fun kick(message: String) {
|
||||
requireBotPermissionHigherThanThis("kick")
|
||||
if (group.members.delegate.remove(this)) {
|
||||
MemberLeaveEvent.Kick(this, group.botAsMember).broadcastBlocking()
|
||||
cancel(CancellationException("Member kicked: $message"))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun modifyAdmin(operation: Boolean) {
|
||||
if (group.botPermission != MemberPermission.OWNER) {
|
||||
throw PermissionDeniedException("Bot is not the owner of group ${group.id}, can't modify the permission of $id($permission")
|
||||
}
|
||||
if (operation && permission > MemberPermission.MEMBER) return
|
||||
|
||||
if (permission == MemberPermission.OWNER) {
|
||||
throw IllegalArgumentException("Not allowed modify permission of owner ($id, $permission)")
|
||||
}
|
||||
val newPerm = if (operation) MemberPermission.ADMINISTRATOR else MemberPermission.MEMBER
|
||||
if (newPerm != permission) {
|
||||
val oldPerm = permission
|
||||
mockApi.permission = oldPerm
|
||||
MemberPermissionChangeEvent(this, oldPerm, newPerm).broadcast()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendMessage(message: Message): MessageReceipt<NormalMember> {
|
||||
return super<AbstractMockContact>.sendMessage(message).cast()
|
||||
}
|
||||
|
||||
override suspend fun mute(durationSeconds: Int) {
|
||||
requireBotPermissionHigherThanThis("mute")
|
||||
require(durationSeconds > 0) {
|
||||
"$durationSeconds < 0"
|
||||
}
|
||||
mockApi.muteTimeEndTimestamp = currentTimeSeconds() + durationSeconds
|
||||
MemberMuteEvent(this, durationSeconds, null)
|
||||
}
|
||||
|
||||
override suspend fun broadcastMute(target: MockNormalMember, durationSeconds: Int) {
|
||||
target.mockApi.muteTimeEndTimestamp = currentTimeSeconds() + durationSeconds
|
||||
if (target.id == bot.id) {
|
||||
if (durationSeconds == 0) {
|
||||
BotUnmuteEvent(this)
|
||||
} else {
|
||||
BotMuteEvent(durationSeconds, this)
|
||||
}
|
||||
} else {
|
||||
if (durationSeconds == 0) {
|
||||
MemberUnmuteEvent(target, this)
|
||||
} else {
|
||||
MemberMuteEvent(target, durationSeconds, this)
|
||||
}
|
||||
}.broadcast()
|
||||
}
|
||||
|
||||
override suspend fun says(message: MessageChain): MessageChain {
|
||||
val src = newMsgSrc(true, message) { ids, internalIds, time ->
|
||||
mockApi.lastSpeakTimestamp = time
|
||||
OnlineMsgSrcFromGroup(ids, internalIds, time, message, bot, this)
|
||||
}
|
||||
val msg = src.withMessage(message)
|
||||
GroupMessageEvent(nameCardOrNick, permission, this, msg, src.time).broadcast()
|
||||
return msg
|
||||
}
|
||||
|
||||
override fun newMessagePreSend(message: Message): MessagePreSendEvent {
|
||||
return GroupTempMessagePreSendEvent(this, message)
|
||||
}
|
||||
|
||||
override suspend fun postMessagePreSend(message: MessageChain, receipt: MessageReceipt<*>) {
|
||||
GroupTempMessagePostSendEvent(this, message, null, receipt.cast()).broadcast()
|
||||
}
|
||||
|
||||
override fun newMessageSource(message: MessageChain): OnlineMessageSource.Outgoing {
|
||||
return newMsgSrc(false, message) { ids, internalIds, time ->
|
||||
OnlineMsgSrcToTemp(ids, internalIds, time, message, bot, bot, this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "NormalMember($id)"
|
||||
}
|
||||
}
|
108
mirai-core-mock/src/internal/contact/MockStrangerImpl.kt
Normal file
108
mirai-core-mock/src/internal/contact/MockStrangerImpl.kt
Normal file
@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.internal.contact
|
||||
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.mamoe.mirai.contact.AvatarSpec
|
||||
import net.mamoe.mirai.contact.Stranger
|
||||
import net.mamoe.mirai.event.broadcast
|
||||
import net.mamoe.mirai.event.events.*
|
||||
import net.mamoe.mirai.message.MessageReceipt
|
||||
import net.mamoe.mirai.message.data.Message
|
||||
import net.mamoe.mirai.message.data.MessageChain
|
||||
import net.mamoe.mirai.message.data.OnlineMessageSource
|
||||
import net.mamoe.mirai.mock.MockBot
|
||||
import net.mamoe.mirai.mock.contact.MockStranger
|
||||
import net.mamoe.mirai.mock.internal.msgsrc.OnlineMsgSrcFromStranger
|
||||
import net.mamoe.mirai.mock.internal.msgsrc.OnlineMsgSrcToStranger
|
||||
import net.mamoe.mirai.mock.internal.msgsrc.newMsgSrc
|
||||
import net.mamoe.mirai.utils.cast
|
||||
import net.mamoe.mirai.utils.lateinitMutableProperty
|
||||
import java.util.concurrent.CancellationException
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
internal class MockStrangerImpl(
|
||||
parentCoroutineContext: CoroutineContext,
|
||||
bot: MockBot,
|
||||
id: Long,
|
||||
|
||||
remark: String,
|
||||
nick: String
|
||||
) : AbstractMockContact(parentCoroutineContext, bot, id), MockStranger {
|
||||
|
||||
override val mockApi: MockStranger.MockApi = object : MockStranger.MockApi {
|
||||
override val contact: MockStranger get() = this@MockStrangerImpl
|
||||
override var nick: String = nick
|
||||
override var remark: String = remark
|
||||
override var avatarUrl: String
|
||||
get() = this@MockStrangerImpl.avatarUrl
|
||||
set(value) {
|
||||
this@MockStrangerImpl.avatarUrl = value
|
||||
|
||||
bot.getFriend(this@MockStrangerImpl.id)?.let { f ->
|
||||
f.mockApi.avatarUrl = value
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
override var avatarUrl: String by lateinitMutableProperty {
|
||||
bot.getFriend(id)?.let { return@lateinitMutableProperty it.avatarUrl }
|
||||
runBlocking { MockImage.random(bot).getUrl(bot) }
|
||||
}
|
||||
|
||||
override fun avatarUrl(spec: AvatarSpec): String {
|
||||
return avatarUrl
|
||||
}
|
||||
|
||||
override fun changeAvatarUrl(newAvatar: String) {
|
||||
this.avatarUrl = newAvatar
|
||||
bot.getFriend(id)?.let { return it.changeAvatarUrl(newAvatar) }
|
||||
}
|
||||
|
||||
override val nick: String
|
||||
get() = mockApi.nick
|
||||
override val remark: String
|
||||
get() = mockApi.remark
|
||||
|
||||
override fun newMessagePreSend(message: Message): MessagePreSendEvent {
|
||||
return StrangerMessagePreSendEvent(this, message)
|
||||
}
|
||||
|
||||
override suspend fun postMessagePreSend(message: MessageChain, receipt: MessageReceipt<*>) {
|
||||
StrangerMessagePostSendEvent(this, message, null, receipt.cast()).broadcast()
|
||||
}
|
||||
|
||||
override fun newMessageSource(message: MessageChain): OnlineMessageSource.Outgoing {
|
||||
return newMsgSrc(false, message) { ids, internalIds, time ->
|
||||
OnlineMsgSrcToStranger(ids, internalIds, time, message, bot, bot, this)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendMessage(message: Message): MessageReceipt<Stranger> {
|
||||
return super<AbstractMockContact>.sendMessage(message).cast()
|
||||
}
|
||||
|
||||
override suspend fun delete() {
|
||||
if (bot.strangers.delegate.remove(this)) {
|
||||
StrangerRelationChangeEvent.Deleted(this).broadcast()
|
||||
cancel(CancellationException("Stranger deleted"))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun says(message: MessageChain): MessageChain {
|
||||
val src = newMsgSrc(true, message) { ids, internalIds, time ->
|
||||
OnlineMsgSrcFromStranger(ids, internalIds, time, message, bot, this)
|
||||
}
|
||||
val msg = src.withMessage(message)
|
||||
StrangerMessageEvent(this, msg, src.time).broadcast()
|
||||
return msg
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.internal.contact.friendfroup
|
||||
|
||||
import net.mamoe.mirai.contact.Friend
|
||||
import net.mamoe.mirai.contact.friendgroup.FriendGroup
|
||||
import net.mamoe.mirai.mock.MockBot
|
||||
import net.mamoe.mirai.mock.contact.MockFriend
|
||||
import net.mamoe.mirai.mock.utils.mock
|
||||
import net.mamoe.mirai.utils.cast
|
||||
|
||||
internal class MockFriendGroup(
|
||||
private val bot: MockBot,
|
||||
override val id: Int,
|
||||
override var name: String,
|
||||
) : FriendGroup {
|
||||
override val friends: Collection<Friend> = object : AbstractCollection<Friend>() {
|
||||
|
||||
private val seq = sequence<Friend> {
|
||||
bot.friends.forEach { mf ->
|
||||
if (mf.mockApi.friendGroupId == id) {
|
||||
yield(mf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun isEmpty(): Boolean {
|
||||
return bot.friends.none { it.mockApi.friendGroupId == id }
|
||||
}
|
||||
|
||||
override fun contains(element: Friend): Boolean {
|
||||
if (element !is MockFriend) return false
|
||||
if (element.bot !== bot) return false
|
||||
return element.mockApi.friendGroupId == id
|
||||
}
|
||||
|
||||
override val size: Int
|
||||
get() = bot.friends.count { it.mockApi.friendGroupId == id }
|
||||
|
||||
override fun iterator(): Iterator<Friend> {
|
||||
return seq.iterator()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override suspend fun renameTo(newName: String): Boolean {
|
||||
name = newName
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun moveIn(friend: Friend): Boolean {
|
||||
val api = friend.mock().mockApi
|
||||
if (api.friendGroupId == id) return false
|
||||
|
||||
api.friendGroupId = id
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun delete(): Boolean {
|
||||
if (id == 0) return false
|
||||
if (bot.friendGroups.cast<MockFriendGroups>().groups.remove(this)) {
|
||||
friends.forEach { it.mock().mockApi.friendGroupId = 0 }
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override val count: Int get() = friends.size
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.internal.contact.friendfroup
|
||||
|
||||
import net.mamoe.mirai.contact.friendgroup.FriendGroup
|
||||
import net.mamoe.mirai.contact.friendgroup.FriendGroups
|
||||
import net.mamoe.mirai.mock.MockBot
|
||||
import net.mamoe.mirai.utils.ConcurrentLinkedDeque
|
||||
import net.mamoe.mirai.utils.asImmutable
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.random.Random
|
||||
|
||||
internal class MockFriendGroups(
|
||||
private val bot: MockBot,
|
||||
) : FriendGroups {
|
||||
internal val groups = ConcurrentLinkedDeque<MockFriendGroup>()
|
||||
private val defaultX = MockFriendGroup(bot, 0, "默认分组")
|
||||
|
||||
override val default: FriendGroup get() = defaultX
|
||||
|
||||
init {
|
||||
groups.addLast(defaultX)
|
||||
}
|
||||
|
||||
override suspend fun create(name: String): FriendGroup {
|
||||
var newId: Int
|
||||
do {
|
||||
newId = Random.nextInt().absoluteValue
|
||||
} while (groups.any { it.id == newId })
|
||||
|
||||
val newG = MockFriendGroup(bot, newId, name)
|
||||
groups.addLast(newG)
|
||||
return newG
|
||||
}
|
||||
|
||||
override fun get(id: Int): FriendGroup? {
|
||||
if (id == 0) return defaultX
|
||||
return groups.find { it.id == id }
|
||||
}
|
||||
|
||||
override fun asCollection(): Collection<FriendGroup> {
|
||||
return groups.asImmutable()
|
||||
}
|
||||
|
||||
fun findOrDefault(friendGroupId: Int): FriendGroup {
|
||||
return get(friendGroupId) ?: defaultX
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.internal.contact.roaming
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import net.mamoe.mirai.contact.Friend
|
||||
import net.mamoe.mirai.contact.Group
|
||||
import net.mamoe.mirai.contact.Stranger
|
||||
import net.mamoe.mirai.contact.roaming.RoamingMessageFilter
|
||||
import net.mamoe.mirai.contact.roaming.RoamingMessages
|
||||
import net.mamoe.mirai.contact.roaming.RoamingSupported
|
||||
import net.mamoe.mirai.message.data.MessageChain
|
||||
import net.mamoe.mirai.message.data.MessageSourceKind
|
||||
import net.mamoe.mirai.mock.internal.MockBotImpl
|
||||
import net.mamoe.mirai.utils.JavaFriendlyAPI
|
||||
import net.mamoe.mirai.utils.cast
|
||||
import java.util.stream.Stream
|
||||
import kotlin.streams.asStream
|
||||
|
||||
internal class MockRoamingMessages(
|
||||
internal val contact: RoamingSupported,
|
||||
) : RoamingMessages {
|
||||
override suspend fun getMessagesIn(
|
||||
timeStart: Long,
|
||||
timeEnd: Long,
|
||||
filter: RoamingMessageFilter?
|
||||
): Flow<MessageChain> {
|
||||
return getMsg(timeStart, timeEnd, filter).asFlow()
|
||||
}
|
||||
|
||||
private fun getMsg(
|
||||
timeStart: Long,
|
||||
timeEnd: Long,
|
||||
filter: RoamingMessageFilter?
|
||||
): Sequence<MessageChain> {
|
||||
val msgDb = contact.bot.cast<MockBotImpl>().msgDatabase
|
||||
return msgDb.queryMessageInfosBy(
|
||||
contact.id,
|
||||
when (contact) {
|
||||
is Friend -> MessageSourceKind.FRIEND
|
||||
is Group -> MessageSourceKind.GROUP
|
||||
is Stranger -> MessageSourceKind.STRANGER
|
||||
else -> error(contact.javaClass.toString())
|
||||
},
|
||||
contact,
|
||||
timeStart,
|
||||
timeEnd,
|
||||
filter ?: RoamingMessageFilter.ANY
|
||||
).map { it.message }
|
||||
}
|
||||
|
||||
@JavaFriendlyAPI
|
||||
override suspend fun getMessagesStream(
|
||||
timeStart: Long,
|
||||
timeEnd: Long,
|
||||
filter: RoamingMessageFilter?
|
||||
): Stream<MessageChain> {
|
||||
return getMsg(timeStart, timeEnd, filter).asStream()
|
||||
}
|
||||
}
|
140
mirai-core-mock/src/internal/contact/util.kt
Normal file
140
mirai-core-mock/src/internal/contact/util.kt
Normal file
@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "CANNOT_OVERRIDE_INVISIBLE_MEMBER")
|
||||
|
||||
package net.mamoe.mirai.mock.internal.contact
|
||||
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.contact.Group
|
||||
import net.mamoe.mirai.contact.Member
|
||||
import net.mamoe.mirai.contact.PermissionDeniedException
|
||||
import net.mamoe.mirai.internal.contact.uin
|
||||
import net.mamoe.mirai.internal.message.data.OnlineAudioImpl
|
||||
import net.mamoe.mirai.internal.message.image.DeferredOriginUrlAware
|
||||
import net.mamoe.mirai.message.data.*
|
||||
import net.mamoe.mirai.mock.MockBot
|
||||
import net.mamoe.mirai.mock.contact.MockGroup
|
||||
import net.mamoe.mirai.mock.utils.mock
|
||||
import net.mamoe.mirai.mock.utils.plusHttpSubpath
|
||||
import net.mamoe.mirai.mock.utils.randomImageContent
|
||||
import net.mamoe.mirai.utils.ExternalResource
|
||||
import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
|
||||
import net.mamoe.mirai.utils.cast
|
||||
import net.mamoe.mirai.utils.toUHexString
|
||||
|
||||
|
||||
internal fun Member.requireBotPermissionHigherThanThis(msg: String) {
|
||||
if (this.permission < this.group.botPermission) return
|
||||
|
||||
throw PermissionDeniedException("bot current permission ${group.botPermission} can't modify $id($permission), $msg")
|
||||
}
|
||||
|
||||
internal fun MessageSource.withMessage(msg: Message): MessageChain = buildMessageChain {
|
||||
add(this@withMessage)
|
||||
if (msg is MessageChain) {
|
||||
msg.forEach { sub ->
|
||||
if (sub !is MessageSource) {
|
||||
add(sub)
|
||||
}
|
||||
}
|
||||
} else if (msg !is MessageSource) {
|
||||
add(msg)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
internal suspend fun ExternalResource.mockUploadAudio(bot: MockBot) = inResource {
|
||||
OfflineAudio(
|
||||
filename = md5.toUHexString() + ".amr",
|
||||
fileMd5 = md5,
|
||||
fileSize = size,
|
||||
codec = AudioCodec.SILK,
|
||||
extraData = null,
|
||||
)
|
||||
}
|
||||
|
||||
internal suspend fun ExternalResource.mockUploadVoice(bot: MockBot) = kotlin.run {
|
||||
val md5 = this.md5
|
||||
val size = this.size
|
||||
@Suppress("DEPRECATION", "DEPRECATION_ERROR")
|
||||
net.mamoe.mirai.message.data.Voice(
|
||||
fileName = md5.toUHexString() + ".amr",
|
||||
md5 = md5,
|
||||
fileSize = size,
|
||||
_url = bot.tmpResourceServer.uploadResourceAndGetUrl(this)
|
||||
)
|
||||
}
|
||||
|
||||
internal const val AQQ_RECALL_FAILED_MESSAGE: String = "No message meets the requirements"
|
||||
|
||||
internal val Group.mockUin: Long
|
||||
get() = when (this) {
|
||||
is MockGroup -> this.uin
|
||||
else -> this.uin
|
||||
}
|
||||
|
||||
|
||||
internal suspend fun ExternalResource.mockImplUploadAudioAsOnline(bot: MockBot): OnlineAudio {
|
||||
val md5 = this.md5
|
||||
val size = this.size
|
||||
return OnlineAudioImpl(
|
||||
filename = md5.toUHexString() + ".amr",
|
||||
fileMd5 = md5,
|
||||
fileSize = size,
|
||||
codec = AudioCodec.SILK,
|
||||
url = bot.tmpResourceServer.uploadResourceAndGetUrl(this),
|
||||
length = size,
|
||||
originalPtt = null,
|
||||
)
|
||||
}
|
||||
|
||||
internal class MockImage(
|
||||
override val imageId: String,
|
||||
private val urlPath: String,
|
||||
override val width: Int = 0,
|
||||
override val height: Int = 0,
|
||||
override val size: Long = 0,
|
||||
override val imageType: ImageType = ImageType.UNKNOWN,
|
||||
) : DeferredOriginUrlAware, Image {
|
||||
|
||||
companion object {
|
||||
// create a mockImage with random content
|
||||
internal suspend fun random(bot: MockBot): MockImage {
|
||||
val text = Image.randomImageContent()
|
||||
return bot.uploadMockImage(text.toExternalResource().toAutoCloseable()).cast()
|
||||
}
|
||||
}
|
||||
|
||||
private val _stringValue: String? by lazy(LazyThreadSafetyMode.NONE) { "[mirai:image:$imageId]" }
|
||||
|
||||
override fun getUrl(bot: Bot): String {
|
||||
if (urlPath.startsWith("http"))
|
||||
return urlPath
|
||||
return bot.mock().tmpResourceServer.storageRoot.toString().plusHttpSubpath(urlPath)
|
||||
}
|
||||
|
||||
override fun toString(): String = _stringValue!!
|
||||
override fun contentToString(): String = if (isEmoji) {
|
||||
"[动画表情]"
|
||||
} else {
|
||||
"[图片]"
|
||||
}
|
||||
|
||||
override fun appendMiraiCodeTo(builder: StringBuilder) {
|
||||
builder.append("[mirai:image:").append(imageId).append("]")
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = imageId.hashCode()
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other === this) return true
|
||||
if (other !is Image) return false
|
||||
return this.imageId == other.imageId
|
||||
}
|
||||
}
|
102
mirai-core-mock/src/internal/db/MsgDatabaseImpl.kt
Normal file
102
mirai-core-mock/src/internal/db/MsgDatabaseImpl.kt
Normal file
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.internal.db
|
||||
|
||||
import net.mamoe.mirai.contact.Contact
|
||||
import net.mamoe.mirai.contact.roaming.RoamingMessage
|
||||
import net.mamoe.mirai.contact.roaming.RoamingMessageFilter
|
||||
import net.mamoe.mirai.message.data.MessageChain
|
||||
import net.mamoe.mirai.message.data.MessageSourceKind
|
||||
import net.mamoe.mirai.mock.database.MessageDatabase
|
||||
import net.mamoe.mirai.mock.database.MessageInfo
|
||||
import net.mamoe.mirai.mock.database.mockMsgDatabaseId
|
||||
import java.util.concurrent.ConcurrentLinkedDeque
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.random.Random
|
||||
|
||||
internal class MsgDatabaseImpl : MessageDatabase {
|
||||
override fun disconnect() {}
|
||||
override fun connect() {}
|
||||
|
||||
val db = ConcurrentLinkedDeque<MessageInfo>()
|
||||
val idCounter1 = AtomicInteger(Random.nextInt())
|
||||
val idCounter2 = AtomicInteger(Random.nextInt())
|
||||
|
||||
override fun newMessageInfo(
|
||||
sender: Long, subject: Long,
|
||||
kind: MessageSourceKind,
|
||||
time: Long,
|
||||
message: MessageChain,
|
||||
): MessageInfo {
|
||||
val dbid = mockMsgDatabaseId(idCounter1.getAndIncrement(), idCounter2.getAndDecrement())
|
||||
val info = MessageInfo(
|
||||
mixinedMsgId = dbid,
|
||||
sender = sender,
|
||||
subject = subject,
|
||||
kind = kind,
|
||||
time = time,
|
||||
message = message,
|
||||
)
|
||||
db.add(info)
|
||||
return info
|
||||
}
|
||||
|
||||
override fun queryMessageInfo(msgId: Long): MessageInfo? {
|
||||
return db.firstOrNull { it.mixinedMsgId == msgId }
|
||||
}
|
||||
|
||||
override fun removeMessageInfo(msgId: Long) {
|
||||
db.removeIf { it.mixinedMsgId == msgId }
|
||||
}
|
||||
|
||||
override fun queryMessageInfosBy(
|
||||
subject: Long, kind: MessageSourceKind,
|
||||
contact: Contact,
|
||||
timeStart: Long,
|
||||
timeEnd: Long,
|
||||
filter: RoamingMessageFilter
|
||||
): Sequence<MessageInfo> {
|
||||
if (timeEnd < timeStart) return emptySequence()
|
||||
return sequence<MessageInfo> {
|
||||
val rm = object : RoamingMessage {
|
||||
override val contact: Contact get() = contact
|
||||
override var sender: Long = -1
|
||||
override var target: Long = -1
|
||||
override var time: Long = -1
|
||||
override val ids: IntArray = intArrayOf(-1)
|
||||
override val internalIds: IntArray = intArrayOf(-1)
|
||||
}
|
||||
for (msgInfo in db) {
|
||||
if (msgInfo.kind != kind) continue
|
||||
if (msgInfo.time < timeStart) continue
|
||||
if (msgInfo.time > timeEnd) continue
|
||||
if (msgInfo.subject != subject) continue
|
||||
|
||||
rm.sender = msgInfo.sender
|
||||
if (kind != MessageSourceKind.GROUP) {
|
||||
if (msgInfo.sender == contact.id) {
|
||||
rm.target = contact.bot.id
|
||||
} else {
|
||||
rm.target = msgInfo.subject
|
||||
}
|
||||
} else {
|
||||
rm.target = msgInfo.subject
|
||||
}
|
||||
rm.time = msgInfo.time
|
||||
rm.ids[0] = msgInfo.id
|
||||
rm.internalIds[0] = msgInfo.internal
|
||||
|
||||
if (filter.invoke(rm)) {
|
||||
yield(msgInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
176
mirai-core-mock/src/internal/msgsrc/OnlineMsgSrc.kt
Normal file
176
mirai-core-mock/src/internal/msgsrc/OnlineMsgSrc.kt
Normal file
@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.internal.msgsrc
|
||||
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.contact.*
|
||||
import net.mamoe.mirai.message.data.MessageChain
|
||||
import net.mamoe.mirai.message.data.MessageSourceKind
|
||||
import net.mamoe.mirai.message.data.OnlineMessageSource
|
||||
import net.mamoe.mirai.mock.internal.contact.AbstractMockContact
|
||||
import net.mamoe.mirai.utils.currentTimeSeconds
|
||||
|
||||
internal class OnlineMsgSrcToGroup(
|
||||
override val ids: IntArray,
|
||||
override val internalIds: IntArray,
|
||||
override val time: Int,
|
||||
override val originalMessage: MessageChain,
|
||||
override val bot: Bot,
|
||||
override val sender: Bot,
|
||||
override val target: Group
|
||||
) : OnlineMessageSource.Outgoing.ToGroup() {
|
||||
override val isOriginalMessageInitialized: Boolean get() = true
|
||||
}
|
||||
|
||||
internal class OnlineMsgSrcToFriend(
|
||||
override val ids: IntArray,
|
||||
override val internalIds: IntArray,
|
||||
override val time: Int,
|
||||
override val originalMessage: MessageChain,
|
||||
override val bot: Bot,
|
||||
override val sender: Bot,
|
||||
override val target: Friend
|
||||
) : OnlineMessageSource.Outgoing.ToFriend() {
|
||||
override val isOriginalMessageInitialized: Boolean get() = true
|
||||
}
|
||||
|
||||
internal class OnlineMsgSrcToStranger(
|
||||
override val ids: IntArray,
|
||||
override val internalIds: IntArray,
|
||||
override val time: Int,
|
||||
override val originalMessage: MessageChain,
|
||||
override val bot: Bot,
|
||||
override val sender: Bot,
|
||||
override val target: Stranger
|
||||
) : OnlineMessageSource.Outgoing.ToStranger() {
|
||||
override val isOriginalMessageInitialized: Boolean get() = true
|
||||
}
|
||||
|
||||
internal class OnlineMsgSrcToTemp(
|
||||
override val ids: IntArray,
|
||||
override val internalIds: IntArray,
|
||||
override val time: Int,
|
||||
override val originalMessage: MessageChain,
|
||||
override val bot: Bot,
|
||||
override val sender: Bot,
|
||||
override val target: Member
|
||||
) : OnlineMessageSource.Outgoing.ToTemp() {
|
||||
override val isOriginalMessageInitialized: Boolean get() = true
|
||||
}
|
||||
|
||||
internal class OnlineMsgFromGroup(
|
||||
override val ids: IntArray,
|
||||
override val internalIds: IntArray,
|
||||
override val time: Int,
|
||||
override val originalMessage: MessageChain,
|
||||
override val bot: Bot,
|
||||
override val sender: Member
|
||||
) : OnlineMessageSource.Incoming.FromGroup() {
|
||||
override val isOriginalMessageInitialized: Boolean get() = true
|
||||
}
|
||||
|
||||
internal class OnlineMsgSrcFromFriend(
|
||||
override val ids: IntArray,
|
||||
override val internalIds: IntArray,
|
||||
override val time: Int,
|
||||
override val originalMessage: MessageChain,
|
||||
override val bot: Bot,
|
||||
override val sender: Friend
|
||||
) : OnlineMessageSource.Incoming.FromFriend() {
|
||||
override val isOriginalMessageInitialized: Boolean get() = true
|
||||
}
|
||||
|
||||
internal class OnlineMsgSrcFromStranger(
|
||||
override val ids: IntArray,
|
||||
override val internalIds: IntArray,
|
||||
override val time: Int,
|
||||
override val originalMessage: MessageChain,
|
||||
override val bot: Bot,
|
||||
override val sender: Stranger
|
||||
) : OnlineMessageSource.Incoming.FromStranger() {
|
||||
override val isOriginalMessageInitialized: Boolean get() = true
|
||||
}
|
||||
|
||||
internal class OnlineMsgSrcFromTemp(
|
||||
override val ids: IntArray,
|
||||
override val internalIds: IntArray,
|
||||
override val time: Int,
|
||||
override val originalMessage: MessageChain,
|
||||
override val bot: Bot,
|
||||
override val sender: Member
|
||||
) : OnlineMessageSource.Incoming.FromTemp() {
|
||||
override val isOriginalMessageInitialized: Boolean get() = true
|
||||
}
|
||||
|
||||
internal class OnlineMsgSrcFromGroup(
|
||||
override val ids: IntArray,
|
||||
override val internalIds: IntArray,
|
||||
override val time: Int,
|
||||
override val originalMessage: MessageChain,
|
||||
override val bot: Bot,
|
||||
override val sender: Member
|
||||
) : OnlineMessageSource.Incoming.FromGroup() {
|
||||
override val isOriginalMessageInitialized: Boolean get() = true
|
||||
}
|
||||
|
||||
internal typealias MsgSrcConstructor<R> = (
|
||||
ids: IntArray,
|
||||
internalIds: IntArray,
|
||||
time: Int,
|
||||
) -> R
|
||||
|
||||
internal inline fun <R> AbstractMockContact.newMsgSrc(
|
||||
isSaying: Boolean,
|
||||
messageChain: MessageChain,
|
||||
time: Long = currentTimeSeconds(),
|
||||
constructor: MsgSrcConstructor<R>,
|
||||
): R {
|
||||
val db = bot.msgDatabase
|
||||
val info = if (isSaying) {
|
||||
db.newMessageInfo(
|
||||
sender = id,
|
||||
subject = when (this) {
|
||||
is Member -> group.id
|
||||
is Stranger,
|
||||
is Friend,
|
||||
-> this.id
|
||||
else -> error("Invalid contact: $this")
|
||||
},
|
||||
kind = when (this) {
|
||||
is Member -> MessageSourceKind.GROUP
|
||||
is Stranger -> MessageSourceKind.STRANGER
|
||||
is Friend -> MessageSourceKind.FRIEND
|
||||
else -> error("Invalid contact: $this")
|
||||
},
|
||||
message = messageChain,
|
||||
time = time,
|
||||
)
|
||||
} else {
|
||||
db.newMessageInfo(
|
||||
sender = bot.id,
|
||||
subject = this.id,
|
||||
kind = when (this) {
|
||||
is NormalMember -> MessageSourceKind.TEMP
|
||||
is Stranger -> MessageSourceKind.STRANGER
|
||||
is Friend -> MessageSourceKind.FRIEND
|
||||
is Group -> MessageSourceKind.GROUP
|
||||
else -> error("Invalid contact: $this")
|
||||
},
|
||||
message = messageChain,
|
||||
time = time,
|
||||
)
|
||||
}
|
||||
return constructor(
|
||||
intArrayOf(info.id),
|
||||
intArrayOf(info.internal),
|
||||
info.time.toInt(),
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
@file:Suppress("invisible_member", "INVISIBLE_REFERENCE")
|
||||
|
||||
package net.mamoe.mirai.mock.internal.remotefile.absolutefile
|
||||
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import net.mamoe.mirai.contact.FileSupported
|
||||
import net.mamoe.mirai.contact.file.AbsoluteFile
|
||||
import net.mamoe.mirai.contact.file.AbsoluteFolder
|
||||
import net.mamoe.mirai.internal.message.data.FileMessageImpl
|
||||
import net.mamoe.mirai.message.data.FileMessage
|
||||
import net.mamoe.mirai.mock.internal.remotefile.remotefile.MockRemoteFile
|
||||
import net.mamoe.mirai.mock.resserver.MockServerRemoteFile
|
||||
import net.mamoe.mirai.mock.utils.mock
|
||||
|
||||
internal class MockAbsoluteFile(
|
||||
override val sha1: ByteArray,
|
||||
override val md5: ByteArray,
|
||||
private val files: MockRemoteFiles,
|
||||
override var parent: AbsoluteFolder?,
|
||||
override val id: String,
|
||||
override var name: String,
|
||||
override var absolutePath: String,
|
||||
override val contact: FileSupported = files.contact,
|
||||
override var expiryTime: Long = 0L,
|
||||
override val size: Long = 0,
|
||||
override val isFile: Boolean = true,
|
||||
override val isFolder: Boolean = false,
|
||||
override val uploadTime: Long = 0,
|
||||
override var lastModifiedTime: Long = 0,
|
||||
override val uploaderId: Long = 0
|
||||
) : AbsoluteFile {
|
||||
@Volatile
|
||||
private var _exists = true
|
||||
override suspend fun moveTo(folder: AbsoluteFolder): Boolean {
|
||||
if (!exists()) return false
|
||||
files.fileSystem.resolveById(id)!!.moveTo(files.fileSystem.resolveById(folder.id)!!)
|
||||
this.parent = folder
|
||||
refresh()
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun getUrl(): String =
|
||||
files.contact.bot.mock().tmpResourceServer.resolveHttpUrlByPath(
|
||||
files.fileSystem.resolveById(id)!!.resolveNativePath()
|
||||
).toString()
|
||||
|
||||
override fun toMessage(): FileMessage {
|
||||
//todo busId
|
||||
return FileMessageImpl(id, 0, name, size)
|
||||
}
|
||||
|
||||
override suspend fun refreshed(): AbsoluteFile? =
|
||||
parent!!.files().filter { it.id == id }.firstOrNull()
|
||||
|
||||
|
||||
private fun canModify(resolved: MockServerRemoteFile): Boolean {
|
||||
return MockRemoteFile.canModify(resolved, contact)
|
||||
}
|
||||
|
||||
override suspend fun exists(): Boolean = _exists
|
||||
|
||||
override suspend fun renameTo(newName: String): Boolean {
|
||||
if (!exists()) return false
|
||||
val resolved = files.fileSystem.resolveById(id) ?: return false
|
||||
if (!canModify(resolved)) return false
|
||||
if (resolved.rename(newName)) {
|
||||
refresh()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override suspend fun delete(): Boolean {
|
||||
if (!exists()) return false
|
||||
val resolved = files.fileSystem.resolveById(id) ?: return false
|
||||
if (!canModify(resolved)) return false
|
||||
if (resolved.delete()) {
|
||||
_exists = false
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override suspend fun refresh(): Boolean {
|
||||
val new = refreshed()
|
||||
if (new == null) {
|
||||
_exists = false
|
||||
return false
|
||||
}
|
||||
_exists = true
|
||||
this.parent = new.parent
|
||||
this.expiryTime = new.expiryTime
|
||||
this.name = new.name
|
||||
this.lastModifiedTime = new.lastModifiedTime
|
||||
this.absolutePath = new.absolutePath
|
||||
return true
|
||||
}
|
||||
|
||||
override fun toString(): String = "MockAbsoluteFile(id=$id,absolutePath=$absolutePath,name=$name)"
|
||||
override fun equals(other: Any?): Boolean =
|
||||
other != null && other is AbsoluteFile && other.id == id
|
||||
|
||||
override fun hashCode(): Int {
|
||||
// from absoluteFileImpl
|
||||
var result = super.hashCode()
|
||||
result = 31 * result + expiryTime.hashCode()
|
||||
result = 31 * result + size.hashCode()
|
||||
result = 31 * result + sha1.contentHashCode()
|
||||
result = 31 * result + md5.contentHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
@ -0,0 +1,266 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
@file: Suppress("invisible_member", "invisible_reference")
|
||||
|
||||
package net.mamoe.mirai.mock.internal.remotefile.absolutefile
|
||||
|
||||
import kotlinx.coroutines.flow.*
|
||||
import net.mamoe.mirai.contact.FileSupported
|
||||
import net.mamoe.mirai.contact.PermissionDeniedException
|
||||
import net.mamoe.mirai.contact.file.AbsoluteFile
|
||||
import net.mamoe.mirai.contact.file.AbsoluteFileFolder
|
||||
import net.mamoe.mirai.contact.file.AbsoluteFolder
|
||||
import net.mamoe.mirai.contact.isOperator
|
||||
import net.mamoe.mirai.internal.utils.FileSystem
|
||||
import net.mamoe.mirai.mock.contact.MockGroup
|
||||
import net.mamoe.mirai.mock.internal.remotefile.remotefile.MockRemoteFile
|
||||
import net.mamoe.mirai.mock.resserver.MockServerRemoteFile
|
||||
import net.mamoe.mirai.utils.ExternalResource
|
||||
import net.mamoe.mirai.utils.JavaFriendlyAPI
|
||||
import net.mamoe.mirai.utils.ProgressionCallback
|
||||
import net.mamoe.mirai.utils.safeCast
|
||||
import java.util.stream.Stream
|
||||
import kotlin.streams.asStream
|
||||
|
||||
private fun MockServerRemoteFile.toMockAbsFolder(files: MockRemoteFiles): AbsoluteFolder {
|
||||
if (this == files.fileSystem.root) return files.root
|
||||
val parent = this.parent.toMockAbsFolder(files)
|
||||
return MockAbsoluteFolder(
|
||||
files,
|
||||
parent,
|
||||
this.id,
|
||||
this.name,
|
||||
parent.absolutePath.removeSuffix("/") + "/" + this.name,
|
||||
contentsCount = this.listFiles()?.count() ?: 0
|
||||
)
|
||||
}
|
||||
|
||||
private fun MockServerRemoteFile.toMockAbsFile(
|
||||
files: MockRemoteFiles,
|
||||
md5: ByteArray = byteArrayOf(),
|
||||
sha1: ByteArray = byteArrayOf()
|
||||
): AbsoluteFile {
|
||||
val parent = this.parent.toMockAbsFolder(files)
|
||||
// todo md5 and sha
|
||||
return MockAbsoluteFile(
|
||||
sha1,
|
||||
md5,
|
||||
files,
|
||||
parent,
|
||||
this.id,
|
||||
this.name,
|
||||
parent.absolutePath.removeSuffix("/") + "/" + this.name
|
||||
)
|
||||
}
|
||||
|
||||
internal open class MockAbsoluteFolder(
|
||||
internal val files: MockRemoteFiles,
|
||||
override val parent: AbsoluteFolder? = null,
|
||||
override val id: String = "/",
|
||||
override var name: String = "/",
|
||||
override var absolutePath: String = "/",
|
||||
override val contact: FileSupported = files.contact,
|
||||
override val isFile: Boolean = false,
|
||||
override val isFolder: Boolean = true,
|
||||
override val uploadTime: Long = 0L,
|
||||
override var lastModifiedTime: Long = 0L,
|
||||
override val uploaderId: Long = 0L,
|
||||
override var contentsCount: Int = 0
|
||||
) : AbsoluteFolder {
|
||||
private var _exists = true
|
||||
override suspend fun refreshed(): AbsoluteFolder? = parent!!.resolveFolderById(id)
|
||||
|
||||
private fun currentTxRF() = files.fileSystem.resolveById(id)!!
|
||||
|
||||
override suspend fun folders(): Flow<AbsoluteFolder> =
|
||||
currentTxRF().listFiles()?.filter { it.isDirectory }?.map { it.toMockAbsFolder(files) }?.asFlow() ?: emptyFlow()
|
||||
|
||||
|
||||
@JavaFriendlyAPI
|
||||
override suspend fun foldersStream(): Stream<AbsoluteFolder> =
|
||||
currentTxRF().listFiles()?.filter { it.isDirectory }?.map { it.toMockAbsFolder(files) }?.asStream()
|
||||
?: Stream.empty()
|
||||
|
||||
override suspend fun files(): Flow<AbsoluteFile> =
|
||||
currentTxRF().listFiles()?.filter { it.isFile }?.map { it.toMockAbsFile(files) }?.asFlow() ?: emptyFlow()
|
||||
|
||||
@JavaFriendlyAPI
|
||||
override suspend fun filesStream(): Stream<AbsoluteFile> =
|
||||
currentTxRF().listFiles()?.filter { it.isFile }?.map { it.toMockAbsFile(files) }?.asStream() ?: Stream.empty()
|
||||
|
||||
override suspend fun children(): Flow<AbsoluteFileFolder> =
|
||||
files.fileSystem.resolveById(id)!!.listFiles()?.map {
|
||||
if (it.isFile) it.toMockAbsFile(files)
|
||||
else it.toMockAbsFolder(files)
|
||||
}?.asFlow() ?: emptyFlow()
|
||||
|
||||
@JavaFriendlyAPI
|
||||
override suspend fun childrenStream(): Stream<AbsoluteFileFolder> =
|
||||
files.fileSystem.resolveById(id)!!.listFiles()?.map {
|
||||
if (it.isFile) it.toMockAbsFile(files)
|
||||
else it.toMockAbsFolder(files)
|
||||
}?.asStream() ?: Stream.empty()
|
||||
|
||||
override suspend fun createFolder(name: String): AbsoluteFolder {
|
||||
if (name.isBlank()) throw IllegalArgumentException("folder name cannot be blank.")
|
||||
|
||||
contact.safeCast<MockGroup>()?.let check@{ group ->
|
||||
if (group.botPermission.isOperator()) return@check
|
||||
throw IllegalStateException("Requires admin permission to create folder `$name`")
|
||||
}
|
||||
|
||||
FileSystem.checkLegitimacy(name)
|
||||
currentTxRF().mksubdir(name, 0L)
|
||||
return resolveFolder(name)!!
|
||||
}
|
||||
|
||||
override suspend fun resolveFolder(name: String): AbsoluteFolder? {
|
||||
FileSystem.checkLegitimacy(name)
|
||||
if (name.isBlank()) throw IllegalArgumentException("folder path cannot be blank")
|
||||
val n = name.removePrefix("/").removeSuffix("/")
|
||||
val a = absolutePath.removeSuffix("/")
|
||||
val f = files.fileSystem.findByPath("$a/$n").firstOrNull() ?: return null
|
||||
return f.toMockAbsFolder(files)
|
||||
}
|
||||
|
||||
override suspend fun resolveFolderById(id: String): AbsoluteFolder? {
|
||||
if (name.isBlank()) throw IllegalArgumentException("folder id cannot be blank.")
|
||||
if (!FileSystem.isLegal(id)) return null
|
||||
if (id == files.root.id) return files.root
|
||||
if (this.id != files.root.id) return null // tx服务器只支持一层文件夹
|
||||
val f = files.fileSystem.resolveById(id) ?: return null
|
||||
if (!f.exists || !f.isDirectory) return null
|
||||
return f.toMockAbsFolder(files)
|
||||
}
|
||||
|
||||
override suspend fun resolveFileById(id: String, deep: Boolean): AbsoluteFile? {
|
||||
if (id == "/" || id.isEmpty()) throw IllegalArgumentException("Illegal file id: $id")
|
||||
files().firstOrNull { it.id == id }?.let { return it }
|
||||
if (!deep) return null
|
||||
return folders().map { it.resolveFileById(id, deep) }.firstOrNull { it != null }
|
||||
}
|
||||
|
||||
override suspend fun resolveFiles(path: String): Flow<AbsoluteFile> {
|
||||
if (path.isBlank()) throw IllegalArgumentException("path cannot be blank.")
|
||||
if (!FileSystem.isLegal(path)) return emptyFlow()
|
||||
if (path[0] == '/') return files.root.resolveFiles(path.removePrefix("/"))
|
||||
return files.fileSystem.findByPath(absolutePath.removeSuffix("/") + "/" + path.removePrefix("/")).map {
|
||||
it.toMockAbsFile(files)
|
||||
}.asFlow()
|
||||
}
|
||||
|
||||
@JavaFriendlyAPI
|
||||
override suspend fun resolveFilesStream(path: String): Stream<AbsoluteFile> {
|
||||
if (path.isBlank()) throw IllegalArgumentException("path cannot be blank.")
|
||||
if (!FileSystem.isLegal(path)) return Stream.empty()
|
||||
if (path[0] == '/') return files.root.resolveFilesStream(path.removePrefix("/"))
|
||||
if (path.contains("/")) return resolveFolder(path.substringBefore("/"))?.resolveFilesStream(
|
||||
path.substringAfter(
|
||||
"/"
|
||||
)
|
||||
) ?: Stream.empty()
|
||||
return files.fileSystem.findByPath(absolutePath).map {
|
||||
it.toMockAbsFile(files)
|
||||
}.asStream()
|
||||
}
|
||||
|
||||
override suspend fun resolveAll(path: String): Flow<AbsoluteFileFolder> {
|
||||
if (path.isBlank()) throw IllegalArgumentException("path cannot be blank.")
|
||||
FileSystem.checkLegitimacy(path)
|
||||
val p = if (path.startsWith("/")) path
|
||||
else "${absolutePath.removeSuffix("/")}/$path"
|
||||
return files.fileSystem.findByPath(p).map {
|
||||
if (it.isDirectory) it.toMockAbsFolder(files)
|
||||
else it.toMockAbsFile(files)
|
||||
}.asFlow()
|
||||
}
|
||||
|
||||
@JavaFriendlyAPI
|
||||
override suspend fun resolveAllStream(path: String): Stream<AbsoluteFileFolder> {
|
||||
if (path.isBlank()) throw IllegalArgumentException("path cannot be blank.")
|
||||
FileSystem.checkLegitimacy(path)
|
||||
val p = if (path.startsWith("/")) path
|
||||
else "${absolutePath.removeSuffix("/")}/$path"
|
||||
return files.fileSystem.findByPath(p).map {
|
||||
if (it.isDirectory) it.toMockAbsFolder(files)
|
||||
else it.toMockAbsFile(files)
|
||||
}.asStream()
|
||||
}
|
||||
|
||||
override suspend fun uploadNewFile(
|
||||
filepath: String, content: ExternalResource, callback: ProgressionCallback<AbsoluteFile, Long>?
|
||||
): AbsoluteFile {
|
||||
contact.safeCast<MockGroup>()?.let check@{ group ->
|
||||
if (group.controlPane.isAllowMemberFileUploading) return@check
|
||||
if (group.botPermission.isOperator()) return@check
|
||||
throw PermissionDeniedException("Group $group not allowed members to uploading new files.")
|
||||
}
|
||||
|
||||
FileSystem.checkLegitimacy(filepath)
|
||||
val folderName = filepath.removePrefix("/").substringBeforeLast("/")
|
||||
val folder =
|
||||
if (folderName == "") files.root
|
||||
else if (filepath.removePrefix("/").contains("/")) resolveFolder(folderName) ?: createFolder(folderName)
|
||||
else this
|
||||
val f = files.fileSystem.resolveById(folder.id)!!
|
||||
.uploadFile(filepath.substringAfterLast("/"), content, 0L)
|
||||
return f.toMockAbsFile(files, content.md5, content.sha1)
|
||||
|
||||
}
|
||||
|
||||
override suspend fun exists(): Boolean = _exists
|
||||
|
||||
private fun canModify(resolved: MockServerRemoteFile): Boolean {
|
||||
return MockRemoteFile.canModify(resolved, contact)
|
||||
}
|
||||
|
||||
override suspend fun renameTo(newName: String): Boolean {
|
||||
val resolved = files.fileSystem.resolveById(id) ?: return false
|
||||
if (!canModify(resolved)) return false
|
||||
if (resolved.rename(newName)) {
|
||||
refresh()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override suspend fun delete(): Boolean {
|
||||
if (!_exists) return false
|
||||
val resolved = files.fileSystem.resolveById(id) ?: return false
|
||||
if (!canModify(resolved)) return false
|
||||
if (resolved.delete()) {
|
||||
_exists = false
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override suspend fun refresh(): Boolean {
|
||||
val new = refreshed() ?: let {
|
||||
_exists = false
|
||||
return false
|
||||
}
|
||||
this.name = new.name
|
||||
this.lastModifiedTime = new.lastModifiedTime
|
||||
this.contentsCount = new.contentsCount
|
||||
this.absolutePath = new.absolutePath
|
||||
return false
|
||||
}
|
||||
|
||||
override fun toString(): String = "MockAbsoluteFolder(id=$id,absolutePath=$absolutePath,name=$name"
|
||||
override fun equals(other: Any?): Boolean =
|
||||
other != null && other is AbsoluteFolder && other.id == id
|
||||
|
||||
override fun hashCode(): Int {
|
||||
// from AbsoluteFolderImpl
|
||||
var result = super.hashCode()
|
||||
result = 31 * result + contentsCount.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
@file:Suppress("ClassName")
|
||||
|
||||
package net.mamoe.mirai.mock.internal.remotefile.absolutefile
|
||||
|
||||
import net.mamoe.mirai.contact.FileSupported
|
||||
import net.mamoe.mirai.contact.file.AbsoluteFolder
|
||||
import net.mamoe.mirai.contact.file.RemoteFiles
|
||||
import net.mamoe.mirai.mock.resserver.MockServerFileSystem
|
||||
|
||||
internal class MockRemoteFiles(
|
||||
override val contact: FileSupported,
|
||||
val fileSystem: MockServerFileSystem,
|
||||
) : RemoteFiles {
|
||||
override val root: AbsoluteFolder = MRF_AbsoluteFolderRoot(this)
|
||||
}
|
||||
|
||||
internal class MRF_AbsoluteFolderRoot(files: MockRemoteFiles) : MockAbsoluteFolder(files) {
|
||||
override var contentsCount: Int
|
||||
get() = 0
|
||||
set(_) {}
|
||||
|
||||
override suspend fun refreshed(): AbsoluteFolder = MRF_AbsoluteFolderRoot(files)
|
||||
override val parent: AbsoluteFolder? get() = null
|
||||
override val id: String get() = "/"
|
||||
override var name: String
|
||||
get() = "/"
|
||||
set(_) {}
|
||||
override var absolutePath: String
|
||||
get() = "/"
|
||||
set(_) {}
|
||||
override val isFile: Boolean get() = false
|
||||
override val isFolder: Boolean get() = true
|
||||
override val uploadTime: Long get() = 0
|
||||
override var lastModifiedTime: Long
|
||||
get() = 0
|
||||
set(_) {}
|
||||
override val uploaderId: Long get() = 0
|
||||
override suspend fun exists(): Boolean = true
|
||||
override suspend fun renameTo(newName: String): Boolean = false
|
||||
override suspend fun delete(): Boolean = false
|
||||
override suspend fun refresh(): Boolean = true
|
||||
}
|
@ -0,0 +1,358 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||
|
||||
package net.mamoe.mirai.mock.internal.remotefile.remotefile
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import net.mamoe.mirai.contact.Contact
|
||||
import net.mamoe.mirai.contact.FileSupported
|
||||
import net.mamoe.mirai.contact.PermissionDeniedException
|
||||
import net.mamoe.mirai.contact.isOperator
|
||||
import net.mamoe.mirai.internal.message.data.FileMessageImpl
|
||||
import net.mamoe.mirai.message.MessageReceipt
|
||||
import net.mamoe.mirai.message.data.FileMessage
|
||||
import net.mamoe.mirai.mock.contact.MockGroup
|
||||
import net.mamoe.mirai.mock.resserver.MockServerFileSystem
|
||||
import net.mamoe.mirai.mock.resserver.MockServerRemoteFile
|
||||
import net.mamoe.mirai.mock.utils.mock
|
||||
import net.mamoe.mirai.utils.*
|
||||
import kotlin.io.path.inputStream
|
||||
|
||||
internal class RootRemoteFile(
|
||||
val fileSystem: MockServerFileSystem,
|
||||
override val contact: FileSupported,
|
||||
) : RemoteFile {
|
||||
override val name: String get() = ""
|
||||
override val id: String get() = "/"
|
||||
override val path: String get() = "/"
|
||||
override val parent: RemoteFile get() = this
|
||||
|
||||
override suspend fun isFile(): Boolean = false
|
||||
override suspend fun length(): Long = 0
|
||||
override suspend fun getInfo(): RemoteFile.FileInfo = fileSystem.root.fileInfo.let { inf ->
|
||||
RemoteFile.FileInfo(
|
||||
name = "/",
|
||||
path = "/",
|
||||
id = "/",
|
||||
length = 0,
|
||||
downloadTimes = 0,
|
||||
uploaderId = inf.creator,
|
||||
uploadTime = inf.createTime,
|
||||
lastModifyTime = inf.lastUpdateTime,
|
||||
sha1 = byteArrayOf(),
|
||||
md5 = byteArrayOf(),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun exists(): Boolean = true
|
||||
override fun toString(): String = "MockRemoteFile[ROOT, contact=$contact]"
|
||||
|
||||
override fun resolve(relative: String): RemoteFile {
|
||||
if (relative.isEmpty()) return this
|
||||
|
||||
val fixedPath = when {
|
||||
relative[0] == '/' -> relative
|
||||
else -> "/$relative"
|
||||
}.let { ist ->
|
||||
var end = ist.length
|
||||
while (end > 1 && ist[end - 1] == '/') {
|
||||
end--
|
||||
}
|
||||
ist.substring(0, end)
|
||||
}
|
||||
|
||||
if (fixedPath == "/" || fixedPath == ".") return this
|
||||
|
||||
val fixedName = fixedPath.substringAfterLast('/')
|
||||
|
||||
return MockRemoteFile(
|
||||
root = this,
|
||||
parent = resolve(fixedPath.substring(0, fixedPath.lastIndexOf('/'))),
|
||||
path = fixedPath,
|
||||
fileId = null,
|
||||
name = fixedName,
|
||||
)
|
||||
}
|
||||
|
||||
override fun resolve(relative: RemoteFile): RemoteFile = resolve(relative.path)
|
||||
|
||||
override suspend fun resolveById(id: String, deep: Boolean): RemoteFile? {
|
||||
if (id == "/") return this
|
||||
val resolved = fileSystem.resolveById(id) ?: return null
|
||||
return convert(resolved)
|
||||
}
|
||||
|
||||
internal fun convert(src: MockServerRemoteFile): RemoteFile {
|
||||
if (src == fileSystem.root) return this
|
||||
return MockRemoteFile(
|
||||
name = src.name,
|
||||
root = this,
|
||||
path = src.path,
|
||||
fileId = src.id,
|
||||
parent = convert(src.parent)
|
||||
)
|
||||
}
|
||||
|
||||
override fun resolveSibling(relative: String): RemoteFile = resolve(relative)
|
||||
override fun resolveSibling(relative: RemoteFile): RemoteFile = resolveSibling(relative.path)
|
||||
|
||||
override suspend fun delete(): Boolean = false
|
||||
override suspend fun renameTo(name: String): Boolean = false
|
||||
override suspend fun moveTo(target: RemoteFile): Boolean = false
|
||||
override suspend fun mkdir(): Boolean = true
|
||||
|
||||
private fun listFilesSeq(): Sequence<RemoteFile> {
|
||||
return fileSystem.root.listFiles()!!.map { convert(it) }
|
||||
}
|
||||
|
||||
override suspend fun listFiles(): Flow<RemoteFile> = listFilesSeq().asFlow()
|
||||
|
||||
@JavaFriendlyAPI
|
||||
override suspend fun listFilesIterator(lazy: Boolean): Iterator<RemoteFile> {
|
||||
return listFilesSeq().iterator()
|
||||
}
|
||||
|
||||
override suspend fun toMessage(): FileMessage? = null
|
||||
|
||||
@Deprecated(
|
||||
"Use uploadAndSend instead.",
|
||||
replaceWith = ReplaceWith("this.uploadAndSend(resource, callback)"),
|
||||
level = DeprecationLevel.ERROR
|
||||
)
|
||||
override suspend fun upload(resource: ExternalResource, callback: RemoteFile.ProgressionCallback?): FileMessage {
|
||||
error("Uploading as root directory")
|
||||
}
|
||||
|
||||
@MiraiExperimentalApi
|
||||
override suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt<Contact> {
|
||||
error("Uploading as root directory")
|
||||
}
|
||||
|
||||
override suspend fun getDownloadInfo(): RemoteFile.DownloadInfo? = null
|
||||
|
||||
fun resolveTx(f: RemoteFile?): MockServerRemoteFile? {
|
||||
if (f === this) return fileSystem.root
|
||||
return f.cast<MockRemoteFile>().resolveFile()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DuplicatedCode")
|
||||
internal class MockRemoteFile(
|
||||
val root: RootRemoteFile,
|
||||
override val parent: RemoteFile,
|
||||
override val path: String,
|
||||
val fileId: String?,
|
||||
override val name: String,
|
||||
) : RemoteFile {
|
||||
override val id: String? get() = fileId
|
||||
override val contact: FileSupported get() = root.contact
|
||||
private val fileSystem get() = root.fileSystem
|
||||
internal fun resolveFile(): MockServerRemoteFile? {
|
||||
fileId?.let { fid ->
|
||||
fileSystem.resolveById(fid)?.let { return it }
|
||||
}
|
||||
return fileSystem.findByPath(path).firstOrNull()
|
||||
}
|
||||
|
||||
private fun convert(src: MockServerRemoteFile): RemoteFile = root.convert(src)
|
||||
|
||||
override suspend fun isFile(): Boolean {
|
||||
return resolveFile()?.isFile ?: false
|
||||
}
|
||||
|
||||
override suspend fun length(): Long {
|
||||
val file = resolveFile() ?: return 0
|
||||
return file.size
|
||||
}
|
||||
|
||||
override suspend fun getInfo(): RemoteFile.FileInfo? {
|
||||
val resolved = resolveFile() ?: return null
|
||||
val fileInf = resolved.fileInfo
|
||||
return RemoteFile.FileInfo(
|
||||
name = resolved.name,
|
||||
id = resolved.id,
|
||||
path = resolved.path,
|
||||
length = resolved.size,
|
||||
downloadTimes = if (resolved.isFile) 1 else 0,
|
||||
uploaderId = fileInf.creator,
|
||||
uploadTime = fileInf.createTime,
|
||||
lastModifyTime = fileInf.lastUpdateTime,
|
||||
sha1 = if (resolved.isDirectory) {
|
||||
byteArrayOf()
|
||||
} else {
|
||||
resolved.resolveNativePath().inputStream().use { it.sha1() }
|
||||
},
|
||||
md5 = if (resolved.isDirectory) {
|
||||
byteArrayOf()
|
||||
} else {
|
||||
resolved.resolveNativePath().inputStream().use { it.md5() }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun exists(): Boolean = resolveFile() != null
|
||||
|
||||
override fun toString(): String {
|
||||
val resolved = resolveFile()
|
||||
return "MockFile[c=$contact, resolved=$resolved]"
|
||||
}
|
||||
|
||||
override fun resolve(relative: String): RemoteFile {
|
||||
if (relative == "/" || relative == "" || relative[0] == '/') {
|
||||
return root.resolve(relative)
|
||||
}
|
||||
return root.resolve("$path/$relative")
|
||||
}
|
||||
|
||||
override fun resolve(relative: RemoteFile): RemoteFile = resolve(relative.path)
|
||||
|
||||
override suspend fun resolveById(id: String, deep: Boolean): RemoteFile? {
|
||||
val resolved = fileSystem.resolveById(id) ?: return null
|
||||
if (deep) return convert(resolved)
|
||||
val thiz = resolveFile()
|
||||
if (resolved.parent == thiz) return convert(resolved)
|
||||
return null
|
||||
}
|
||||
|
||||
override fun resolveSibling(relative: String): RemoteFile {
|
||||
return parent.resolve(relative)
|
||||
}
|
||||
|
||||
override fun resolveSibling(relative: RemoteFile): RemoteFile {
|
||||
return parent.resolve(relative)
|
||||
}
|
||||
|
||||
override suspend fun delete(): Boolean {
|
||||
val resolved = resolveFile() ?: return false
|
||||
if (!canModify(resolved, contact)) return false
|
||||
return resolved.delete()
|
||||
}
|
||||
|
||||
override suspend fun renameTo(name: String): Boolean {
|
||||
val resolved = resolveFile() ?: return false
|
||||
|
||||
if (!canModify(resolved, contact)) return false
|
||||
|
||||
return resolved.rename(name)
|
||||
}
|
||||
|
||||
override suspend fun moveTo(target: RemoteFile): Boolean {
|
||||
val resolved = resolveFile() ?: return false
|
||||
|
||||
if (!canModify(resolved, contact)) return false
|
||||
|
||||
val targetF = root.resolveTx(target.parent) ?: return false
|
||||
resolved.moveTo(targetF)
|
||||
resolved.rename(target.name)
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun mkdir(): Boolean {
|
||||
contact.safeCast<MockGroup>()?.let check@{ group ->
|
||||
if (group.botPermission.isOperator()) return@check
|
||||
return false
|
||||
}
|
||||
if (resolveFile() != null) return false
|
||||
val dirx = root.resolveTx(parent) ?: return false
|
||||
return kotlin.runCatching {
|
||||
dirx.mksubdir(name, contact.bot.id)
|
||||
}.isSuccess
|
||||
}
|
||||
|
||||
private fun listFilesSeq(): Sequence<RemoteFile> {
|
||||
val resolved = resolveFile()?.listFiles() ?: return emptySequence()
|
||||
return resolved.map { convert(it) }
|
||||
}
|
||||
|
||||
override suspend fun listFiles(): Flow<RemoteFile> {
|
||||
return listFilesSeq().asFlow()
|
||||
}
|
||||
|
||||
@JavaFriendlyAPI
|
||||
override suspend fun listFilesIterator(lazy: Boolean): Iterator<RemoteFile> {
|
||||
return listFilesSeq().iterator()
|
||||
}
|
||||
|
||||
override suspend fun toMessage(): FileMessage? {
|
||||
val resolved = resolveFile() ?: return null
|
||||
return FileMessageImpl(
|
||||
name = resolved.name,
|
||||
id = resolved.id,
|
||||
size = resolved.size,
|
||||
busId = 1544241
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION", "DEPRECATION_ERROR", "OVERRIDE_DEPRECATION", "OverridingDeprecatedMember")
|
||||
override suspend fun upload(resource: ExternalResource, callback: RemoteFile.ProgressionCallback?): FileMessage {
|
||||
callback?.onBegin(this, resource)
|
||||
try {
|
||||
contact.safeCast<MockGroup>()?.let check@{ group ->
|
||||
if (group.botPermission.isOperator()) return@check
|
||||
if (group.controlPane.isAllowMemberFileUploading) return@check
|
||||
throw PermissionDeniedException("Group $group disabled member file uploading...")
|
||||
}
|
||||
|
||||
|
||||
val parent = root.resolveTx(this.parent) ?: throw IllegalStateException("Parent ${this.parent} not found.")
|
||||
val rsSize = resource.size
|
||||
val rsp = parent.uploadFile(this.name, resource, contact.bot.id)
|
||||
callback?.onProgression(this, resource, rsSize)
|
||||
callback?.onSuccess(this, resource)
|
||||
return FileMessageImpl(
|
||||
name = rsp.name,
|
||||
id = rsp.id,
|
||||
size = rsp.size,
|
||||
busId = 1544241
|
||||
)
|
||||
} catch (errx: Throwable) {
|
||||
callback?.onFailure(this, resource, errx)
|
||||
throw errx
|
||||
}
|
||||
}
|
||||
|
||||
@MiraiExperimentalApi
|
||||
@Suppress("DEPRECATION_ERROR", "OverridingDeprecatedMember")
|
||||
override suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt<Contact> {
|
||||
return contact.sendMessage(upload(resource))
|
||||
}
|
||||
|
||||
override suspend fun getDownloadInfo(): RemoteFile.DownloadInfo? {
|
||||
val resolved = resolveFile() ?: return null
|
||||
if (!resolved.isFile) return null
|
||||
val ntp = resolved.resolveNativePath()
|
||||
return RemoteFile.DownloadInfo(
|
||||
filename = resolved.name,
|
||||
id = resolved.id,
|
||||
path = resolved.path,
|
||||
sha1 = ntp.inputStream().use { it.sha1() },
|
||||
md5 = ntp.inputStream().use { it.md5() },
|
||||
url = contact.bot.mock().tmpResourceServer.resolveHttpUrlByPath(ntp).toString()
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal fun canModify(resolved: MockServerRemoteFile, contact: FileSupported): Boolean {
|
||||
contact.safeCast<MockGroup>()?.let check@{ group ->
|
||||
if (group.botPermission.isOperator()) return true
|
||||
if (resolved.isDirectory) return false
|
||||
|
||||
val finf = resolved.fileInfo
|
||||
if (finf.creator == group.bot.id) return true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
368
mirai-core-mock/src/internal/serverfs/MockServerFileDiskImpl.kt
Normal file
368
mirai-core-mock/src/internal/serverfs/MockServerFileDiskImpl.kt
Normal file
@ -0,0 +1,368 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||
|
||||
package net.mamoe.mirai.mock.internal.serverfs
|
||||
|
||||
import io.ktor.utils.io.core.*
|
||||
import io.ktor.utils.io.streams.*
|
||||
import net.mamoe.mirai.mock.resserver.MockServerFileDisk
|
||||
import net.mamoe.mirai.mock.resserver.MockServerFileSystem
|
||||
import net.mamoe.mirai.mock.resserver.MockServerRemoteFile
|
||||
import net.mamoe.mirai.mock.resserver.TxRemoteFileInfo
|
||||
import net.mamoe.mirai.utils.*
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.attribute.FileTime
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentLinkedDeque
|
||||
import kotlin.io.path.*
|
||||
import kotlin.io.use
|
||||
import net.mamoe.mirai.internal.utils.FileSystem as MiraiFileSystem
|
||||
|
||||
private fun allocateNewPath(base: Path): Path {
|
||||
while (true) {
|
||||
val p = base.resolve(UUID.randomUUID().toString())
|
||||
if (!p.exists()) return p
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkFileName(name: String) {
|
||||
MiraiFileSystem.checkLegitimacy(name)
|
||||
if (name.contains('/')) error("$name contains '/'")
|
||||
if (name.isEmpty()) error("Empty name")
|
||||
}
|
||||
|
||||
internal class MockServerFileDiskImpl(
|
||||
internal val storage: Path
|
||||
) : MockServerFileDisk {
|
||||
internal val fs: MutableCollection<MockServerFileSystem> = ConcurrentLinkedDeque()
|
||||
|
||||
override val availableSystems: Sequence<MockServerFileSystem> = Sequence { fs.iterator() }
|
||||
|
||||
override fun newFsSystem(): MockServerFileSystem = MockServerFileSystemImpl(this)
|
||||
}
|
||||
|
||||
internal class MockServerFileSystemImpl(
|
||||
override val disk: MockServerFileDiskImpl,
|
||||
) : MockServerFileSystem {
|
||||
internal val storage: Path = allocateNewPath(disk.storage)
|
||||
|
||||
internal fun resolvePath(id: String): Path = when {
|
||||
id.isEmpty() || id == "/" -> storage.resolve("root")
|
||||
id[0] == '/' -> storage.resolve(id.substring(1))
|
||||
else -> error("file not exists: $id")
|
||||
}
|
||||
|
||||
internal fun fileDetails(id: String): Path? = when {
|
||||
id.isEmpty() || id == "/" -> storage.resolve("details/root")
|
||||
id[0] == '/' -> {
|
||||
storage
|
||||
.resolve("details")
|
||||
.resolve(id.substring(1))
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
internal fun resolveName(id: String): String = when {
|
||||
id.isEmpty() || id == "/" -> ""
|
||||
id[0] == '/' -> {
|
||||
val nameMapping = fileDetails(id)?.resolve("name")
|
||||
if (nameMapping == null) null
|
||||
else if (nameMapping.isFile) {
|
||||
nameMapping.readText()
|
||||
} else null
|
||||
}
|
||||
|
||||
else -> null
|
||||
} ?: id.substringAfterLast('/')
|
||||
|
||||
fun resolveParent(id: String): MockServerFileImpl {
|
||||
val details = fileDetails(id) ?: return root
|
||||
val parent = details.resolve("parent")
|
||||
if (parent.isFile) {
|
||||
return resolveById(parent.readText()) ?: root
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
init {
|
||||
storage.mkdirs()
|
||||
storage.resolve("details/root").mkdirs()
|
||||
storage.resolve("root").mkdirs()
|
||||
overrideDetails(fileDetails("/")!!, name = "", creator = 0, createTime = 0)
|
||||
disk.fs.add(this)
|
||||
}
|
||||
|
||||
override val root = MockServerFileImpl(this, "/")
|
||||
|
||||
override fun resolveById(id: String): MockServerFileImpl? {
|
||||
if (id == "/" || id.isEmpty()) return root
|
||||
if (id[0] != '/') return null
|
||||
if (MiraiFileSystem.isLegal(id) && id.count { it == '/' } == 1) {
|
||||
return MockServerFileImpl(this, id).takeIf { it.toPath.exists() }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun findByPath(path: String): Sequence<MockServerRemoteFile> {
|
||||
return root.findByPath(
|
||||
MiraiFileSystem.normalize(path)
|
||||
.removePrefix("/")
|
||||
.split('/')
|
||||
.toMutableList()
|
||||
)
|
||||
}
|
||||
|
||||
fun findDirByName(base: MockServerFileImpl, name: String): MockServerFileImpl? {
|
||||
return (base.listFiles() ?: return null)
|
||||
.filter { it.isDirectory }
|
||||
.filter { it.name == name }
|
||||
.firstOrNull()?.cast()
|
||||
}
|
||||
|
||||
fun uploadFile(
|
||||
name: String,
|
||||
content: ExternalResource,
|
||||
uploader: Long,
|
||||
id: String,
|
||||
toPath: Path
|
||||
): MockServerFileImpl {
|
||||
val path = allocateNewPath(storage)
|
||||
val fid = '/' + path.name
|
||||
|
||||
path.outputStream().buffered().use { output ->
|
||||
content.inputStream().use { resource -> resource.copyTo(output) }
|
||||
}
|
||||
|
||||
toPath.resolve(path.name).createFile()
|
||||
|
||||
val details = fileDetails(fid)!!
|
||||
details.mkdirs()
|
||||
overrideDetails(details, id, name, uploader, currentTimeMillis())
|
||||
|
||||
return MockServerFileImpl(this, fid)
|
||||
}
|
||||
|
||||
fun overrideDetails(
|
||||
details: Path,
|
||||
parent: String? = null,
|
||||
name: String? = null,
|
||||
creator: Long = -1L,
|
||||
createTime: Long = -1L,
|
||||
) {
|
||||
if (parent != null) {
|
||||
details.resolve("parent").writeText(parent)
|
||||
}
|
||||
if (name != null) {
|
||||
details.resolve("name").writeText(name)
|
||||
}
|
||||
if (creator != -1L) {
|
||||
details.resolve("creator").writeBytes(creator.toByteArray())
|
||||
}
|
||||
if (createTime != -1L) {
|
||||
details.resolve("createTime").writeBytes(createTime.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
fun mkdir(id: String, name: String, creator: Long, toPath: Path): MockServerFileImpl {
|
||||
if (id != "/") error("Creating 2nd directories, MockServerFileSystem current not support")
|
||||
|
||||
// Find existing subdir
|
||||
Files.newDirectoryStream(toPath).use { ptdirstream ->
|
||||
val exists = ptdirstream.firstOrNull { subfile ->
|
||||
if (storage.resolve(subfile).isFile) return@firstOrNull false
|
||||
val nameFile = storage.resolve("details").resolve(subfile.fileName).resolve("name")
|
||||
return@firstOrNull nameFile.readText() == name
|
||||
}
|
||||
if (exists != null) {
|
||||
return MockServerFileImpl(this, "/" + exists.fileName)
|
||||
}
|
||||
}
|
||||
|
||||
val path = allocateNewPath(storage)
|
||||
val fid = '/' + path.name
|
||||
path.mkdir()
|
||||
|
||||
toPath.resolve(path.name).createFile()
|
||||
val details = fileDetails(fid)!!
|
||||
details.mkdirs()
|
||||
overrideDetails(details, id, name, creator, currentTimeMillis())
|
||||
|
||||
return MockServerFileImpl(this, fid)
|
||||
}
|
||||
|
||||
fun resolveAbsPath(id: String): String {
|
||||
if (id == "/") return "/"
|
||||
|
||||
val details = fileDetails(id) ?: return "<not exists>"
|
||||
val fileNamePath = details.resolve("name")
|
||||
val fileName = fileNamePath.takeIf { it.isFile }?.readText() ?: "<not exists>"
|
||||
val parentPath = details.resolve("parent")
|
||||
if (!parentPath.isFile) {
|
||||
return fileName
|
||||
}
|
||||
val pid = parentPath.readText()
|
||||
val pabs = resolveAbsPath(pid)
|
||||
if (pabs.endsWith("/")) return "$pabs$fileName"
|
||||
return "$pabs/$fileName"
|
||||
}
|
||||
}
|
||||
|
||||
internal class MockServerFileImpl(
|
||||
override val system: MockServerFileSystemImpl,
|
||||
override val id: String,
|
||||
) : MockServerRemoteFile {
|
||||
internal val toPath: Path get() = system.resolvePath(id)
|
||||
override val exists: Boolean get() = toPath.exists()
|
||||
override val isFile: Boolean get() = toPath.isFile
|
||||
override val isDirectory: Boolean get() = toPath.isDirectory()
|
||||
override val name: String get() = system.resolveName(id)
|
||||
override val path: String get() = system.resolveAbsPath(id)
|
||||
override val parent: MockServerFileImpl get() = system.resolveParent(id)
|
||||
override val size: Long
|
||||
get() {
|
||||
val pt = toPath
|
||||
if (pt.isFile) return pt.fileSize()
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun listFiles(): Sequence<MockServerRemoteFile>? {
|
||||
val pt = toPath
|
||||
if (!pt.isDirectory()) {
|
||||
return null
|
||||
}
|
||||
return pt.listDirectoryEntries().asSequence().filter {
|
||||
it.exists()
|
||||
}.map { MockServerFileImpl(system, '/' + it.name) }
|
||||
}
|
||||
|
||||
override fun delete(): Boolean {
|
||||
if (!toPath.deleteIfExists()) return false
|
||||
val details = system.fileDetails(id) ?: return false
|
||||
system.resolvePath(details.resolve("parent").readText())
|
||||
.resolve(id.substring(1))
|
||||
.deleteIfExists()
|
||||
details.deleteRecursively()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun rename(name: String): Boolean {
|
||||
checkFileName(name)
|
||||
if (id.isEmpty() || id == "/") return false
|
||||
val details = system.fileDetails(id) ?: return false
|
||||
details.resolve("name").writeText(name)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun moveTo(path: MockServerRemoteFile) {
|
||||
path.cast<MockServerFileImpl>()
|
||||
if (path.system !== this.system) error("Cross file system moving")
|
||||
|
||||
if (!path.isDirectory) error("Remote file $path not exists")
|
||||
if (id == "/") error("Moving root")
|
||||
|
||||
// TODO: 移动到自己的子目录
|
||||
|
||||
val details = system.fileDetails(id) ?: error("Moving ghost file: $id")
|
||||
|
||||
val currentParent = parent
|
||||
currentParent.toPath.resolve(id.substring(1)).deleteIfExists()
|
||||
|
||||
details.resolve("parent").writeText(path.id)
|
||||
path.toPath.resolve(id.substring(1)).createFile()
|
||||
}
|
||||
|
||||
override fun resolveNativePath(): Path {
|
||||
val pt = toPath
|
||||
if (!pt.isFile) error("file not exists: $this <$pt>")
|
||||
return pt
|
||||
}
|
||||
|
||||
override fun asExternalResource(): ExternalResource {
|
||||
val pt = toPath
|
||||
if (!pt.isFile) error("file not exists: $pt")
|
||||
return object : AbstractExternalResource() {
|
||||
override fun inputStream0(): InputStream {
|
||||
return toPath.inputStream()
|
||||
}
|
||||
|
||||
override val size: Long
|
||||
get() = toPath.fileSize()
|
||||
|
||||
@MiraiExperimentalApi
|
||||
override fun input(): Input {
|
||||
return inputStream0().asInput()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun uploadFile(name: String, content: ExternalResource, uploader: Long): MockServerFileImpl {
|
||||
content.withAutoClose {
|
||||
checkFileName(name)
|
||||
val storage = toPath
|
||||
if (storage.isFile) error("Uploading file to a file")
|
||||
if (!storage.isDirectory()) error("$this not exists")
|
||||
|
||||
return system.uploadFile(name, content, uploader, id, toPath)
|
||||
}
|
||||
}
|
||||
|
||||
override fun mksubdir(name: String, creator: Long): MockServerRemoteFile {
|
||||
checkFileName(name)
|
||||
return system.mkdir(id, name, creator, toPath)
|
||||
}
|
||||
|
||||
override var fileInfo: TxRemoteFileInfo
|
||||
get() {
|
||||
val details = system.fileDetails(id) ?: error("File not exists")
|
||||
if (!details.isDirectory()) {
|
||||
error("File not exists")
|
||||
}
|
||||
// parent, name, creator, createTime
|
||||
return TxRemoteFileInfo(
|
||||
creator = details.resolve("creator").readBytes().toLong(),
|
||||
createTime = details.resolve("createTime").readBytes().toLong(),
|
||||
lastUpdateTime = toPath.getLastModifiedTime().toMillis(),
|
||||
)
|
||||
}
|
||||
set(value) {
|
||||
val details = system.fileDetails(id) ?: error("File not exists")
|
||||
if (!details.isDirectory()) {
|
||||
error("File not exists")
|
||||
}
|
||||
details.resolve("creator").writeBytes(value.creator.toByteArray())
|
||||
details.resolve("createTime").writeBytes(value.createTime.toByteArray())
|
||||
toPath.setLastModifiedTime(FileTime.fromMillis(value.lastUpdateTime))
|
||||
}
|
||||
|
||||
override fun toString(): String = "$path := $id"
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is MockServerFileImpl) return false
|
||||
if (other.system !== system) return false
|
||||
return other.id == this.id
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return id.hashCode() + system.hashCode()
|
||||
}
|
||||
|
||||
fun findByPath(path: MutableList<String>): Sequence<MockServerRemoteFile> {
|
||||
if (path.isEmpty()) error("Empty path")
|
||||
val nxt = path.removeAt(0)
|
||||
if (nxt.isEmpty()) error("Empty subpath")
|
||||
if (path.isEmpty()) return listFiles()?.filter { it.name == nxt } ?: emptySequence()
|
||||
|
||||
return system.findDirByName(this, nxt)?.findByPath(path) ?: emptySequence()
|
||||
}
|
||||
}
|
153
mirai-core-mock/src/internal/serverfs/TmpResourceServerImpl.kt
Normal file
153
mirai-core-mock/src/internal/serverfs/TmpResourceServerImpl.kt
Normal file
@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.internal.serverfs
|
||||
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.engine.*
|
||||
import io.ktor.server.netty.*
|
||||
import io.ktor.server.plugins.*
|
||||
import io.ktor.server.response.*
|
||||
import net.mamoe.mirai.mock.resserver.MockServerFileDisk
|
||||
import net.mamoe.mirai.mock.resserver.TmpResourceServer
|
||||
import net.mamoe.mirai.utils.*
|
||||
import java.net.ServerSocket
|
||||
import java.net.URI
|
||||
import java.net.URLDecoder
|
||||
import java.net.URLEncoder
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.*
|
||||
|
||||
internal class TmpResourceServerImpl(
|
||||
override val storageRoot: Path,
|
||||
private val serverPort: Int,
|
||||
private val closeSystemOnShutdown: Boolean,
|
||||
) : TmpResourceServer {
|
||||
var logger by lateinitMutableProperty {
|
||||
MiraiLogger.Factory.create(TmpResourceServerImpl::class.java, "TmpFsServer-${hashCode()}")
|
||||
}
|
||||
lateinit var server: NettyApplicationEngine
|
||||
|
||||
private var _serverUri: URI by lateinitMutableProperty {
|
||||
URI.create("http://localhost:$serverPort")
|
||||
}
|
||||
override val serverUri: URI get() = _serverUri
|
||||
|
||||
override val mockServerFileDisk: MockServerFileDisk by lazy {
|
||||
MockServerFileDiskImpl(storageRoot.resolve("tx-fs-disk"))
|
||||
}
|
||||
|
||||
private var _isActive: Boolean = false
|
||||
override val isActive: Boolean get() = _isActive
|
||||
|
||||
private val storage: Path = storageRoot.resolve("storage").mkdirsIfMissing()
|
||||
private val images: Path = storageRoot.resolve("images").mkdirsIfMissing()
|
||||
|
||||
override suspend fun uploadResource(resource: ExternalResource): String {
|
||||
fun ByteArray.hex() = toUHexString(separator = "")
|
||||
|
||||
resource.useAutoClose {
|
||||
val resourceId = "${resource.size}-${resource.sha1.hex()}-${resource.md5.hex()}"
|
||||
val locPath = storage.resolve(resourceId)
|
||||
if (locPath.isFile) return resourceId
|
||||
runBIO {
|
||||
locPath.outputStream().use { output ->
|
||||
resource.inputStream().use { it.copyTo(output) }
|
||||
}
|
||||
}
|
||||
return resourceId
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun uploadResourceAsImage(resource: ExternalResource): URI {
|
||||
val imgId = generateUUID(resource.md5)
|
||||
val resId = uploadResource(resource)
|
||||
images.resolve(imgId).createLinkPointingTo(storage.resolve(resId))
|
||||
return resolveImageUrl(imgId)
|
||||
}
|
||||
|
||||
override fun resolveHttpUrl(resourceId: String): URI {
|
||||
return serverUri.resolve("storage/$resourceId")
|
||||
}
|
||||
|
||||
override fun resolveImageUrl(imgId: String): URI {
|
||||
return serverUri.resolve("images/$imgId")
|
||||
}
|
||||
|
||||
override suspend fun invalidateResource(resourceId: String) {
|
||||
storage.resolve(resourceId).deleteIfExists()
|
||||
}
|
||||
|
||||
override fun resolveHttpUrlByPath(path: Path): URI {
|
||||
if (path.fileSystem !== storageRoot.fileSystem)
|
||||
throw UnsupportedOperationException("Cross file system linking is not supported now")
|
||||
val pt = path.toAbsolutePath().toString().replace('\\', '/')
|
||||
return serverUri.resolve(
|
||||
"abs/" + URLEncoder.encode(pt, "UTF-8")
|
||||
)
|
||||
}
|
||||
|
||||
override fun startupServer() {
|
||||
val port = if (serverPort == 0) {
|
||||
ServerSocket(0).use { it.localPort }
|
||||
} else serverPort
|
||||
_serverUri = URI.create("http://127.0.0.1:$port/")
|
||||
logger.info { "Tmp Fs Server started: $serverUri" }
|
||||
|
||||
val server = embeddedServer(Netty, environment = applicationEngineEnvironment {
|
||||
connector {
|
||||
this.host = "127.0.0.1"
|
||||
this.port = port
|
||||
}
|
||||
module {
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
intercept(ApplicationCallPipeline.Call) {
|
||||
val req = URI.create(call.request.origin.uri).path.removePrefix("/")
|
||||
val targetPath = if (req.startsWith("abs/")) {
|
||||
storageRoot.fileSystem.getPath(URLDecoder.decode(req.substring(4), "UTF-8"))
|
||||
} else {
|
||||
storageRoot.resolve(req)
|
||||
}
|
||||
if (targetPath.exists()) {
|
||||
call.respondOutputStream {
|
||||
net.mamoe.mirai.utils.runBIO {
|
||||
targetPath.inputStream().buffered().use { it.copyTo(this@respondOutputStream) }
|
||||
}
|
||||
}
|
||||
return@intercept
|
||||
}
|
||||
if (req.startsWith("images/")) {
|
||||
call.respondRedirect(
|
||||
"http://gchat.qpic.cn/gchatpic_new/1145141919/0-0-${
|
||||
req.substring(7)
|
||||
}/0?term=2", false
|
||||
)
|
||||
return@intercept
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
this.server = server
|
||||
server.start(wait = false)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
if (this::server.isInitialized) {
|
||||
server.stop(0, 0)
|
||||
}
|
||||
if (closeSystemOnShutdown) {
|
||||
storageRoot.fileSystem.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Path.mkdirsIfMissing(): Path {
|
||||
if (!exists()) createDirectories()
|
||||
return this
|
||||
}
|
11
mirai-core-mock/src/package.kt
Normal file
11
mirai-core-mock/src/package.kt
Normal file
@ -0,0 +1,11 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock
|
||||
|
25
mirai-core-mock/src/resserver/MockServerFileDisk.kt
Normal file
25
mirai-core-mock/src/resserver/MockServerFileDisk.kt
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.resserver
|
||||
|
||||
import net.mamoe.mirai.mock.internal.serverfs.MockServerFileDiskImpl
|
||||
import java.nio.file.Path
|
||||
|
||||
public interface MockServerFileDisk {
|
||||
public val availableSystems: Sequence<MockServerFileSystem>
|
||||
public fun newFsSystem(): MockServerFileSystem
|
||||
|
||||
public companion object {
|
||||
@JvmStatic
|
||||
public fun newFileDisk(storage: Path): MockServerFileDisk {
|
||||
return MockServerFileDiskImpl(storage)
|
||||
}
|
||||
}
|
||||
}
|
17
mirai-core-mock/src/resserver/MockServerFileSystem.kt
Normal file
17
mirai-core-mock/src/resserver/MockServerFileSystem.kt
Normal file
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.resserver
|
||||
|
||||
public interface MockServerFileSystem {
|
||||
public val disk: MockServerFileDisk
|
||||
public val root: MockServerRemoteFile
|
||||
public fun resolveById(id: String): MockServerRemoteFile?
|
||||
public fun findByPath(path: String): Sequence<MockServerRemoteFile>
|
||||
}
|
51
mirai-core-mock/src/resserver/MockServerRemoteFile.kt
Normal file
51
mirai-core-mock/src/resserver/MockServerRemoteFile.kt
Normal file
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.resserver
|
||||
|
||||
import net.mamoe.mirai.utils.ExternalResource
|
||||
import java.nio.file.Path
|
||||
|
||||
public interface MockServerRemoteFile {
|
||||
public val system: MockServerFileSystem
|
||||
|
||||
public val isFile: Boolean
|
||||
public val isDirectory: Boolean
|
||||
public val name: String
|
||||
public val path: String
|
||||
public val id: String
|
||||
public val exists: Boolean
|
||||
public val parent: MockServerRemoteFile
|
||||
public val size: Long
|
||||
|
||||
public fun listFiles(): Sequence<MockServerRemoteFile>?
|
||||
public fun delete(): Boolean
|
||||
public fun rename(name: String): Boolean
|
||||
|
||||
/**
|
||||
* 移动文件
|
||||
* @param path 目标目录
|
||||
*/
|
||||
public fun moveTo(path: MockServerRemoteFile)
|
||||
|
||||
public fun asExternalResource(): ExternalResource
|
||||
public fun resolveNativePath(): Path
|
||||
|
||||
public fun uploadFile(name: String, content: ExternalResource, uploader: Long): MockServerRemoteFile
|
||||
|
||||
public fun mksubdir(name: String, creator: Long): MockServerRemoteFile
|
||||
|
||||
public var fileInfo: TxRemoteFileInfo
|
||||
}
|
||||
|
||||
public data class TxRemoteFileInfo(
|
||||
@JvmField var creator: Long,
|
||||
@JvmField var createTime: Long,
|
||||
@JvmField var lastUpdateTime: Long,
|
||||
)
|
95
mirai-core-mock/src/resserver/TmpResourceServer.kt
Normal file
95
mirai-core-mock/src/resserver/TmpResourceServer.kt
Normal file
@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.resserver
|
||||
|
||||
import com.google.common.jimfs.Configuration
|
||||
import com.google.common.jimfs.Jimfs
|
||||
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
|
||||
import net.mamoe.mirai.mock.MockBot
|
||||
import net.mamoe.mirai.mock.internal.serverfs.TmpResourceServerImpl
|
||||
import net.mamoe.mirai.utils.ExternalResource
|
||||
import java.io.Closeable
|
||||
import java.net.URI
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* 临时资源中转服务器
|
||||
*
|
||||
* 此服务器用于中转测试中涉及到的各种临时数据, 如 图片、语音、群文件 等
|
||||
*
|
||||
* 如果 [TmpResourceServer] 被用于 [MockBot], 在 [MockBot] 关闭时也会同步关闭 [TmpResourceServer]
|
||||
*
|
||||
*/
|
||||
@JvmBlockingBridge
|
||||
public interface TmpResourceServer : Closeable {
|
||||
public val serverUri: URI
|
||||
public val storageRoot: Path
|
||||
public val mockServerFileDisk: MockServerFileDisk
|
||||
public val isActive: Boolean
|
||||
|
||||
/**
|
||||
* 上传一个资源
|
||||
*
|
||||
* @return 资源 ID, 可通过 [resolveHttpUrl] 获得 http 链接
|
||||
*/
|
||||
public suspend fun uploadResource(resource: ExternalResource): String
|
||||
|
||||
/**
|
||||
* 上传图片
|
||||
*
|
||||
* @return 图片的 http 链接
|
||||
*/
|
||||
public suspend fun uploadResourceAsImage(resource: ExternalResource): URI
|
||||
public suspend fun uploadResourceAndGetUrl(resource: ExternalResource): String {
|
||||
return resolveHttpUrl(uploadResource(resource)).toString()
|
||||
}
|
||||
|
||||
public fun resolveHttpUrl(resourceId: String): URI
|
||||
public fun resolveImageUrl(imgId: String): URI
|
||||
|
||||
/**
|
||||
* 立即释放目标资源, 此后再次访问该资源 ([resourceId]) 时会得到 404 Not Found
|
||||
*/
|
||||
public suspend fun invalidateResource(resourceId: String)
|
||||
|
||||
/**
|
||||
* 获取一个对应 [path] 的 http 链接
|
||||
*/
|
||||
public fun resolveHttpUrlByPath(path: Path): URI
|
||||
|
||||
/**
|
||||
* 启动 Http Server.
|
||||
*
|
||||
* 如果 [TmpResourceServer] 被用于 [MockBot], [MockBot] 会自动启动服务器, 请不要自行启动
|
||||
*/
|
||||
public fun startupServer()
|
||||
|
||||
public companion object {
|
||||
@JvmStatic
|
||||
public fun of(
|
||||
path: Path,
|
||||
port: Int = 0,
|
||||
closeFileSystemWhenClose: Boolean = false,
|
||||
): TmpResourceServer {
|
||||
return TmpResourceServerImpl(path, port, closeFileSystemWhenClose)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun newInMemoryTmpResourceServer(port: Int = 0): TmpResourceServer {
|
||||
val fs = Jimfs.newFileSystem(
|
||||
Configuration.unix()
|
||||
.toBuilder()
|
||||
.setWorkingDirectory("/")
|
||||
.build()
|
||||
)
|
||||
return of(fs.getPath("/"), port, true)
|
||||
}
|
||||
}
|
||||
}
|
171
mirai-core-mock/src/userprofile/UserProfileService.kt
Normal file
171
mirai-core-mock/src/userprofile/UserProfileService.kt
Normal file
@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.userprofile
|
||||
|
||||
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
|
||||
import net.mamoe.mirai.IMirai
|
||||
import net.mamoe.mirai.data.UserProfile
|
||||
import net.mamoe.mirai.mock.MockBot
|
||||
import net.mamoe.mirai.mock.userprofile.MockUserProfileBuilder.Companion.invoke
|
||||
import net.mamoe.mirai.utils.runBIO
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
/**
|
||||
* 用户资料服务, 用于 [IMirai.queryProfile] 查询用户资料
|
||||
*
|
||||
* implementation note: Java 请实现 [JUserProfileService]
|
||||
*
|
||||
* @see MockBot.userProfileService
|
||||
* @see MockUserProfileBuilder
|
||||
*/
|
||||
@JvmBlockingBridge
|
||||
public interface UserProfileService {
|
||||
public suspend fun doQueryUserProfile(id: Long): UserProfile
|
||||
|
||||
/**
|
||||
* 将 [id] 的用户资料指定为 [profile]
|
||||
*
|
||||
* implementation note:
|
||||
*
|
||||
* 框架内部并不会使用此接口, 该接口是设计于测试单元动态注册 [UserProfile],
|
||||
* 如无调用此接口的需求可以实现为 `throw new UnsupportedOperationException()`
|
||||
*/
|
||||
public suspend fun putUserProfile(id: Long, profile: UserProfile)
|
||||
|
||||
public companion object {
|
||||
@JvmStatic
|
||||
public fun getInstance(): UserProfileService {
|
||||
return UserProfileServiceImpl()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于资料服务, 用于 [IMirai.queryProfile] 查询用户资料
|
||||
*
|
||||
* 该接口是为了方便 Java 实现 [UserProfileService],
|
||||
* kotlin 请实现 [UserProfileService]
|
||||
*/
|
||||
@Suppress("ILLEGAL_JVM_NAME", "INAPPLICABLE_JVM_NAME")
|
||||
public interface JUserProfileService : UserProfileService {
|
||||
override suspend fun doQueryUserProfile(id: Long): UserProfile {
|
||||
return runBIO {
|
||||
doQueryUserProfileJ(id) ?: buildUserProfile { }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun putUserProfile(id: Long, profile: UserProfile) {
|
||||
runBIO {
|
||||
putUserProfileJ(id, profile)
|
||||
}
|
||||
}
|
||||
|
||||
// override UserProfileService @JvmBlockingBridge
|
||||
@JvmName("doQueryUserProfile")
|
||||
public fun doQueryUserProfileJ(id: Long): UserProfile?
|
||||
|
||||
@JvmName("putUserProfile")
|
||||
public fun putUserProfileJ(id: Long, profile: UserProfile)
|
||||
}
|
||||
|
||||
/**
|
||||
* [UserProfile] 的构造器
|
||||
*
|
||||
* @see [invoke]
|
||||
* @see [buildUserProfile]
|
||||
*/
|
||||
public interface MockUserProfileBuilder {
|
||||
public fun build(): UserProfile
|
||||
|
||||
public fun nickname(value: String): MockUserProfileBuilder
|
||||
public fun email(value: String): MockUserProfileBuilder
|
||||
public fun age(value: Int): MockUserProfileBuilder
|
||||
public fun qLevel(value: Int): MockUserProfileBuilder
|
||||
public fun sex(value: UserProfile.Sex): MockUserProfileBuilder
|
||||
public fun sign(value: String): MockUserProfileBuilder
|
||||
public fun friendGroupId(value: Int): MockUserProfileBuilder
|
||||
|
||||
public companion object {
|
||||
@JvmStatic
|
||||
@JvmName("newBuilder")
|
||||
public operator fun invoke(): MockUserProfileBuilder = MockUPBuilderImpl()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造一个 [UserProfile]
|
||||
*
|
||||
* @see MockUserProfileBuilder
|
||||
*/
|
||||
public inline fun buildUserProfile(block: MockUserProfileBuilder.() -> Unit): UserProfile {
|
||||
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
|
||||
return MockUserProfileBuilder().apply(block).build()
|
||||
}
|
||||
|
||||
internal class MockUPBuilderImpl : MockUserProfileBuilder, UserProfile {
|
||||
override var nickname: String = ""
|
||||
override var email: String = ""
|
||||
override var age: Int = -1
|
||||
override var qLevel: Int = -1
|
||||
override var sex: UserProfile.Sex = UserProfile.Sex.UNKNOWN
|
||||
override var sign: String = ""
|
||||
override var friendGroupId: Int = 0
|
||||
|
||||
// unmodifiable
|
||||
override fun build(): UserProfile {
|
||||
return object : UserProfile by this {}
|
||||
}
|
||||
|
||||
override fun nickname(value: String): MockUserProfileBuilder = apply {
|
||||
nickname = value
|
||||
}
|
||||
|
||||
override fun email(value: String): MockUserProfileBuilder = apply {
|
||||
email = value
|
||||
}
|
||||
|
||||
override fun age(value: Int): MockUserProfileBuilder = apply {
|
||||
age = value
|
||||
}
|
||||
|
||||
override fun qLevel(value: Int): MockUserProfileBuilder = apply {
|
||||
qLevel = value
|
||||
}
|
||||
|
||||
override fun sex(value: UserProfile.Sex): MockUserProfileBuilder = apply {
|
||||
sex = value
|
||||
}
|
||||
|
||||
override fun sign(value: String): MockUserProfileBuilder = apply {
|
||||
sign = value
|
||||
}
|
||||
|
||||
override fun friendGroupId(value: Int): MockUserProfileBuilder = apply {
|
||||
friendGroupId = value
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class UserProfileServiceImpl : UserProfileService {
|
||||
val db = ConcurrentHashMap<Long, UserProfile>()
|
||||
val def = buildUserProfile {
|
||||
}
|
||||
|
||||
override suspend fun doQueryUserProfile(id: Long): UserProfile {
|
||||
return db[id] ?: def
|
||||
}
|
||||
|
||||
override suspend fun putUserProfile(id: Long, profile: UserProfile) {
|
||||
db[id] = profile
|
||||
}
|
||||
|
||||
}
|
154
mirai-core-mock/src/userprofile/contactinfos.kt
Normal file
154
mirai-core-mock/src/userprofile/contactinfos.kt
Normal file
@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.userprofile
|
||||
|
||||
import net.mamoe.mirai.contact.MemberPermission
|
||||
import net.mamoe.mirai.data.FriendInfo
|
||||
import net.mamoe.mirai.data.MemberInfo
|
||||
import net.mamoe.mirai.data.StrangerInfo
|
||||
import net.mamoe.mirai.data.UserInfo
|
||||
import net.mamoe.mirai.utils.currentTimeSeconds
|
||||
|
||||
public interface MockUserInfoBuilder {
|
||||
public fun uin(value: Long): MockUserInfoBuilder
|
||||
|
||||
public fun nick(value: String): MockUserInfoBuilder
|
||||
|
||||
public fun remark(value: String): MockUserInfoBuilder
|
||||
|
||||
public fun build(): UserInfo
|
||||
|
||||
public companion object {
|
||||
@JvmStatic
|
||||
@JvmName("builder")
|
||||
public operator fun invoke(): MockUserInfoBuilder = ThreeInOneInfoBuilder()
|
||||
|
||||
@JvmSynthetic
|
||||
public inline fun create(action: MockUserInfoBuilder.() -> Unit): UserInfo = invoke().apply(action).build()
|
||||
}
|
||||
}
|
||||
|
||||
public interface MockFriendInfoBuilder : MockUserInfoBuilder {
|
||||
public override fun build(): FriendInfo
|
||||
|
||||
override fun uin(value: Long): MockFriendInfoBuilder
|
||||
|
||||
override fun nick(value: String): MockFriendInfoBuilder
|
||||
|
||||
override fun remark(value: String): MockFriendInfoBuilder
|
||||
|
||||
public fun friendGroupId(value: Int): MockFriendInfoBuilder
|
||||
|
||||
public companion object {
|
||||
@JvmStatic
|
||||
@JvmName("builder")
|
||||
public operator fun invoke(): MockFriendInfoBuilder = ThreeInOneInfoBuilder()
|
||||
|
||||
@JvmSynthetic
|
||||
public inline fun create(action: MockFriendInfoBuilder.() -> Unit): FriendInfo = invoke().apply(action).build()
|
||||
}
|
||||
}
|
||||
|
||||
public interface MockMemberInfoBuilder : MockUserInfoBuilder {
|
||||
override fun build(): MemberInfo
|
||||
|
||||
public fun nameCard(value: String): MockMemberInfoBuilder
|
||||
|
||||
public fun specialTitle(value: String): MockMemberInfoBuilder
|
||||
|
||||
public fun anonymousId(value: String?): MockMemberInfoBuilder
|
||||
|
||||
public fun joinTimestamp(value: Int): MockMemberInfoBuilder
|
||||
|
||||
public fun lastSpeakTimestamp(value: Int): MockMemberInfoBuilder
|
||||
|
||||
public fun isOfficialBot(value: Boolean): MockMemberInfoBuilder
|
||||
|
||||
public fun permission(value: MemberPermission): MockMemberInfoBuilder
|
||||
|
||||
override fun uin(value: Long): MockMemberInfoBuilder
|
||||
|
||||
override fun nick(value: String): MockMemberInfoBuilder
|
||||
|
||||
override fun remark(value: String): MockMemberInfoBuilder
|
||||
|
||||
public companion object {
|
||||
@JvmStatic
|
||||
@JvmName("builder")
|
||||
public operator fun invoke(): MockMemberInfoBuilder = ThreeInOneInfoBuilder()
|
||||
|
||||
@JvmSynthetic
|
||||
public inline fun create(action: MockMemberInfoBuilder.() -> Unit): MemberInfo = invoke().apply(action).build()
|
||||
}
|
||||
}
|
||||
|
||||
public interface MockStrangerInfoBuilder : MockUserInfoBuilder {
|
||||
public fun fromGroup(value: Long): MockUserInfoBuilder
|
||||
|
||||
override fun uin(value: Long): MockUserInfoBuilder
|
||||
|
||||
override fun nick(value: String): MockUserInfoBuilder
|
||||
|
||||
override fun remark(value: String): MockUserInfoBuilder
|
||||
|
||||
override fun build(): StrangerInfo
|
||||
|
||||
public companion object {
|
||||
@JvmStatic
|
||||
@JvmName("builder")
|
||||
public operator fun invoke(): MockStrangerInfoBuilder = ThreeInOneInfoBuilder()
|
||||
|
||||
|
||||
@JvmSynthetic
|
||||
public inline fun create(action: MockStrangerInfoBuilder.() -> Unit): StrangerInfo =
|
||||
invoke().apply(action).build()
|
||||
}
|
||||
}
|
||||
|
||||
private class ThreeInOneInfoBuilder :
|
||||
MockUserInfoBuilder,
|
||||
MockFriendInfoBuilder,
|
||||
MockMemberInfoBuilder,
|
||||
MockStrangerInfoBuilder,
|
||||
|
||||
UserInfo,
|
||||
FriendInfo,
|
||||
MemberInfo,
|
||||
StrangerInfo {
|
||||
|
||||
override var nameCard: String = ""
|
||||
override var permission: MemberPermission = MemberPermission.MEMBER
|
||||
override var specialTitle: String = ""
|
||||
override var muteTimestamp: Int = 0
|
||||
override var joinTimestamp: Int = currentTimeSeconds().toInt()
|
||||
override var lastSpeakTimestamp: Int = 0
|
||||
override var isOfficialBot: Boolean = false
|
||||
override var fromGroup: Long = 0L
|
||||
override var remark: String = ""
|
||||
override var uin: Long = 0
|
||||
override var nick: String = ""
|
||||
override var anonymousId: String? = null
|
||||
override var friendGroupId: Int = 0
|
||||
|
||||
override fun build(): ThreeInOneInfoBuilder = this
|
||||
|
||||
override fun nameCard(value: String): ThreeInOneInfoBuilder = apply { this.nameCard = value }
|
||||
override fun specialTitle(value: String): ThreeInOneInfoBuilder = apply { this.specialTitle = value }
|
||||
override fun anonymousId(value: String?): ThreeInOneInfoBuilder = apply { this.anonymousId = value }
|
||||
override fun joinTimestamp(value: Int): ThreeInOneInfoBuilder = apply { this.joinTimestamp = value }
|
||||
override fun lastSpeakTimestamp(value: Int): ThreeInOneInfoBuilder = apply { this.lastSpeakTimestamp = value }
|
||||
override fun isOfficialBot(value: Boolean): ThreeInOneInfoBuilder = apply { this.isOfficialBot = value }
|
||||
override fun fromGroup(value: Long): ThreeInOneInfoBuilder = apply { this.fromGroup = value }
|
||||
override fun uin(value: Long): ThreeInOneInfoBuilder = apply { this.uin = value }
|
||||
override fun nick(value: String): ThreeInOneInfoBuilder = apply { this.nick = value }
|
||||
override fun remark(value: String): ThreeInOneInfoBuilder = apply { this.remark = value }
|
||||
override fun permission(value: MemberPermission): ThreeInOneInfoBuilder = apply { this.permission = value }
|
||||
override fun friendGroupId(value: Int): ThreeInOneInfoBuilder = apply { this.friendGroupId = value }
|
||||
}
|
36
mirai-core-mock/src/utils/MemberInfo.kt
Normal file
36
mirai-core-mock/src/utils/MemberInfo.kt
Normal file
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.utils
|
||||
|
||||
import net.mamoe.mirai.contact.MemberPermission
|
||||
import net.mamoe.mirai.data.MemberInfo
|
||||
|
||||
public fun simpleMemberInfo(
|
||||
uin: Long,
|
||||
name: String,
|
||||
nick: String = name,
|
||||
nameCard: String = "",
|
||||
remark: String = "",
|
||||
permission: MemberPermission,
|
||||
specialTitle: String = "",
|
||||
): MemberInfo {
|
||||
return object : MemberInfo {
|
||||
override val nameCard: String get() = nameCard
|
||||
override val permission: MemberPermission get() = permission
|
||||
override val specialTitle: String get() = specialTitle
|
||||
override val muteTimestamp: Int get() = 0
|
||||
override val joinTimestamp: Int get() = 0
|
||||
override val lastSpeakTimestamp: Int get() = 0
|
||||
override val isOfficialBot: Boolean get() = false
|
||||
override val uin: Long get() = uin
|
||||
override val nick: String get() = nick
|
||||
override val remark: String get() = remark
|
||||
}
|
||||
}
|
158
mirai-core-mock/src/utils/MockActionsScope.kt
Normal file
158
mirai-core-mock/src/utils/MockActionsScope.kt
Normal file
@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.utils
|
||||
|
||||
import net.mamoe.mirai.contact.MemberPermission
|
||||
import net.mamoe.mirai.contact.User
|
||||
import net.mamoe.mirai.event.events.MemberPermissionChangeEvent
|
||||
import net.mamoe.mirai.event.events.MemberSpecialTitleChangeEvent
|
||||
import net.mamoe.mirai.message.MessageReceipt
|
||||
import net.mamoe.mirai.message.data.*
|
||||
import net.mamoe.mirai.mock.MockActions
|
||||
import net.mamoe.mirai.mock.contact.MockNormalMember
|
||||
import net.mamoe.mirai.mock.contact.MockUser
|
||||
import net.mamoe.mirai.mock.contact.MockUserOrBot
|
||||
|
||||
/**
|
||||
* 广播一些模拟事件
|
||||
*/
|
||||
public inline fun broadcastMockEvents(action: MockActionsScope.() -> Unit) {
|
||||
return MockActionsScopeInstance.action()
|
||||
}
|
||||
|
||||
@PublishedApi
|
||||
internal val MockActionsScopeInstance: MockActionsScope = object : MockActionsScope {}
|
||||
|
||||
|
||||
public interface MockActionsScope { // use context receivers in the future
|
||||
/**
|
||||
* 修改 [MockUserOrBot.nick] 并广播相关事件 (如 [FriendNickChangedEvent])
|
||||
*/
|
||||
@MockActionsDsl
|
||||
public suspend infix fun MockUserOrBot.nickChangesTo(value: String) {
|
||||
return MockActions.fireNickChanged(this, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改 [MockNormalMember.nameCard] 并广播 [MemberCardChangeEvent]
|
||||
*/
|
||||
@MockActionsDsl
|
||||
public suspend infix fun MockNormalMember.nameCardChangesTo(value: String) {
|
||||
return MockActions.fireNameCardChanged(this, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改 [MockNormalMember.specialTitle] 并广播 [MemberSpecialTitleChangeEvent]
|
||||
*/
|
||||
@MockActionsDsl
|
||||
public suspend infix fun MockNormalMember.specialTitleChangesTo(value: String) {
|
||||
return MockActions.fireSpecialTitleChanged(this, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改一名成员的权限并广播 [MemberPermissionChangeEvent]
|
||||
*/
|
||||
@MockActionsDsl
|
||||
public suspend infix fun MockNormalMember.permissionChangesTo(perm: MemberPermission) {
|
||||
return MockActions.firePermissionChanged(this, perm)
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播 [this] 被 [actor] 戳了的事件([NudgeEvent])
|
||||
*
|
||||
* - [actor] 戳了戳 [this] 的 XXXX
|
||||
*/
|
||||
@MockActionsDsl
|
||||
public suspend fun MockUserOrBot.nudgedBy(actor: MockUserOrBot, block: NudgeDsl.() -> Unit = {}) {
|
||||
actor.nudged0(this, NudgeDsl().also(block))
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播 [target] 被 [this] 戳了的事件([NudgeEvent])
|
||||
*
|
||||
* - [this] 戳了戳 [target] 的 XXXX
|
||||
*/
|
||||
@MockActionsDsl
|
||||
public suspend fun MockUserOrBot.nudges(target: MockUserOrBot, block: NudgeDsl.() -> Unit = {}) {
|
||||
nudged0(target, NudgeDsl().also(block))
|
||||
}
|
||||
|
||||
/**
|
||||
* @see [MockUser.says]
|
||||
*/
|
||||
@MockActionsDsl
|
||||
public suspend infix fun MockUser.says(block: MessageChainBuilder.() -> Unit): MessageChain {
|
||||
return says(buildMessageChain(block))
|
||||
}
|
||||
|
||||
/**
|
||||
* @see [MockUser.says]
|
||||
*/
|
||||
@MockActionsDsl
|
||||
public suspend infix fun MockUser.saysMessage(block: () -> Message): MessageChain {
|
||||
// no contract because compiler error
|
||||
// contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
|
||||
return says(block())
|
||||
}
|
||||
|
||||
/**
|
||||
* 令消息原作者撤回一条消息
|
||||
*/
|
||||
@MockActionsDsl
|
||||
public suspend fun MessageChain.recalledBySender() {
|
||||
return MockActions.fireMessageRecalled(this, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* 令消息原作者撤回一条消息
|
||||
*/
|
||||
@MockActionsDsl
|
||||
public suspend fun MessageSource.recalledBySender() {
|
||||
return MockActions.fireMessageRecalled(this, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* 令消息原作者撤回一条消息
|
||||
*/
|
||||
@MockActionsDsl
|
||||
public suspend fun MessageReceipt<*>.recalledBySender() {
|
||||
this.source.recalledBy(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* 令 [operator] 撤回一条消息
|
||||
*
|
||||
* @param operator 当 [operator] 为 null 时代表是发送者自己撤回
|
||||
*/
|
||||
@MockActionsDsl
|
||||
public suspend infix fun MessageChain.recalledBy(operator: User?) {
|
||||
return MockActions.fireMessageRecalled(this, operator)
|
||||
}
|
||||
|
||||
/**
|
||||
* 令 [operator] 撤回一条消息
|
||||
*
|
||||
* @param operator 当 [operator] 为 null 时代表是发送者自己撤回
|
||||
*/
|
||||
@MockActionsDsl
|
||||
public suspend infix fun MessageSource.recalledBy(operator: User?) {
|
||||
return MockActions.fireMessageRecalled(this, operator)
|
||||
}
|
||||
|
||||
/**
|
||||
* 令 [operator] 撤回一条消息
|
||||
*
|
||||
* @param operator 当 [operator] 为 null 时代表是发送者自己撤回
|
||||
*/
|
||||
@MockActionsDsl
|
||||
public suspend infix fun MessageReceipt<*>.recalledBy(operator: User?) {
|
||||
this.source.recalledBy(operator)
|
||||
}
|
||||
}
|
76
mirai-core-mock/src/utils/MockConversions.kt
Normal file
76
mirai-core-mock/src/utils/MockConversions.kt
Normal file
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
@file:JvmName("MockConversions")
|
||||
|
||||
package net.mamoe.mirai.mock.utils
|
||||
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.contact.*
|
||||
import net.mamoe.mirai.message.data.OnlineAudio
|
||||
import net.mamoe.mirai.mock.MockBot
|
||||
import net.mamoe.mirai.mock.MockBotDSL
|
||||
import net.mamoe.mirai.mock.contact.*
|
||||
import net.mamoe.mirai.utils.ExternalResource
|
||||
import kotlin.contracts.contract
|
||||
|
||||
|
||||
public fun Bot.mock(): MockBot {
|
||||
contract { returns() implies (this@mock is MockBot) }
|
||||
return this as MockBot
|
||||
}
|
||||
|
||||
public fun Group.mock(): MockGroup {
|
||||
contract { returns() implies (this@mock is MockGroup) }
|
||||
return this as MockGroup
|
||||
}
|
||||
|
||||
public fun NormalMember.mock(): MockNormalMember {
|
||||
contract { returns() implies (this@mock is MockNormalMember) }
|
||||
return this as MockNormalMember
|
||||
}
|
||||
|
||||
public fun Contact.mock(): MockContact {
|
||||
contract { returns() implies (this@mock is MockContact) }
|
||||
return this as MockContact
|
||||
}
|
||||
|
||||
public fun AnonymousMember.mock(): MockAnonymousMember {
|
||||
contract { returns() implies (this@mock is MockAnonymousMember) }
|
||||
return this as MockAnonymousMember
|
||||
}
|
||||
|
||||
public fun Friend.mock(): MockFriend {
|
||||
contract { returns() implies (this@mock is MockFriend) }
|
||||
return this as MockFriend
|
||||
}
|
||||
|
||||
public fun Member.mock(): MockMember {
|
||||
contract { returns() implies (this@mock is MockMember) }
|
||||
return this as MockMember
|
||||
}
|
||||
|
||||
public fun OtherClient.mock(): MockOtherClient {
|
||||
contract { returns() implies (this@mock is MockOtherClient) }
|
||||
return this as MockOtherClient
|
||||
}
|
||||
|
||||
public fun Stranger.mock(): MockStranger {
|
||||
contract { returns() implies (this@mock is MockStranger) }
|
||||
return this as MockStranger
|
||||
}
|
||||
|
||||
/**
|
||||
* @see MockBot.uploadOnlineAudio
|
||||
*/
|
||||
@MockBotDSL
|
||||
public suspend fun ExternalResource.mockUploadAsOnlineAudio(bot: MockBot): OnlineAudio {
|
||||
return bot.uploadOnlineAudio(this)
|
||||
}
|
||||
|
38
mirai-core-mock/src/utils/NameGenerator.kt
Normal file
38
mirai-core-mock/src/utils/NameGenerator.kt
Normal file
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.utils
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
/**
|
||||
* 名称生成器
|
||||
*
|
||||
* 部分事件没有 `nick`, `name` 等相关的字段以确定名字,
|
||||
* [NameGenerator] 的作用就是在无法确定一个准确的名字的时候生成一个默认的名字
|
||||
*/
|
||||
public interface NameGenerator {
|
||||
public fun nextGroupName(): String
|
||||
public fun nextFriendName(): String
|
||||
|
||||
public companion object {
|
||||
private val DEFAULT: NameGenerator = SimpleNameGenerator()
|
||||
|
||||
@JvmStatic
|
||||
public fun getDefault(): NameGenerator = DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
public open class SimpleNameGenerator : NameGenerator {
|
||||
private val groupCounter = AtomicInteger(0)
|
||||
private val friendCounter = AtomicInteger(0)
|
||||
|
||||
override fun nextGroupName(): String = "Testing Group #" + groupCounter.getAndIncrement()
|
||||
override fun nextFriendName(): String = "Testing Friend #" + friendCounter.getAndIncrement()
|
||||
}
|
73
mirai-core-mock/src/utils/NudgeDsl.kt
Normal file
73
mirai-core-mock/src/utils/NudgeDsl.kt
Normal file
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.utils
|
||||
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.contact.*
|
||||
import net.mamoe.mirai.event.broadcast
|
||||
import net.mamoe.mirai.event.events.NudgeEvent
|
||||
import net.mamoe.mirai.mock.contact.MockUserOrBot
|
||||
|
||||
/**
|
||||
* 构造 Nudge 的 DSL
|
||||
*
|
||||
* @see MockActionsScope.nudgedBy
|
||||
*/
|
||||
public class NudgeDsl {
|
||||
@set:JvmSynthetic
|
||||
public var action: String = "戳了戳"
|
||||
|
||||
@set:JvmSynthetic
|
||||
public var suffix: String = ""
|
||||
|
||||
@MockActionsDsl
|
||||
public fun action(value: String): NudgeDsl = apply { action = value }
|
||||
|
||||
@MockActionsDsl
|
||||
public fun suffix(value: String): NudgeDsl = apply { suffix = value }
|
||||
}
|
||||
|
||||
@PublishedApi
|
||||
internal suspend fun MockUserOrBot.nudged0(target: MockUserOrBot, dsl: NudgeDsl) {
|
||||
|
||||
when {
|
||||
this is Member && target is Member -> {
|
||||
if (this.group != target.group)
|
||||
error("Cross group nudging")
|
||||
}
|
||||
|
||||
this is AnonymousMember -> error("anonymous member can't starting a nudge action")
|
||||
target is AnonymousMember -> error("anonymous member is not nudgeable")
|
||||
|
||||
this is Bot && target is Bot -> error("Not yet support bot nudging bot")
|
||||
}
|
||||
|
||||
val subject: Contact = when {
|
||||
this is Member -> this.group
|
||||
target is Member -> target.group
|
||||
|
||||
this is Friend -> this
|
||||
target is Friend -> target
|
||||
|
||||
this is Stranger -> this
|
||||
target is Stranger -> target
|
||||
|
||||
else -> error("Not yet support $target nudging $this")
|
||||
}
|
||||
|
||||
NudgeEvent(
|
||||
from = this,
|
||||
target = target,
|
||||
subject = subject,
|
||||
action = dsl.action,
|
||||
suffix = dsl.suffix,
|
||||
).broadcast()
|
||||
|
||||
}
|
19
mirai-core-mock/src/utils/event.kt
Normal file
19
mirai-core-mock/src/utils/event.kt
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.utils
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.mamoe.mirai.event.Event
|
||||
import net.mamoe.mirai.event.broadcast
|
||||
|
||||
|
||||
public fun <T : Event> T.broadcastBlocking(): T = apply {
|
||||
runBlocking { broadcast() }
|
||||
}
|
25
mirai-core-mock/src/utils/http.kt
Normal file
25
mirai-core-mock/src/utils/http.kt
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
@file:JvmMultifileClass
|
||||
@file:JvmName("MiraiUtils")
|
||||
@file:Suppress("NOTHING_TO_INLINE")
|
||||
|
||||
package net.mamoe.mirai.mock.utils
|
||||
|
||||
import kotlin.jvm.JvmMultifileClass
|
||||
import kotlin.jvm.JvmName
|
||||
|
||||
public fun String.plusHttpSubpath(subpath: String): String {
|
||||
|
||||
if (this[this.lastIndex] == '/') return this + subpath
|
||||
|
||||
return "$this/$subpath"
|
||||
}
|
||||
|
41
mirai-core-mock/src/utils/image.kt
Normal file
41
mirai-core-mock/src/utils/image.kt
Normal file
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.utils
|
||||
|
||||
import net.mamoe.mirai.message.data.Image
|
||||
|
||||
import java.awt.Color
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.ByteArrayOutputStream
|
||||
import javax.imageio.ImageIO
|
||||
|
||||
internal fun randomImage(): BufferedImage {
|
||||
val width = (500..800).random()
|
||||
val height = (500..800).random()
|
||||
val image = BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
|
||||
val graphics = image.createGraphics()
|
||||
for (x in 0 until width) {
|
||||
for (y in 0 until height) {
|
||||
graphics.color = Color(
|
||||
(0..0xFFFFFF).random()
|
||||
)
|
||||
graphics.drawRect(x, y, 1, 1)
|
||||
}
|
||||
}
|
||||
graphics.dispose()
|
||||
return image
|
||||
}
|
||||
|
||||
internal fun BufferedImage.saveToBytes(): ByteArray = ByteArrayOutputStream().apply {
|
||||
ImageIO.write(this@saveToBytes, "png", this)
|
||||
}.toByteArray()
|
||||
|
||||
|
||||
public fun Image.Key.randomImageContent(): ByteArray = randomImage().saveToBytes()
|
15
mirai-core-mock/src/utils/mockdsl.kt
Normal file
15
mirai-core-mock/src/utils/mockdsl.kt
Normal file
@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||
|
||||
package net.mamoe.mirai.mock.utils
|
||||
|
||||
@DslMarker
|
||||
public annotation class MockActionsDsl
|
100
mirai-core-mock/test/AbsoluteFileTest.kt
Normal file
100
mirai-core-mock/test/AbsoluteFileTest.kt
Normal file
@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.test
|
||||
|
||||
import com.google.common.jimfs.Configuration
|
||||
import com.google.common.jimfs.Jimfs
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import net.mamoe.mirai.contact.MemberPermission
|
||||
import net.mamoe.mirai.event.events.GroupMessageEvent
|
||||
import net.mamoe.mirai.message.data.FileMessage
|
||||
import net.mamoe.mirai.mock.internal.remotefile.absolutefile.MockRemoteFiles
|
||||
import net.mamoe.mirai.mock.internal.serverfs.MockServerFileSystemImpl
|
||||
import net.mamoe.mirai.mock.utils.simpleMemberInfo
|
||||
import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
|
||||
import net.mamoe.mirai.utils.cast
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.nio.file.FileSystem
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotEquals
|
||||
|
||||
internal class AbsoluteFileTest : MockBotTestBase() {
|
||||
private val tmpfs: FileSystem = Jimfs.newFileSystem(Configuration.unix())
|
||||
private val disk = bot.tmpResourceServer.mockServerFileDisk
|
||||
private val group = bot.addGroup(11L, "a").also { println(it.owner) }
|
||||
private val fsys = MockServerFileSystemImpl(disk.cast())
|
||||
private val files = MockRemoteFiles(group, fsys)
|
||||
|
||||
@AfterEach
|
||||
internal fun release() {
|
||||
tmpfs.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
internal fun listFileAndFolder() = runTest {
|
||||
val folder = files.root.createFolder("test1")
|
||||
files.root.createFolder("test2")
|
||||
val file = folder.uploadNewFile("test.txt", "cc".toByteArray().toExternalResource().toAutoCloseable())
|
||||
folder.uploadNewFile("test.txt", "cac".toByteArray().toExternalResource().toAutoCloseable())
|
||||
println(files.root.folders().toList())
|
||||
println(files.root.resolveFolder("test1")!!.files().toList())
|
||||
assertEquals(2, files.root.folders().toList().size)
|
||||
assertEquals(2, files.root.resolveFolder("test1")!!.files().toList().size)
|
||||
assertEquals("test.txt", files.root.resolveFolder("test1")!!.files().toList()[0].name)
|
||||
assertEquals("test1", files.root.resolveFolderById(folder.id)!!.name)
|
||||
assertEquals("test.txt", files.root.resolveFileById(file.id, true)!!.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
internal fun testDeleteAndMoveTo() = runTest {
|
||||
val f = files.root.createFolder("test")
|
||||
val ff = f.uploadNewFile("test.txt", "ccc".toByteArray().toExternalResource())
|
||||
val fff = files.root.resolveFileById(ff.id, true)!!
|
||||
assertEquals(fff, ff)
|
||||
f.renameTo("test2")
|
||||
assertEquals("test2", files.root.folders().first().name)
|
||||
fff.refresh()
|
||||
assertEquals(f.absolutePath + "/" + fff.name, fff.absolutePath)
|
||||
fff.moveTo(files.root)
|
||||
assertEquals("/${fff.name}", fff.absolutePath)
|
||||
assertEquals(files.root, fff.parent)
|
||||
fff.delete()
|
||||
assertEquals(false, fff.exists())
|
||||
assertEquals(null, files.root.resolveFileById(fff.id))
|
||||
}
|
||||
|
||||
@Test
|
||||
internal fun testSendAndDownload() = runTest {
|
||||
val f = files.root.uploadNewFile("test.txt", "c".toByteArray().toExternalResource())
|
||||
println(files.fileSystem.findByPath("/test.txt").first().path)
|
||||
runAndReceiveEventBroadcast {
|
||||
group.addMember(simpleMemberInfo(222, "bb", permission = MemberPermission.MEMBER))
|
||||
.saysMessage { f.toMessage() }
|
||||
}.let { events ->
|
||||
assertEquals(1, events.size)
|
||||
assertEquals(true, events[0].cast<GroupMessageEvent>().message.contains(FileMessage))
|
||||
}
|
||||
assertEquals("c", f.getUrl()!!.toUrl().readText())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRename() = runTest {
|
||||
val folder = files.root.createFolder("test1")
|
||||
val file = folder.uploadNewFile("test.txt", "content".toByteArray().toExternalResource().toAutoCloseable())
|
||||
assertEquals(file.id, folder.resolveFiles("test.txt").first().id)
|
||||
folder.renameTo("test2")
|
||||
file.refresh()
|
||||
assertEquals(true, file.exists())
|
||||
assertNotEquals(null, folder.resolveFiles("test.txt").firstOrNull())
|
||||
}
|
||||
}
|
138
mirai-core-mock/test/DslTest.kt
Normal file
138
mirai-core-mock/test/DslTest.kt
Normal file
@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.test
|
||||
|
||||
import net.mamoe.mirai.contact.MemberPermission
|
||||
import net.mamoe.mirai.mock.MockActions
|
||||
import net.mamoe.mirai.mock.MockBotFactory
|
||||
import net.mamoe.mirai.mock.userprofile.MockMemberInfoBuilder
|
||||
import net.mamoe.mirai.mock.utils.NudgeDsl
|
||||
import net.mamoe.mirai.mock.utils.broadcastMockEvents
|
||||
import net.mamoe.mirai.mock.utils.mockUploadAsOnlineAudio
|
||||
import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
|
||||
import java.io.File
|
||||
|
||||
|
||||
/*
|
||||
* This file only for showing MockDSL and how to use mock bot.
|
||||
* Not included in testing running
|
||||
*/
|
||||
|
||||
@Suppress("unused")
|
||||
internal suspend fun dslTest() {
|
||||
val bot = MockBotFactory.newMockBotBuilder().create()
|
||||
|
||||
bot.addFriend(5, "OhMyFriend")
|
||||
|
||||
bot.addGroup(1, "").apply {
|
||||
addMember(
|
||||
MockMemberInfoBuilder.create {
|
||||
uin(541)
|
||||
nameCard("Dmo")
|
||||
permission(MemberPermission.OWNER)
|
||||
}
|
||||
)
|
||||
}
|
||||
bot.addGroup(7, "")
|
||||
.appendMember(MockMemberInfoBuilder.create { // Kotlin
|
||||
uin(571)
|
||||
nameCard("Hi")
|
||||
permission(MemberPermission.ADMINISTRATOR)
|
||||
})
|
||||
.appendMember(
|
||||
MockMemberInfoBuilder.invoke() // Java, MockMemberInfoBuilder.builder() in java
|
||||
.uin(1654441)
|
||||
.nameCard("60")
|
||||
.permission(MemberPermission.MEMBER)
|
||||
.specialTitle("ST")
|
||||
.build()
|
||||
)
|
||||
|
||||
|
||||
// 群成员 70 说了一句话
|
||||
bot.getGroupOrFail(50).getOrFail(70).says("0")
|
||||
|
||||
// 群成员 1 发了一条语音
|
||||
bot.getGroupOrFail(1).getOrFail(1).says { // Kotlin
|
||||
+File("helloworld.amr").toExternalResource().toAutoCloseable().mockUploadAsOnlineAudio(bot)
|
||||
}
|
||||
/*
|
||||
Java:
|
||||
bot.getGroupOrFail(1).getOrFail(1).says(() -> {
|
||||
return bot.uploadOnlineAudio(
|
||||
ExternalResource.toExternalResource(new File("")).toAutoCloseable()
|
||||
);
|
||||
});
|
||||
*/
|
||||
|
||||
|
||||
|
||||
broadcastMockEvents { // Required for kotlin
|
||||
|
||||
// 50 拍了拍 bot 的 sys32
|
||||
bot.getGroupOrFail(5).getOrFail(50).nudges(bot) {
|
||||
action("拍了拍")
|
||||
suffix("sys32")
|
||||
}
|
||||
MockActions.fireNudge( // Java
|
||||
bot.getGroupOrFail(5).getOrFail(50),
|
||||
bot,
|
||||
/*new*/ NudgeDsl().action("拍了拍").suffix("sys32")
|
||||
)
|
||||
|
||||
// 1 拍了拍 bot 的 sys32
|
||||
bot.nudgedBy(bot.getGroupOrFail(1).getOrFail(1)) {
|
||||
action("拍了拍")
|
||||
suffix("sys32")
|
||||
}
|
||||
|
||||
|
||||
// 群成员 2 修改了群名片
|
||||
bot.getGroupOrFail(1).getOrFail(2) nameCardChangesTo "Test"
|
||||
MockActions.fireNameCardChanged( // Java
|
||||
bot.getGroupOrFail(1).getOrFail(2), "Test"
|
||||
)
|
||||
|
||||
// 群成员 2 被群主修改了头衔
|
||||
bot.getGroupOrFail(1).getOrFail(2) specialTitleChangesTo "管埋员"
|
||||
MockActions.fireSpecialTitleChanged( // Java
|
||||
bot.getGroupOrFail(1).getOrFail(2), "管埋员"
|
||||
)
|
||||
|
||||
// 群主修改了群成员 2 的权限为 Administrator
|
||||
bot.getGroupOrFail(1).getOrFail(2) permissionChangesTo MemberPermission.ADMINISTRATOR
|
||||
MockActions.firePermissionChanged( // Java
|
||||
bot.getGroupOrFail(1).getOrFail(2),
|
||||
MemberPermission.ADMINISTRATOR
|
||||
)
|
||||
|
||||
// 群主撤回了一条群员消息
|
||||
bot.getGroupOrFail(1).owner.recallMessage( // Kotlin & Java
|
||||
bot.getGroupOrFail(1).getOrFail(1) says { append("SB") }
|
||||
)
|
||||
}
|
||||
|
||||
// 新的入群申请
|
||||
bot.getGroupOrFail(50).broadcastNewMemberJoinRequestEvent(
|
||||
requester = 3,
|
||||
requesterName = "Him188moe",
|
||||
message = "Hi!",
|
||||
).reject(message = "Hello!")
|
||||
|
||||
// 新的好友申请
|
||||
bot.broadcastNewFriendRequestEvent(
|
||||
requester = 1,
|
||||
requesterNick = "Karlatemp",
|
||||
fromGroup = 0,
|
||||
message = "さくらが落ちる",
|
||||
).accept()
|
||||
|
||||
bot.broadcastNewFriendRequestEvent(9, "", 0, "").reject()
|
||||
}
|
41
mirai-core-mock/test/FsServerTest.kt
Normal file
41
mirai-core-mock/test/FsServerTest.kt
Normal file
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION", "DEPRECATION_ERROR")
|
||||
|
||||
package net.mamoe.mirai.mock.test
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.mamoe.mirai.mock.resserver.TmpResourceServer
|
||||
import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
|
||||
import net.mamoe.mirai.utils.mkParentDirs
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.io.path.writeText
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@Suppress("RemoveExplicitTypeArguments")
|
||||
internal class FsServerTest {
|
||||
@Test
|
||||
fun testFsServer() = runBlocking<Unit> {
|
||||
val fsServer = TmpResourceServer.newInMemoryTmpResourceServer()
|
||||
fsServer.startupServer()
|
||||
val testFile = "Test".toByteArray().toExternalResource()
|
||||
val resourceId = fsServer.uploadResource(testFile)
|
||||
val response = fsServer.resolveHttpUrl(resourceId).toURL().readText()
|
||||
assertEquals("Test", response)
|
||||
|
||||
val pt0 = fsServer.storageRoot.resolve("/rand/etc/randrand/somedata")
|
||||
pt0.mkParentDirs()
|
||||
pt0.writeText("Test")
|
||||
|
||||
assertEquals("Test", fsServer.resolveHttpUrlByPath(pt0).toURL().readText())
|
||||
|
||||
fsServer.close()
|
||||
}
|
||||
}
|
47
mirai-core-mock/test/ImageUploadTest.kt
Normal file
47
mirai-core-mock/test/ImageUploadTest.kt
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.test
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.mamoe.mirai.message.data.Image
|
||||
import net.mamoe.mirai.message.data.Image.Key.queryUrl
|
||||
import net.mamoe.mirai.mock.MockBotFactory
|
||||
import net.mamoe.mirai.mock.utils.randomImageContent
|
||||
import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.net.URL
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_METHOD)
|
||||
internal class ImageUploadTest {
|
||||
internal val bot = MockBotFactory.newMockBotBuilder()
|
||||
.id(1234567890)
|
||||
.nick("Sakura")
|
||||
.create()
|
||||
|
||||
@AfterEach
|
||||
internal fun botDestroy() {
|
||||
bot.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testImageUpload() = runBlocking<Unit> {
|
||||
val data = Image.randomImageContent()
|
||||
val img = bot.asFriend.uploadImage(
|
||||
data.toExternalResource().toAutoCloseable()
|
||||
)
|
||||
println(img.imageId)
|
||||
assertTrue {
|
||||
data.contentEquals(URL(img.queryUrl()).readBytes())
|
||||
}
|
||||
}
|
||||
}
|
58
mirai-core-mock/test/MockBotTestBase.kt
Normal file
58
mirai-core-mock/test/MockBotTestBase.kt
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.test
|
||||
|
||||
import net.mamoe.mirai.event.Event
|
||||
import net.mamoe.mirai.event.GlobalEventChannel
|
||||
import net.mamoe.mirai.mock.MockBotFactory
|
||||
import net.mamoe.mirai.mock.internal.MockBotImpl
|
||||
import net.mamoe.mirai.mock.utils.MockActionsScope
|
||||
import net.mamoe.mirai.mock.utils.broadcastMockEvents
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_METHOD)
|
||||
internal open class MockBotTestBase : TestBase() {
|
||||
internal val bot = MockBotFactory.newMockBotBuilder()
|
||||
.id((100000000L..321111111L).random())
|
||||
.nick("Kafusumi")
|
||||
.create()
|
||||
|
||||
@AfterEach
|
||||
internal fun `$$bot dispose`() {
|
||||
bot.close()
|
||||
}
|
||||
|
||||
internal suspend fun runAndReceiveEventBroadcast(
|
||||
action: suspend MockActionsScope.() -> Unit
|
||||
): List<Event> {
|
||||
|
||||
contract {
|
||||
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
|
||||
val result = mutableListOf<Event>()
|
||||
val listener = GlobalEventChannel.subscribeAlways<Event> {
|
||||
result.add(this)
|
||||
}
|
||||
|
||||
broadcastMockEvents {
|
||||
action()
|
||||
}
|
||||
|
||||
(bot as MockBotImpl).joinEventBroadcast()
|
||||
|
||||
listener.cancel()
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
49
mirai-core-mock/test/MsgDbTest.kt
Normal file
49
mirai-core-mock/test/MsgDbTest.kt
Normal file
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.test
|
||||
|
||||
import net.mamoe.mirai.message.data.MessageSourceKind
|
||||
import net.mamoe.mirai.message.data.messageChainOf
|
||||
import net.mamoe.mirai.mock.database.MessageDatabase
|
||||
import net.mamoe.mirai.mock.database.MessageInfo
|
||||
import net.mamoe.mirai.mock.database.mockMsgDatabaseId
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.random.Random
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
internal class MsgDbTest {
|
||||
@Test
|
||||
fun testIdConversion() {
|
||||
repeat(50) {
|
||||
val id1 = Random.nextInt()
|
||||
val id2 = Random.nextInt()
|
||||
val msgInfo = MessageInfo(
|
||||
mixinedMsgId = mockMsgDatabaseId(id1, id2),
|
||||
sender = 0, subject = 0, kind = MessageSourceKind.FRIEND, time = 0,
|
||||
messageChainOf()
|
||||
)
|
||||
assertEquals(id1, msgInfo.id)
|
||||
assertEquals(id2, msgInfo.internal)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDatabase() {
|
||||
val db = MessageDatabase.newDefaultDatabase()
|
||||
db.connect()
|
||||
|
||||
repeat(90) {
|
||||
val info = db.newMessageInfo(Random.nextLong(), Random.nextLong(), MessageSourceKind.FRIEND, 0, messageChainOf())
|
||||
assertEquals(info, db.queryMessageInfo(info.mixinedMsgId))
|
||||
}
|
||||
|
||||
db.disconnect()
|
||||
}
|
||||
}
|
50
mirai-core-mock/test/TestBase.kt
Normal file
50
mirai-core-mock/test/TestBase.kt
Normal file
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.test
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.runBlocking
|
||||
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.fail
|
||||
import java.net.URL
|
||||
import kotlin.reflect.jvm.jvmName
|
||||
import kotlin.test.assertFails
|
||||
|
||||
internal open class TestBase {
|
||||
|
||||
internal inline fun <reified T> assertIsInstance(value: Any?, block: T.() -> Unit = {}) {
|
||||
if (value !is T) {
|
||||
fail { "Actual value $value (${value?.javaClass}) is not instanceof ${T::class.jvmName}" }
|
||||
}
|
||||
block(value)
|
||||
}
|
||||
|
||||
internal fun runTest(action: suspend CoroutineScope.() -> Unit) {
|
||||
runBlocking(block = action)
|
||||
}
|
||||
|
||||
internal fun List<Event>.dropMessagePrePost() = filterNot {
|
||||
it is MessagePreSendEvent || it is MessagePostSendEvent<*>
|
||||
}
|
||||
|
||||
internal fun List<Event>.dropMsgChat() = filterNot {
|
||||
it is MessageEvent || it is MessagePreSendEvent || it is MessagePostSendEvent<*>
|
||||
}
|
||||
|
||||
internal fun String.toUrl(): URL = URL(this)
|
||||
|
||||
internal inline fun <T> T.runAndAssertFails(block: T.() -> Unit) {
|
||||
assertFails { block() }
|
||||
}
|
||||
|
||||
}
|
118
mirai-core-mock/test/TxFsDiskTest.kt
Normal file
118
mirai-core-mock/test/TxFsDiskTest.kt
Normal file
@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.test
|
||||
|
||||
import com.google.common.jimfs.Configuration
|
||||
import com.google.common.jimfs.Jimfs
|
||||
import net.mamoe.mirai.mock.resserver.MockServerFileDisk
|
||||
import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.nio.file.Files
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFails
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
internal class TxFsDiskTest {
|
||||
val tmpfs = Jimfs.newFileSystem(Configuration.unix())
|
||||
val disk = MockServerFileDisk.newFileDisk(tmpfs.getPath("/disk"))
|
||||
private fun splitLine() = println("==================================================================")
|
||||
|
||||
@AfterEach
|
||||
fun release() {
|
||||
println("===================[ FILE SYSTEM STRUCT DUMP ]========================")
|
||||
Files.walk(tmpfs.getPath("/")).use { s ->
|
||||
s.forEach { pt ->
|
||||
println(pt)
|
||||
}
|
||||
}
|
||||
println("===================[ ]========================")
|
||||
tmpfs.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDisk() {
|
||||
val system = disk.newFsSystem()
|
||||
val root = system.root
|
||||
println(root)
|
||||
println(root.fileInfo)
|
||||
|
||||
splitLine()
|
||||
|
||||
kotlin.run {
|
||||
val subdir = root.mksubdir("a-dir", 0)
|
||||
println(subdir)
|
||||
println(subdir.fileInfo)
|
||||
assertEquals("/a-dir", subdir.path)
|
||||
|
||||
assertFails { root.moveTo(subdir) }
|
||||
|
||||
val children = root.listFiles()!!.onEach { println(it) }.toList()
|
||||
assertEquals(1, children.size)
|
||||
assertEquals(subdir, children[0])
|
||||
assertEquals(root, subdir.parent)
|
||||
|
||||
subdir.delete()
|
||||
println(subdir)
|
||||
assertFalse { subdir.exists }
|
||||
assertFalse { subdir.isFile }
|
||||
assertFalse { subdir.isDirectory }
|
||||
assertTrue { subdir.toString().startsWith("<not exists>") }
|
||||
assertFails { subdir.fileInfo }
|
||||
}
|
||||
|
||||
splitLine()
|
||||
|
||||
kotlin.run {
|
||||
val newFile = root.uploadFile(
|
||||
"test.txt",
|
||||
"""A""".toByteArray().toExternalResource().toAutoCloseable(),
|
||||
5
|
||||
)
|
||||
val newFileInfo = newFile.fileInfo
|
||||
assertEquals(5, newFileInfo.creator)
|
||||
assertEquals(root, newFile.parent)
|
||||
assertEquals("test.txt", newFile.name)
|
||||
assertEquals("/test.txt", newFile.path)
|
||||
|
||||
newFile.rename("hello world.bin")
|
||||
assertEquals("hello world.bin", newFile.name)
|
||||
|
||||
|
||||
val children = root.listFiles()!!.onEach { println(it) }.toList()
|
||||
assertEquals(1, children.size)
|
||||
assertEquals(children[0], newFile)
|
||||
|
||||
val subdir = root.mksubdir("1", 3)
|
||||
newFile.moveTo(subdir)
|
||||
assertEquals("/1/hello world.bin", newFile.path)
|
||||
|
||||
assertEquals(subdir, newFile.parent)
|
||||
|
||||
val children1 = subdir.listFiles()!!.toList()
|
||||
assertEquals(1, children1.size)
|
||||
assertEquals(newFile, children1[0])
|
||||
|
||||
val children2 = root.listFiles()!!.toList()
|
||||
assertEquals(1, children2.size)
|
||||
assertEquals(subdir, children2[0])
|
||||
|
||||
|
||||
assertEquals(newFile, system.findByPath("/1/hello world.bin").firstOrNull())
|
||||
|
||||
println("TEST SUB DIR: $subdir")
|
||||
|
||||
// TODO: Download content
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
208
mirai-core-mock/test/mock/MessagingTest.kt
Normal file
208
mirai-core-mock/test/mock/MessagingTest.kt
Normal file
@ -0,0 +1,208 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.test.mock
|
||||
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import net.mamoe.mirai.contact.MemberPermission
|
||||
import net.mamoe.mirai.event.events.*
|
||||
import net.mamoe.mirai.message.data.MessageSource.Key.recall
|
||||
import net.mamoe.mirai.message.data.OnlineMessageSource
|
||||
import net.mamoe.mirai.message.data.PlainText
|
||||
import net.mamoe.mirai.message.data.messageChainOf
|
||||
import net.mamoe.mirai.message.data.source
|
||||
import net.mamoe.mirai.mock.MockActions.mockFireRecalled
|
||||
import net.mamoe.mirai.mock.test.MockBotTestBase
|
||||
import net.mamoe.mirai.mock.utils.broadcastMockEvents
|
||||
import net.mamoe.mirai.mock.utils.simpleMemberInfo
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFails
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertSame
|
||||
|
||||
internal class MessagingTest: MockBotTestBase() {
|
||||
|
||||
@Test
|
||||
internal fun testMessageEventBroadcast() = runTest {
|
||||
runAndReceiveEventBroadcast {
|
||||
bot.addGroup(5597122, "testing!")
|
||||
.addMember(simpleMemberInfo(5971, "test", permission = MemberPermission.OWNER))
|
||||
.says("Hello World")
|
||||
|
||||
bot.addFriend(9815, "tester").says("Msg By TestFriend")
|
||||
|
||||
bot.addStranger(987166, "sudo").says("How are you")
|
||||
|
||||
bot.getGroupOrFail(5597122).sendMessage("Testing message")
|
||||
bot.getFriendOrFail(9815).sendMessage("Hi my friend")
|
||||
bot.getStrangerOrFail(987166).sendMessage("How are you")
|
||||
}.let { events ->
|
||||
assertEquals(9, events.size)
|
||||
|
||||
assertIsInstance<GroupMessageEvent>(events[0]) {
|
||||
assertEquals("Hello World", message.contentToString())
|
||||
assertEquals("test", senderName)
|
||||
assertEquals(5971, sender.id)
|
||||
assertEquals(5597122, group.id)
|
||||
assertIsInstance<OnlineMessageSource.Incoming.FromGroup>(message.source)
|
||||
}
|
||||
assertIsInstance<FriendMessageEvent>(events[1]) {
|
||||
assertEquals("Msg By TestFriend", message.contentToString())
|
||||
assertEquals("tester", senderName)
|
||||
assertEquals(9815, sender.id)
|
||||
assertIsInstance<OnlineMessageSource.Incoming.FromFriend>(message.source)
|
||||
|
||||
}
|
||||
assertIsInstance<StrangerMessageEvent>(events[2]) {
|
||||
assertEquals("How are you", message.contentToString())
|
||||
assertEquals("sudo", senderName)
|
||||
assertEquals(987166, sender.id)
|
||||
assertIsInstance<OnlineMessageSource.Incoming.FromStranger>(message.source)
|
||||
}
|
||||
|
||||
assertIsInstance<GroupMessagePreSendEvent>(events[3])
|
||||
assertIsInstance<GroupMessagePostSendEvent>(events[4]) {
|
||||
assertIsInstance<OnlineMessageSource.Outgoing.ToGroup>(receipt!!.source)
|
||||
}
|
||||
assertIsInstance<FriendMessagePreSendEvent>(events[5])
|
||||
assertIsInstance<FriendMessagePostSendEvent>(events[6]) {
|
||||
assertIsInstance<OnlineMessageSource.Outgoing.ToFriend>(receipt!!.source)
|
||||
}
|
||||
assertIsInstance<StrangerMessagePreSendEvent>(events[7])
|
||||
assertIsInstance<StrangerMessagePostSendEvent>(events[8]) {
|
||||
assertIsInstance<OnlineMessageSource.Outgoing.ToStranger>(receipt!!.source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
internal fun testNudge() = runTest {
|
||||
val group = bot.addGroup(1, "1")
|
||||
val nudgeSender = group.addMember(simpleMemberInfo(3, "3", permission = MemberPermission.MEMBER))
|
||||
val nudged = group.addMember(simpleMemberInfo(4, "4", permission = MemberPermission.MEMBER))
|
||||
|
||||
val myFriend = bot.addFriend(1, "514")
|
||||
val myStranger = bot.addStranger(2, "awef")
|
||||
|
||||
runAndReceiveEventBroadcast {
|
||||
nudged.nudgedBy(nudgeSender)
|
||||
nudged.nudge().sendTo(group)
|
||||
myFriend.nudges(bot)
|
||||
myStranger.nudges(bot)
|
||||
myFriend.nudgedBy(bot)
|
||||
myStranger.nudgedBy(bot)
|
||||
}.let { events ->
|
||||
assertEquals(6, events.size)
|
||||
assertIsInstance<NudgeEvent>(events[0]) {
|
||||
assertSame(nudgeSender, this.from)
|
||||
assertSame(nudged, this.target)
|
||||
assertSame(group, this.subject)
|
||||
}
|
||||
assertIsInstance<NudgeEvent>(events[1]) {
|
||||
assertSame(bot, this.from)
|
||||
assertSame(nudged, this.target)
|
||||
assertSame(group, this.subject)
|
||||
}
|
||||
assertIsInstance<NudgeEvent>(events[2]) {
|
||||
assertSame(myFriend, this.from)
|
||||
assertSame(bot, this.target)
|
||||
assertSame(myFriend, this.subject)
|
||||
}
|
||||
assertIsInstance<NudgeEvent>(events[3]) {
|
||||
assertSame(myStranger, this.from)
|
||||
assertSame(bot, this.target)
|
||||
assertSame(myStranger, this.subject)
|
||||
}
|
||||
assertIsInstance<NudgeEvent>(events[4]) {
|
||||
assertSame(bot, this.from)
|
||||
assertSame(myFriend, this.target)
|
||||
assertSame(myFriend, this.subject)
|
||||
}
|
||||
assertIsInstance<NudgeEvent>(events[5]) {
|
||||
assertSame(bot, this.from)
|
||||
assertSame(myStranger, this.target)
|
||||
assertSame(myStranger, this.subject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
internal fun testRoamingMessages() = runTest {
|
||||
val mockFriend = bot.addFriend(1, "1")
|
||||
broadcastMockEvents {
|
||||
mockFriend says { append("Testing!") }
|
||||
mockFriend says { append("Test2!") }
|
||||
}
|
||||
mockFriend.sendMessage("Pong!")
|
||||
|
||||
mockFriend.roamingMessages.getAllMessages().toList().let { messages ->
|
||||
assertEquals(3, messages.size)
|
||||
assertEquals(messageChainOf(PlainText("Testing!")), messages[0])
|
||||
assertEquals(messageChainOf(PlainText("Test2!")), messages[1])
|
||||
assertEquals(messageChainOf(PlainText("Pong!")), messages[2])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
internal fun testMessageRecallEventBroadcast() = runTest {
|
||||
val group = bot.addGroup(8484846, "g")
|
||||
val admin = group.addMember(simpleMemberInfo(945474, "admin", permission = MemberPermission.ADMINISTRATOR))
|
||||
val sender = group.addMember(simpleMemberInfo(178711, "usr", permission = MemberPermission.MEMBER))
|
||||
|
||||
runAndReceiveEventBroadcast {
|
||||
sender.says("Test").recalledBySender()
|
||||
sender.says("Admin recall").recalledBy(admin)
|
||||
mockFireRecalled(group.sendMessage("Hello world"), admin)
|
||||
sender.says("Hi").recall()
|
||||
admin.says("I'm admin").let { resp ->
|
||||
resp.recall()
|
||||
assertFails { resp.recall() }.let(::println)
|
||||
}
|
||||
}.dropMsgChat().let { events ->
|
||||
assertEquals(5, events.size)
|
||||
assertIsInstance<MessageRecallEvent.GroupRecall>(events[0]) {
|
||||
assertNull(operator)
|
||||
assertSame(sender, author)
|
||||
}
|
||||
assertIsInstance<MessageRecallEvent.GroupRecall>(events[1]) {
|
||||
assertSame(admin, operator)
|
||||
assertSame(sender, author)
|
||||
}
|
||||
assertIsInstance<MessageRecallEvent.GroupRecall>(events[2]) {
|
||||
assertSame(admin, operator)
|
||||
assertSame(group.botAsMember, author)
|
||||
}
|
||||
assertIsInstance<MessageRecallEvent.GroupRecall>(events[3]) {
|
||||
assertSame(null, operator)
|
||||
assertSame(sender, author)
|
||||
}
|
||||
assertIsInstance<MessageRecallEvent.GroupRecall>(events[4]) {
|
||||
assertSame(null, operator)
|
||||
assertSame(admin, author)
|
||||
}
|
||||
}
|
||||
|
||||
val root = group.addMember(simpleMemberInfo(54986565, "root", permission = MemberPermission.OWNER))
|
||||
|
||||
runAndReceiveEventBroadcast {
|
||||
sender.says("0").runAndAssertFails { recall() }
|
||||
admin.says("0").runAndAssertFails { recall() }
|
||||
root.says("0").runAndAssertFails { recall() }
|
||||
group.sendMessage("Hi").recall()
|
||||
}.dropMsgChat().let { events ->
|
||||
assertEquals(1, events.size)
|
||||
assertIsInstance<MessageRecallEvent.GroupRecall>(events[0]) {
|
||||
assertEquals(group.botAsMember, author)
|
||||
assertEquals(null, operator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
102
mirai-core-mock/test/mock/MockBotBaseTest.kt
Normal file
102
mirai-core-mock/test/mock/MockBotBaseTest.kt
Normal file
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.test.mock
|
||||
|
||||
import net.mamoe.mirai.Mirai
|
||||
import net.mamoe.mirai.contact.MemberPermission
|
||||
import net.mamoe.mirai.event.events.MemberPermissionChangeEvent
|
||||
import net.mamoe.mirai.mock.contact.MockNormalMember
|
||||
import net.mamoe.mirai.mock.test.MockBotTestBase
|
||||
import net.mamoe.mirai.mock.userprofile.buildUserProfile
|
||||
import net.mamoe.mirai.mock.utils.simpleMemberInfo
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertSame
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
internal class MockBotBaseTest : MockBotTestBase() {
|
||||
|
||||
@Test
|
||||
internal fun testMockBotMocking() = runTest {
|
||||
repeat(50) { i ->
|
||||
bot.addFriend(20000L + i, "usr$i")
|
||||
bot.addStranger(10000L + i, "stranger$i")
|
||||
bot.addGroup(798100000L + i, "group$i")
|
||||
}
|
||||
assertEquals(50, bot.friends.size)
|
||||
assertEquals(50, bot.strangers.size)
|
||||
assertEquals(50, bot.groups.size)
|
||||
|
||||
repeat(50) { i ->
|
||||
assertEquals("usr$i", bot.getFriendOrFail(20000L + i).nick)
|
||||
assertEquals("stranger$i", bot.getStrangerOrFail(10000L + i).nick)
|
||||
|
||||
val group = bot.getGroupOrFail(798100000L + i)
|
||||
assertEquals("group$i", group.name)
|
||||
assertSame(group.botAsMember, group.owner)
|
||||
assertSame(MemberPermission.OWNER, group.botPermission)
|
||||
assertEquals(0, group.members.size)
|
||||
}
|
||||
|
||||
val mockGroup = bot.getGroupOrFail(798100000L)
|
||||
repeat(50) { i ->
|
||||
mockGroup.appendMember(simpleMemberInfo(3700000L + i, "member$i", permission = MemberPermission.MEMBER))
|
||||
}
|
||||
repeat(50) { i ->
|
||||
val member = mockGroup.getOrFail(3700000L + i)
|
||||
assertEquals(MemberPermission.MEMBER, member.permission)
|
||||
assertEquals("member$i", member.nick)
|
||||
assertTrue(member.nameCard.isEmpty())
|
||||
assertEquals(MemberPermission.OWNER, mockGroup.botPermission)
|
||||
}
|
||||
|
||||
val newOwner: MockNormalMember
|
||||
runAndReceiveEventBroadcast {
|
||||
newOwner = mockGroup.addMember(simpleMemberInfo(84485417, "root", permission = MemberPermission.OWNER))
|
||||
}.let { events ->
|
||||
assertEquals(0, events.size)
|
||||
}
|
||||
assertEquals(MemberPermission.OWNER, newOwner.permission)
|
||||
assertEquals(MemberPermission.MEMBER, mockGroup.botPermission)
|
||||
assertSame(newOwner, mockGroup.owner)
|
||||
|
||||
val newNewOwner = mockGroup.getOrFail(3700000L)
|
||||
runAndReceiveEventBroadcast {
|
||||
mockGroup.changeOwner(newNewOwner)
|
||||
}.let { events ->
|
||||
assertEquals(2, events.size)
|
||||
assertIsInstance<MemberPermissionChangeEvent>(events[0]) {
|
||||
assertSame(newNewOwner, member)
|
||||
assertSame(MemberPermission.OWNER, new)
|
||||
assertSame(MemberPermission.MEMBER, origin)
|
||||
}
|
||||
assertIsInstance<MemberPermissionChangeEvent>(events[1]) {
|
||||
assertSame(newOwner, member)
|
||||
assertSame(MemberPermission.OWNER, origin)
|
||||
assertSame(MemberPermission.MEMBER, new)
|
||||
}
|
||||
}
|
||||
assertEquals(MemberPermission.OWNER, newNewOwner.permission)
|
||||
assertEquals(MemberPermission.MEMBER, newOwner.permission)
|
||||
assertEquals(MemberPermission.MEMBER, mockGroup.botPermission)
|
||||
assertSame(newNewOwner, mockGroup.owner)
|
||||
}
|
||||
|
||||
@Test
|
||||
internal fun testQueryProfile() = runTest {
|
||||
val service = bot.userProfileService
|
||||
val profile = buildUserProfile {
|
||||
nickname("Test0")
|
||||
}
|
||||
service.putUserProfile(1, profile)
|
||||
assertSame(profile, Mirai.queryProfile(bot, 1))
|
||||
}
|
||||
|
||||
}
|
80
mirai-core-mock/test/mock/MockBotEventTest.kt
Normal file
80
mirai-core-mock/test/mock/MockBotEventTest.kt
Normal file
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.test.mock
|
||||
|
||||
import net.mamoe.mirai.event.events.*
|
||||
import net.mamoe.mirai.mock.test.MockBotTestBase
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
internal class MockBotEventTest : MockBotTestBase() {
|
||||
@Test
|
||||
fun testBotOnlineEvent() = runTest {
|
||||
runAndReceiveEventBroadcast {
|
||||
bot.login()
|
||||
}.let { events ->
|
||||
assertEquals(1, events.size)
|
||||
assertIsInstance<BotOnlineEvent>(events[0])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBotOfflineEvent() = runTest {
|
||||
runAndReceiveEventBroadcast {
|
||||
bot.broadcastOfflineEvent()
|
||||
}.let { events ->
|
||||
assertEquals(1, events.size)
|
||||
assertIsInstance<BotOfflineEvent>(events[0])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBotRelogin() = runTest {
|
||||
bot.login()
|
||||
runAndReceiveEventBroadcast {
|
||||
bot.login()
|
||||
}.let { events ->
|
||||
assertEquals(2, events.size)
|
||||
assertIsInstance<BotOnlineEvent>(events[0])
|
||||
assertIsInstance<BotReloginEvent>(events[1])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMockAvatarChange() = runTest {
|
||||
assertEquals("http://q.qlogo.cn/g?b=qq&nk=${bot.id}&s=640", bot.avatarUrl)
|
||||
runAndReceiveEventBroadcast {
|
||||
bot.avatarUrl = "http://localhost/test.png"
|
||||
assertEquals("http://localhost/test.png", bot.avatarUrl)
|
||||
}.let { events ->
|
||||
assertEquals(1, events.size)
|
||||
assertIsInstance<BotAvatarChangedEvent>(events[0])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBotNickChangedEvent() = runTest {
|
||||
runAndReceiveEventBroadcast {
|
||||
bot.nickNoEvent = "HiHi"
|
||||
bot.nick = "AAAA"
|
||||
bot nickChangesTo "BBBB"
|
||||
}.let { events ->
|
||||
assertEquals(2, events.size)
|
||||
assertIsInstance<BotNickChangedEvent>(events[0]) {
|
||||
assertEquals("HiHi", from)
|
||||
assertEquals("AAAA", to)
|
||||
}
|
||||
assertIsInstance<BotNickChangedEvent>(events[1]) {
|
||||
assertEquals("AAAA", from)
|
||||
assertEquals("BBBB", to)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
49
mirai-core-mock/test/mock/MockFriendGroupsTest.kt
Normal file
49
mirai-core-mock/test/mock/MockFriendGroupsTest.kt
Normal file
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.test.mock
|
||||
|
||||
import net.mamoe.mirai.mock.test.MockBotTestBase
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
internal class MockFriendGroupsTest : MockBotTestBase() {
|
||||
@Test
|
||||
internal fun testFriendGroupsDefaultEmpty() = runTest {
|
||||
assertEquals(1, bot.friendGroups.asCollection().size)
|
||||
assertEquals(bot.friendGroups.default, bot.friendGroups[0])
|
||||
assertEquals(bot.friendGroups.default, bot.friendGroups.asCollection().iterator().next())
|
||||
}
|
||||
|
||||
@Test
|
||||
internal fun testFriendGroupCreating() = runTest {
|
||||
val group = bot.friendGroups.create("Test")
|
||||
println(group.id)
|
||||
assertEquals(2, bot.friendGroups.asCollection().size)
|
||||
assertEquals(group, bot.friendGroups[group.id])
|
||||
}
|
||||
|
||||
@Test
|
||||
internal fun testFriendGroupReferences() = runTest {
|
||||
val group = bot.friendGroups.create("Test")
|
||||
|
||||
val friend = bot.addFriend(5, "Test")
|
||||
assertEquals(bot.friendGroups.default, friend.friendGroup)
|
||||
assertEquals(0, friend.mockApi.friendGroupId)
|
||||
|
||||
group.moveIn(friend)
|
||||
assertEquals(group, friend.friendGroup)
|
||||
assertEquals(group.id, friend.mockApi.friendGroupId)
|
||||
|
||||
group.delete()
|
||||
assertEquals(bot.friendGroups.default, friend.friendGroup)
|
||||
|
||||
assertEquals(0, friend.mockApi.friendGroupId)
|
||||
}
|
||||
}
|
149
mirai-core-mock/test/mock/MockFriendTest.kt
Normal file
149
mirai-core-mock/test/mock/MockFriendTest.kt
Normal file
@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.test.mock
|
||||
|
||||
import net.mamoe.mirai.event.events.*
|
||||
import net.mamoe.mirai.mock.internal.contact.MockImage
|
||||
import net.mamoe.mirai.mock.test.MockBotTestBase
|
||||
import net.mamoe.mirai.utils.cast
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotEquals
|
||||
import kotlin.test.assertSame
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
internal class MockFriendTest : MockBotTestBase() {
|
||||
|
||||
@Test
|
||||
internal fun testNewFriendRequest() = runTest {
|
||||
runAndReceiveEventBroadcast {
|
||||
bot.broadcastNewFriendRequestEvent(
|
||||
1, "Hi", 0, "Hello!"
|
||||
).reject()
|
||||
}.let { events ->
|
||||
assertEquals(1, events.size)
|
||||
assertIsInstance<NewFriendRequestEvent>(events[0]) {
|
||||
assertEquals(1, fromId)
|
||||
assertEquals("Hi", fromNick)
|
||||
assertEquals(0, fromGroupId)
|
||||
assertEquals("Hello!", message)
|
||||
}
|
||||
assertEquals(bot.friends.size, 0)
|
||||
}
|
||||
|
||||
runAndReceiveEventBroadcast {
|
||||
bot.broadcastNewFriendRequestEvent(
|
||||
1, "Hi", 0, "Hello!"
|
||||
).accept()
|
||||
}.let { events ->
|
||||
assertEquals(2, events.size, events.toString())
|
||||
assertIsInstance<NewFriendRequestEvent>(events[0]) {
|
||||
assertEquals(1, fromId)
|
||||
assertEquals("Hi", fromNick)
|
||||
assertEquals(0, fromGroupId)
|
||||
assertEquals("Hello!", message)
|
||||
}
|
||||
|
||||
assertIsInstance<FriendAddEvent>(events[1]) {
|
||||
assertEquals(1, friend.id)
|
||||
assertEquals("Hi", friend.nick)
|
||||
assertSame(friend, bot.getFriend(friend.id))
|
||||
}
|
||||
|
||||
assertEquals(1, bot.friends.size)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFriendAvatarChangedEvent() = runTest {
|
||||
runAndReceiveEventBroadcast {
|
||||
bot.addFriend(111, "a").changeAvatarUrl(MockImage.random(bot).getUrl(bot))
|
||||
bot.addFriend(222, "b")
|
||||
}.let { events ->
|
||||
assertIsInstance<FriendAvatarChangedEvent>(events[0])
|
||||
assertEquals(111, events[0].cast<FriendAvatarChangedEvent>().friend.id)
|
||||
assertNotEquals("", bot.getFriend(111)!!.avatarUrl)
|
||||
assertNotEquals("", bot.getFriend(222)!!.avatarUrl)
|
||||
assertNotEquals("", bot.getFriend(222)!!.avatarUrl.toUrl().readText())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFriendRemarkChangeEvent() = runTest {
|
||||
runAndReceiveEventBroadcast {
|
||||
bot.addFriend(1, "").remark = "Test"
|
||||
}.let { events ->
|
||||
assertEquals(1, events.size)
|
||||
assertIsInstance<FriendRemarkChangeEvent>(events[0]) {
|
||||
assertEquals(1, this.friend.id)
|
||||
assertEquals("", oldRemark)
|
||||
assertEquals("Test", newRemark)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFriendRequestAndAddEvent() = runTest {
|
||||
runAndReceiveEventBroadcast {
|
||||
bot.broadcastNewFriendRequestEvent(
|
||||
1, "Test", 0, "Hi"
|
||||
).accept()
|
||||
bot.broadcastNewFriendRequestEvent(
|
||||
2, "Hi", 1, "0"
|
||||
).reject()
|
||||
}.let { events ->
|
||||
assertEquals(3, events.size)
|
||||
assertIsInstance<NewFriendRequestEvent>(events[0]) {
|
||||
assertEquals(1, fromId)
|
||||
assertEquals("Test", fromNick)
|
||||
assertEquals(0, fromGroupId)
|
||||
assertEquals("Hi", message)
|
||||
}
|
||||
assertIsInstance<FriendAddEvent>(events[1]) {
|
||||
assertEquals(1, friend.id)
|
||||
assertEquals("Test", friend.nick)
|
||||
}
|
||||
assertIsInstance<NewFriendRequestEvent>(events[2]) {
|
||||
assertEquals(2, fromId)
|
||||
assertEquals("Hi", fromNick)
|
||||
assertEquals(1, fromGroupId)
|
||||
assertEquals("0", message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFriendNickChangedEvent() = runTest {
|
||||
runAndReceiveEventBroadcast {
|
||||
bot.addFriend(0, "Old").nick = "Test"
|
||||
}.let { events ->
|
||||
assertEquals(1, events.size)
|
||||
assertIsInstance<FriendNickChangedEvent>(events[0]) {
|
||||
assertEquals("Old", from)
|
||||
assertEquals("Test", to)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFriendInputStatusChangedEvent() = runTest {
|
||||
runAndReceiveEventBroadcast {
|
||||
bot.addFriend(1, "a").broadcastFriendInputStateChange(true)
|
||||
}.let { events ->
|
||||
assertEquals(1, events.size)
|
||||
assertIsInstance<FriendInputStatusChangedEvent>(events[0]) {
|
||||
assertTrue(inputting)
|
||||
assertSame(bot.getFriend(1), friend)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
460
mirai-core-mock/test/mock/MockGroupTest.kt
Normal file
460
mirai-core-mock/test/mock/MockGroupTest.kt
Normal file
@ -0,0 +1,460 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.test.mock
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import net.mamoe.mirai.contact.MemberPermission
|
||||
import net.mamoe.mirai.contact.announcement.AnnouncementParametersBuilder
|
||||
import net.mamoe.mirai.contact.isBotMuted
|
||||
import net.mamoe.mirai.data.GroupHonorType
|
||||
import net.mamoe.mirai.event.events.*
|
||||
import net.mamoe.mirai.message.data.FileMessage
|
||||
import net.mamoe.mirai.mock.contact.announcement.MockOnlineAnnouncement
|
||||
import net.mamoe.mirai.mock.test.MockBotTestBase
|
||||
import net.mamoe.mirai.mock.userprofile.MockMemberInfoBuilder
|
||||
import net.mamoe.mirai.mock.utils.simpleMemberInfo
|
||||
import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
|
||||
import net.mamoe.mirai.utils.cast
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.*
|
||||
|
||||
internal class MockGroupTest : MockBotTestBase() {
|
||||
@Test
|
||||
internal fun testMockGroupJoinRequest() = runTest {
|
||||
val group = bot.addGroup(9875555515, "test")
|
||||
|
||||
runAndReceiveEventBroadcast {
|
||||
group.broadcastNewMemberJoinRequestEvent(
|
||||
100000000, "demo", "msg"
|
||||
).accept()
|
||||
}.let { events ->
|
||||
assertEquals(2, events.size)
|
||||
assertIsInstance<MemberJoinRequestEvent>(events[0]) {
|
||||
assertEquals(100000000, fromId)
|
||||
assertEquals("demo", fromNick)
|
||||
assertEquals("msg", message)
|
||||
}
|
||||
assertIsInstance<MemberJoinEvent>(events[1]) {
|
||||
assertEquals(100000000, member.id)
|
||||
assertEquals("demo", member.nick)
|
||||
}
|
||||
}
|
||||
|
||||
val member = group.getOrFail(100000000)
|
||||
assertEquals(MemberPermission.MEMBER, member.permission)
|
||||
}
|
||||
|
||||
@Test
|
||||
internal fun testMockBotJoinGroupRequest() = runTest {
|
||||
val invitor = bot.addFriend(5710, "demo")
|
||||
runAndReceiveEventBroadcast {
|
||||
invitor.broadcastInviteBotJoinGroupRequestEvent(999999999, "test")
|
||||
.accept()
|
||||
}.let { events ->
|
||||
assertEquals(2, events.size)
|
||||
assertIsInstance<BotInvitedJoinGroupRequestEvent>(events[0]) {
|
||||
assertEquals(5710, invitorId)
|
||||
assertEquals("demo", invitorNick)
|
||||
assertEquals(999999999, groupId)
|
||||
assertEquals("test", groupName)
|
||||
}
|
||||
assertIsInstance<BotJoinGroupEvent>(events[1]) {
|
||||
assertNotSame(group.botAsMember, group.owner)
|
||||
assertEquals(MemberPermission.MEMBER, group.botPermission)
|
||||
assertEquals(999999999, group.id)
|
||||
assertEquals(MemberPermission.OWNER, group.owner.permission)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
internal fun testGroupAnnouncements() = runTest {
|
||||
val group = bot.addGroup(8484541, "87")
|
||||
runAndReceiveEventBroadcast {
|
||||
group.announcements.publish(
|
||||
MockOnlineAnnouncement(
|
||||
content = "dlroW olleH",
|
||||
parameters = AnnouncementParametersBuilder().apply { this.sendToNewMember = true }.build(),
|
||||
senderId = 9711221,
|
||||
allConfirmed = false,
|
||||
confirmedMembersCount = 0,
|
||||
publicationTime = 0
|
||||
)
|
||||
)
|
||||
group.announcements.publish(
|
||||
MockOnlineAnnouncement(
|
||||
content = "Hello World",
|
||||
parameters = AnnouncementParametersBuilder().apply { this.sendToNewMember = true }.build(),
|
||||
senderId = 971121,
|
||||
allConfirmed = false,
|
||||
confirmedMembersCount = 0,
|
||||
publicationTime = 0
|
||||
)
|
||||
)
|
||||
}.let { events ->
|
||||
assertEquals(2, events.size)
|
||||
assertIsInstance<GroupEntranceAnnouncementChangeEvent>(events[0])
|
||||
}
|
||||
val anc = group.announcements.asFlow().toList()
|
||||
assertEquals(1, anc.size)
|
||||
assertEquals("Hello World", anc[0].content)
|
||||
assertFalse(anc[0].fid.isEmpty())
|
||||
assertEquals(anc[0], group.announcements.get(anc[0].fid))
|
||||
}
|
||||
|
||||
@Test
|
||||
internal fun testLeave() = runTest {
|
||||
runAndReceiveEventBroadcast {
|
||||
bot.addGroup(1, "1").quit()
|
||||
bot.addFriend(2, "2").delete()
|
||||
bot.addStranger(3, "3").delete()
|
||||
bot.addGroup(4, "4")
|
||||
.addMember(simpleMemberInfo(5, "5", permission = MemberPermission.MEMBER))
|
||||
.broadcastMemberLeave()
|
||||
bot.addGroup(6, "6")
|
||||
.addMember(simpleMemberInfo(7, "7", permission = MemberPermission.OWNER))
|
||||
.broadcastKickBot()
|
||||
}.let { events ->
|
||||
assertEquals(5, events.size)
|
||||
assertIsInstance<BotLeaveEvent.Active>(events[0]) {
|
||||
assertEquals(1, group.id)
|
||||
}
|
||||
assertIsInstance<FriendDeleteEvent>(events[1]) {
|
||||
assertEquals(2, friend.id)
|
||||
}
|
||||
assertIsInstance<StrangerRelationChangeEvent.Deleted>(events[2]) {
|
||||
assertEquals(3, stranger.id)
|
||||
}
|
||||
assertIsInstance<MemberLeaveEvent>(events[3]) {
|
||||
assertEquals(4, group.id)
|
||||
assertEquals(5, member.id)
|
||||
}
|
||||
assertIsInstance<BotLeaveEvent.Kick>(events[4]) {
|
||||
assertEquals(6, group.id)
|
||||
assertEquals(7, operator.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Test
|
||||
internal fun testGroupFileV1() = runTest {
|
||||
val fsroot = bot.addGroup(5417, "58aw").filesRoot
|
||||
fsroot.resolve("helloworld.txt").uploadAndSend(
|
||||
"HelloWorld".toByteArray().toExternalResource().toAutoCloseable()
|
||||
)
|
||||
assertEquals(1, fsroot.listFilesCollection().size)
|
||||
assertEquals(
|
||||
"HelloWorld",
|
||||
fsroot.resolve("helloworld.txt")
|
||||
.getDownloadInfo()!!
|
||||
.url.toUrl()
|
||||
.also { println("Mock file url: $it") }
|
||||
.readText()
|
||||
)
|
||||
fsroot.resolve("helloworld.txt").delete()
|
||||
assertEquals(0, fsroot.listFilesCollection().size)
|
||||
}
|
||||
|
||||
@Test
|
||||
internal fun testMemberHonorChangeEvent() = runTest {
|
||||
runAndReceiveEventBroadcast {
|
||||
val group = bot.addGroup(111, "aa")
|
||||
val member1 = group.addMember(simpleMemberInfo(222, "bb", permission = MemberPermission.MEMBER))
|
||||
val member2 = group.addMember(simpleMemberInfo(333, "cc", permission = MemberPermission.MEMBER))
|
||||
group.honorMembers[GroupHonorType.ACTIVE] = member1
|
||||
group.changeHonorMember(member2, GroupHonorType.ACTIVE)
|
||||
}.let { events ->
|
||||
assertEquals(2, events.size)
|
||||
assertIsInstance<MemberHonorChangeEvent.Lose>(events[0])
|
||||
assertEquals(222, events[0].cast<MemberHonorChangeEvent.Lose>().member.id)
|
||||
assertEquals(GroupHonorType.ACTIVE, events[1].cast<MemberHonorChangeEvent.Achieve>().honorType)
|
||||
assertEquals(333, events[1].cast<MemberHonorChangeEvent.Achieve>().member.id)
|
||||
assertIsInstance<MemberHonorChangeEvent.Achieve>(events[1])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
internal fun testGroupFileUpload() = runTest {
|
||||
val files = bot.addGroup(111, "aaa").files
|
||||
val file = files.uploadNewFile("aaa", "ccc".toByteArray().toExternalResource().toAutoCloseable())
|
||||
assertEquals("ccc", file.getUrl()!!.toUrl().readText())
|
||||
runAndReceiveEventBroadcast {
|
||||
bot.getGroup(111)!!.addMember(simpleMemberInfo(222, "bbb", permission = MemberPermission.ADMINISTRATOR))
|
||||
.says(file.toMessage())
|
||||
}.let { events ->
|
||||
assertTrue(events[0].cast<GroupMessageEvent>().message.contains(FileMessage))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
internal fun testAvatar() = runTest {
|
||||
assertNotEquals("", bot.addGroup(111, "aaa").avatarUrl.toUrl().readText())
|
||||
}
|
||||
|
||||
@Test
|
||||
internal fun testBotLeaveGroup() = runTest {
|
||||
runAndReceiveEventBroadcast {
|
||||
bot.addGroup(1, "A").quit()
|
||||
bot.addGroup(2, "B")
|
||||
.addMember(MockMemberInfoBuilder.create {
|
||||
uin(3).nick("W")
|
||||
permission(MemberPermission.ADMINISTRATOR)
|
||||
}).broadcastKickBot()
|
||||
// TODO: BotLeaveEvent.Disband
|
||||
}.let { events ->
|
||||
assertEquals(2, events.size)
|
||||
assertIsInstance<BotLeaveEvent.Active>(events[0]) {
|
||||
assertEquals(1, group.id)
|
||||
assertEquals("A", group.name)
|
||||
}
|
||||
assertIsInstance<BotLeaveEvent.Kick>(events[1]) {
|
||||
assertEquals(2, group.id)
|
||||
assertEquals("B", group.name)
|
||||
assertEquals(3, operator.id)
|
||||
assertEquals("W", operator.nick)
|
||||
}
|
||||
assertNull(bot.getGroup(1))
|
||||
assertNull(bot.getGroup(2))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBotGroupPermissionChangeEvent() = runTest {
|
||||
runAndReceiveEventBroadcast {
|
||||
bot.addGroup(1, "")
|
||||
.appendMember(MockMemberInfoBuilder.create {
|
||||
uin(1).nick("o")
|
||||
permission(MemberPermission.OWNER)
|
||||
})
|
||||
.botAsMember permissionChangesTo MemberPermission.ADMINISTRATOR
|
||||
|
||||
bot.addGroup(2, "")
|
||||
.appendMember(MockMemberInfoBuilder.create {
|
||||
uin(1).nick("o")
|
||||
permission(MemberPermission.OWNER)
|
||||
})
|
||||
.let {
|
||||
it.changeOwner(it.botAsMember)
|
||||
}
|
||||
}.let { events ->
|
||||
assertEquals(3, events.size)
|
||||
assertIsInstance<BotGroupPermissionChangeEvent>(events[0]) {
|
||||
assertEquals(MemberPermission.ADMINISTRATOR, new)
|
||||
assertEquals(MemberPermission.MEMBER, origin)
|
||||
}
|
||||
assertIsInstance<BotGroupPermissionChangeEvent>(events[1]) {
|
||||
assertEquals(MemberPermission.OWNER, new)
|
||||
assertEquals(MemberPermission.MEMBER, origin)
|
||||
}
|
||||
assertIsInstance<MemberPermissionChangeEvent>(events[2]) {
|
||||
assertEquals(1, member.id)
|
||||
assertEquals("o", member.nick)
|
||||
assertEquals(MemberPermission.MEMBER, new)
|
||||
assertEquals(MemberPermission.OWNER, origin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMuteEvent() = runTest {
|
||||
runAndReceiveEventBroadcast {
|
||||
val group = bot.addGroup(1, "")
|
||||
.appendMember(2, "")
|
||||
|
||||
group.botAsMember.let {
|
||||
it.broadcastMute(it, 2)
|
||||
assertTrue { it.isMuted }
|
||||
it.broadcastMute(it, 0)
|
||||
assertFalse { it.isMuted }
|
||||
it.broadcastMute(it, 5)
|
||||
assertTrue { group.isBotMuted }
|
||||
assertTrue { it.isMuted }
|
||||
}
|
||||
|
||||
group.getOrFail(2).let {
|
||||
it.broadcastMute(it, 2)
|
||||
assertTrue { it.isMuted }
|
||||
it.broadcastMute(it, 0)
|
||||
assertFalse { it.isMuted }
|
||||
it.broadcastMute(it, 5)
|
||||
assertTrue { it.isMuted }
|
||||
}
|
||||
}.let { events ->
|
||||
assertEquals(6, events.size)
|
||||
assertIsInstance<BotMuteEvent>(events[0])
|
||||
assertIsInstance<BotUnmuteEvent>(events[1])
|
||||
assertIsInstance<BotMuteEvent>(events[2])
|
||||
|
||||
assertIsInstance<MemberMuteEvent>(events[3])
|
||||
assertIsInstance<MemberUnmuteEvent>(events[4])
|
||||
assertIsInstance<MemberMuteEvent>(events[5])
|
||||
|
||||
delay(6000L)
|
||||
assertFalse { bot.getGroupOrFail(1).isBotMuted }
|
||||
assertFalse { bot.getGroupOrFail(1).getOrFail(2).isMuted }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGroupNameChangeEvent() = runTest {
|
||||
runAndReceiveEventBroadcast {
|
||||
val g = bot.addGroup(1, "").appendMember(7, "A")
|
||||
g.controlPane.groupName = "OOOOO"
|
||||
g.name = "Test"
|
||||
g.controlPane.withActor(g.getOrFail(7)).groupName = "Hi"
|
||||
}.let { events ->
|
||||
assertEquals(2, events.size)
|
||||
assertIsInstance<GroupNameChangeEvent>(events[0]) {
|
||||
assertEquals("OOOOO", origin)
|
||||
assertEquals("Test", new)
|
||||
assertEquals(1, group.id)
|
||||
assertNull(operator)
|
||||
}
|
||||
assertIsInstance<GroupNameChangeEvent>(events[1]) {
|
||||
assertEquals("Test", origin)
|
||||
assertEquals("Hi", new)
|
||||
assertEquals(1, group.id)
|
||||
assertEquals(7, operator!!.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGroupMuteAllEvent() = runTest {
|
||||
runAndReceiveEventBroadcast {
|
||||
val g = bot.addGroup(1, "").appendMember(7, "A")
|
||||
g.controlPane.isMuteAll = true
|
||||
g.settings.isMuteAll = false
|
||||
g.controlPane.withActor(g.getOrFail(7)).isMuteAll = true
|
||||
}.let { events ->
|
||||
assertEquals(2, events.size)
|
||||
assertIsInstance<GroupMuteAllEvent>(events[0]) {
|
||||
assertEquals(true, origin)
|
||||
assertEquals(false, new)
|
||||
assertEquals(1, group.id)
|
||||
assertNull(operator)
|
||||
}
|
||||
assertIsInstance<GroupMuteAllEvent>(events[1]) {
|
||||
assertEquals(false, origin)
|
||||
assertEquals(true, new)
|
||||
assertEquals(1, group.id)
|
||||
assertEquals(7, operator!!.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGroupAllowAnonymousChatEvent() = runTest {
|
||||
runAndReceiveEventBroadcast {
|
||||
val g = bot.addGroup(1, "").appendMember(7, "A")
|
||||
g.controlPane.isAnonymousChatAllowed = true
|
||||
g.settings.isAnonymousChatEnabled = false
|
||||
g.controlPane.withActor(g.getOrFail(7)).isAnonymousChatAllowed = true
|
||||
}.let { events ->
|
||||
assertEquals(2, events.size)
|
||||
assertIsInstance<GroupAllowAnonymousChatEvent>(events[0]) {
|
||||
assertEquals(true, origin)
|
||||
assertEquals(false, new)
|
||||
assertEquals(1, group.id)
|
||||
assertNull(operator)
|
||||
}
|
||||
assertIsInstance<GroupAllowAnonymousChatEvent>(events[1]) {
|
||||
assertEquals(false, origin)
|
||||
assertEquals(true, new)
|
||||
assertEquals(1, group.id)
|
||||
assertEquals(7, operator!!.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGroupAllowConfessTalkEvent() = runTest {
|
||||
runAndReceiveEventBroadcast {
|
||||
val g = bot.addGroup(1, "").appendMember(7, "A")
|
||||
g.controlPane.isAllowConfessTalk = true
|
||||
g.controlPane.withActor(g.botAsMember).isAllowConfessTalk = false
|
||||
g.controlPane.withActor(g.getOrFail(7)).isAllowConfessTalk = true
|
||||
}.let { events ->
|
||||
assertEquals(2, events.size)
|
||||
assertIsInstance<GroupAllowConfessTalkEvent>(events[0]) {
|
||||
assertEquals(true, origin)
|
||||
assertEquals(false, new)
|
||||
assertEquals(1, group.id)
|
||||
assertTrue(isByBot)
|
||||
}
|
||||
assertIsInstance<GroupAllowConfessTalkEvent>(events[1]) {
|
||||
assertEquals(false, origin)
|
||||
assertEquals(true, new)
|
||||
assertEquals(1, group.id)
|
||||
assertFalse(isByBot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGroupAllowMemberInviteEvent() = runTest {
|
||||
runAndReceiveEventBroadcast {
|
||||
val g = bot.addGroup(1, "").appendMember(7, "A")
|
||||
g.controlPane.isAllowMemberInvite = true
|
||||
g.settings.isAllowMemberInvite = false
|
||||
g.controlPane.withActor(g.getOrFail(7)).isAllowMemberInvite = true
|
||||
}.let { events ->
|
||||
assertEquals(2, events.size)
|
||||
assertIsInstance<GroupAllowMemberInviteEvent>(events[0]) {
|
||||
assertEquals(true, origin)
|
||||
assertEquals(false, new)
|
||||
assertEquals(1, group.id)
|
||||
assertNull(operator)
|
||||
}
|
||||
assertIsInstance<GroupAllowMemberInviteEvent>(events[1]) {
|
||||
assertEquals(false, origin)
|
||||
assertEquals(true, new)
|
||||
assertEquals(1, group.id)
|
||||
assertEquals(7, operator!!.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMemberCardChangeEvent() = runTest {
|
||||
runAndReceiveEventBroadcast {
|
||||
bot.addGroup(1, "")
|
||||
.addMember(MockMemberInfoBuilder.create {
|
||||
uin(2)
|
||||
nameCard("Hi")
|
||||
}).nameCardChangesTo("Hello")
|
||||
}.let { events ->
|
||||
assertEquals(1, events.size)
|
||||
assertIsInstance<MemberCardChangeEvent>(events[0]) {
|
||||
assertEquals("Hi", origin)
|
||||
assertEquals("Hello", new)
|
||||
assertEquals(2, member.id)
|
||||
assertEquals(1, member.group.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMemberSpecialTitleChangeEvent() = runTest {
|
||||
runAndReceiveEventBroadcast {
|
||||
bot.addGroup(1, "").addMember(2, "") specialTitleChangesTo "Hello"
|
||||
}.let { events ->
|
||||
assertEquals(1, events.size)
|
||||
assertIsInstance<MemberSpecialTitleChangeEvent>(events[0]) {
|
||||
assertEquals("", origin)
|
||||
assertEquals("Hello", new)
|
||||
assertEquals(2, member.id)
|
||||
assertEquals(1, member.group.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
24
mirai-core-mock/test/mock/MockMemberTest.kt
Normal file
24
mirai-core-mock/test/mock/MockMemberTest.kt
Normal file
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.test.mock
|
||||
|
||||
import net.mamoe.mirai.contact.MemberPermission
|
||||
import net.mamoe.mirai.mock.test.MockBotTestBase
|
||||
import net.mamoe.mirai.mock.utils.simpleMemberInfo
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertNotEquals
|
||||
|
||||
internal class MockMemberTest : MockBotTestBase() {
|
||||
@Test
|
||||
internal fun testAvatar() = runTest {
|
||||
val m = bot.addGroup(111, "aaa").addMember(simpleMemberInfo(222, "bbb", permission = MemberPermission.MEMBER))
|
||||
assertNotEquals("", m.avatarUrl)
|
||||
}
|
||||
}
|
34
mirai-core-mock/test/mock/MockStrangerTest.kt
Normal file
34
mirai-core-mock/test/mock/MockStrangerTest.kt
Normal file
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.test.mock
|
||||
|
||||
import net.mamoe.mirai.event.events.StrangerRelationChangeEvent
|
||||
import net.mamoe.mirai.mock.test.MockBotTestBase
|
||||
import net.mamoe.mirai.utils.cast
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotEquals
|
||||
|
||||
internal class MockStrangerTest : MockBotTestBase() {
|
||||
@Test
|
||||
internal fun testStrangerRelationChangeEvent() = runTest {
|
||||
runAndReceiveEventBroadcast {
|
||||
bot.addStranger(111, "aa").addAsFriend()
|
||||
bot.addStranger(222, "bb").delete()
|
||||
}.let { events ->
|
||||
assertEquals(2, events.size)
|
||||
assertIsInstance<StrangerRelationChangeEvent.Friended>(events[0])
|
||||
assertEquals(111, events[0].cast<StrangerRelationChangeEvent.Friended>().friend.id)
|
||||
assertIsInstance<StrangerRelationChangeEvent.Deleted>(events[1])
|
||||
assertEquals(222, events[1].cast<StrangerRelationChangeEvent.Deleted>().stranger.id)
|
||||
assertNotEquals("", bot.getFriend(111)!!.avatarUrl)
|
||||
}
|
||||
}
|
||||
}
|
10
mirai-core-mock/test/package.kt
Normal file
10
mirai-core-mock/test/package.kt
Normal file
@ -0,0 +1,10 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.mock.test
|
@ -156,6 +156,22 @@ public fun ByteArray.toInt(offset: Int = 0): Int =
|
||||
.plus(this[offset + 2].toInt().and(255).shl(8))
|
||||
.plus(this[offset + 3].toInt().and(255).shl(0))
|
||||
|
||||
/**
|
||||
* Converts 8 bytes to an Long in network order (big-endian).
|
||||
*/
|
||||
public fun ByteArray.toLong(): Long {
|
||||
var rsp: Long = 0
|
||||
rsp += this[0].toLong().and(255).shl(56)
|
||||
rsp += this[1].toLong().and(255).shl(48)
|
||||
rsp += this[2].toLong().and(255).shl(40)
|
||||
rsp += this[3].toLong().and(255).shl(32)
|
||||
rsp += this[4].toLong().and(255).shl(24)
|
||||
rsp += this[5].toLong().and(255).shl(16)
|
||||
rsp += this[6].toLong().and(255).shl(8)
|
||||
rsp += this[7].toLong().and(255).shl(0)
|
||||
return rsp
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// hexToBytes
|
||||
|
34
mirai-core-utils/src/jvmBaseMain/kotlin/IO.jvm.shared.kt
Normal file
34
mirai-core-utils/src/jvmBaseMain/kotlin/IO.jvm.shared.kt
Normal file
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2019-2022 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
@file:JvmMultifileClass
|
||||
@file:JvmName("MiraiUtils")
|
||||
@file:Suppress("NOTHING_TO_INLINE")
|
||||
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import java.io.InputStream
|
||||
|
||||
private fun dropContent0(stream: InputStream, buffer: ByteArray) {
|
||||
while (true) {
|
||||
val len = stream.read(buffer)
|
||||
if (len == -1) break
|
||||
}
|
||||
}
|
||||
|
||||
public fun InputStream.dropContent(
|
||||
buffer: Int = 2048,
|
||||
close: Boolean = false,
|
||||
) {
|
||||
if (close) {
|
||||
dropContent0(this, ByteArray(buffer))
|
||||
} else {
|
||||
this.use { dropContent0(it, ByteArray(buffer)) }
|
||||
}
|
||||
}
|
49
mirai-core-utils/src/jvmMain/kotlin/IO.jvm.kt
Normal file
49
mirai-core-utils/src/jvmMain/kotlin/IO.jvm.kt
Normal file
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2019-2021 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/dev/LICENSE
|
||||
*/
|
||||
|
||||
@file:JvmMultifileClass
|
||||
@file:JvmName("MiraiUtils")
|
||||
|
||||
@file:Suppress("NOTHING_TO_INLINE")
|
||||
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.deleteIfExists
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.isDirectory
|
||||
import kotlin.io.path.listDirectoryEntries
|
||||
|
||||
public val Path.isFile: Boolean get() = Files.exists(this) && !Files.isDirectory(this)
|
||||
|
||||
public inline fun Path.mkdir() {
|
||||
Files.createDirectory(this)
|
||||
}
|
||||
|
||||
public inline fun Path.mkdirs() {
|
||||
Files.createDirectories(this)
|
||||
}
|
||||
|
||||
public fun Path.mkParentDirs() {
|
||||
val current = parent ?: return
|
||||
if (current == this) return
|
||||
if (current.exists()) return
|
||||
current.mkParentDirs()
|
||||
current.mkdir()
|
||||
}
|
||||
|
||||
public fun Path.deleteRecursively(): Boolean {
|
||||
if (isFile) return deleteIfExists()
|
||||
if (isDirectory()) {
|
||||
listDirectoryEntries().forEach { it.deleteRecursively() }
|
||||
return deleteIfExists()
|
||||
}
|
||||
return false
|
||||
}
|
@ -58,6 +58,7 @@ fun includeConsoleProject(projectPath: String, dir: String? = null) =
|
||||
includeProject(":mirai-core-utils")
|
||||
includeProject(":mirai-core-api")
|
||||
includeProject(":mirai-core")
|
||||
includeProject(":mirai-core-mock")
|
||||
|
||||
includeProject(":mirai-core-all")
|
||||
includeProject(":mirai-bom")
|
||||
|
Loading…
Reference in New Issue
Block a user