Mock Testing Framework (#1521)

Co-authored-by: Eritque arcus <1930893235@qq.com>
Co-authored-by: Him188 <Him188@mamoe.net>
This commit is contained in:
微莹·纤绫 2022-09-10 12:49:13 +08:00 committed by GitHub
parent 2eb2c3acd0
commit 2db9804cf2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
85 changed files with 8018 additions and 0 deletions

View File

@ -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

View File

@ -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"},
]
},
],

View File

@ -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
View 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
View 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`

View 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 }

View 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)
}
}

View 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()
}
}

View 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

View 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))

View 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

View 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)
}

View 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
}

View 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()
}
}

View 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()
}
}

View 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
}

View 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
}

View 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)
}

View 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)
}

View 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

View 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()
}
}

View 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) })
}
}

View 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

View File

@ -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,
)

View 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])
}

View 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()
}
}

View 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)"
}
}

View 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) {
}
}

View File

@ -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()
}
}
}

View 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()
}
}

View File

@ -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)
}
}

View 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)"
}
}

View 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)"
}
}

View 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)"
}
}

View 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)"
}
}

View 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
}
}

View 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
*/
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
}

View File

@ -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
}
}

View File

@ -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()
}
}

View 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
}
}

View 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)
}
}
}
}
}

View 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(),
)
}

View File

@ -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
}
}

View File

@ -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
}
}

View 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
*/
@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
}

View File

@ -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
}
}
}

View 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()
}
}

View 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
}

View 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

View 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)
}
}
}

View 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>
}

View 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,
)

View 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)
}
}
}

View 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
}
}

View 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 }
}

View 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
}
}

View 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)
}
}

View 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)
}

View 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()
}

View 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()
}

View 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() }
}

View 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"
}

View 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()

View 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

View 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())
}
}

View 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()
}

View 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()
}
}

View 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())
}
}
}

View 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
}
}

View 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()
}
}

View 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() }
}
}

View 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
}
}
}

View 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)
}
}
}
}

View 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))
}
}

View 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)
}
}
}
}

View 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)
}
}

View 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)
}
}
}
}

View 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)
}
}
}
}

View 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)
}
}

View 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)
}
}
}

View 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

View File

@ -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

View 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)) }
}
}

View 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
}

View File

@ -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")