Merge remote-tracking branch 'origin/master'

This commit is contained in:
Him188 2020-02-24 10:26:44 +08:00
commit 577348ff32
10 changed files with 270 additions and 116 deletions

View File

@ -173,14 +173,16 @@ fun main() {
| ------------ | ------ | ----- | ----------- | -------------------------------- |
| sessionKey | String | false | YourSession | 已经激活的Session |
| target | Long | false | 987654321 | 发送消息目标好友的QQ号 |
| quote | Long | true | 135798642 | 引用一条消息的messageId进行回复 |
| messageChain | Array | false | [] | 消息链,是一个消息对象构成的数组 |
#### 响应: 返回统一状态码
#### 响应: 返回统一状态码并携带messageId
```json5
{
"code": 0,
"msg": "success"
"msg": "success",
"messageId": 1234567890 // 一个Long类型属性标识本条消息用于撤回和引用回复
}
```
@ -211,52 +213,16 @@ fun main() {
| ------------ | ------ | ----- | ----------- | -------------------------------- |
| sessionKey | String | false | YourSession | 已经激活的Session |
| target | Long | false | 987654321 | 发送消息目标群的群号 |
| quote | Long | true | 135798642 | 引用一条消息的messageId进行回复 |
| messageChain | Array | false | [] | 消息链,是一个消息对象构成的数组 |
#### 响应: 返回统一状态码
#### 响应: 返回统一状态码并携带messageId
```json5
{
"code": 0,
"msg": "success"
}
```
### 发送引用回复消息(仅支持群消息)
```
[POST] /sendQuoteMessage
```
使用此方法向指定的消息进行引用回复
#### 请求
```json5
{
"sessionKey": "YourSession",
"target": 987654321,
"messageChain": [
{ "type": "Plain", "text":"hello\n" },
{ "type": "Plain", "text":"world" }
]
}
```
| 名字 | 类型 | 可选 | 举例 | 说明 |
| ------------ | ------ | ----- | ----------- | -------------------------------- |
| sessionKey | String | false | YourSession | 已经激活的Session |
| target | Long | false | 987654321 | 引用消息的Message Source的Uid |
| messageChain | Array | false | [] | 消息链,是一个消息对象构成的数组 |
#### 响应: 返回统一状态码
```json5
{
"code": 0,
"msg": "success"
"msg": "success",
"messageId": 1234567890 // 一个Long类型属性标识本条消息用于撤回和引用回复
}
```
@ -331,6 +297,39 @@ Content-Typemultipart/form-data
### 撤回消息
```
[POST] /recall
```
使用此方法撤回指定消息。对于bot发送的消息又2分钟时间限制。对于撤回群聊中群员的消息需要有相应权限
#### 请求
```json5
{
"sessionKey": "YourSession",
"target": 987654321
}
```
| 名字 | 类型 | 可选 | 举例 | 说明 |
| ------------ | ------ | ----- | ----------- | -------------------------------- |
| sessionKey | String | false | YourSession | 已经激活的Session |
| target | Long | false | 987654321 | 需要撤回的消息的messageId |
#### 响应: 返回统一状态码
```json5
{
"code": 0,
"msg": "success"
}
```
### 获取Bot收到的消息和事件
```
@ -370,7 +369,10 @@ Content-Typemultipart/form-data
}
},{
"type": "FriendMessage", // 消息类型GroupMessage或FriendMessage或各类Event
"messageChain": [{ // 消息链,是一个消息对象构成的数组
"messageChain": [{ // 消息链,是一个消息对象构成的数组
"type": "Source",
"uid": 123456
},{
"type": "Plain",
"text": "Miral牛逼"
}],

View File

@ -12,7 +12,7 @@ import net.mamoe.mirai.utils.MiraiExperimentalAPI
sealed class BotEventDTO : EventDTO()
@UseExperimental(MiraiExperimentalAPI::class)
fun BotEvent.toDTO() = when(this) {
suspend fun BotEvent.toDTO() = when(this) {
is MessagePacket<*, *> -> toDTO()
else -> when(this) {
is BotOnlineEvent -> BotOnlineEventDTO(bot.uin)

View File

@ -17,8 +17,9 @@ import net.mamoe.mirai.message.FriendMessage
import net.mamoe.mirai.message.GroupMessage
import net.mamoe.mirai.message.MessagePacket
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.message.uploadImage
import net.mamoe.mirai.utils.MiraiInternalAPI
import java.net.URL
/*
* DTO data class
@ -57,7 +58,7 @@ data class PlainDTO(val text: String) : MessageDTO()
@Serializable
@SerialName("Image")
data class ImageDTO(val imageId: String) : MessageDTO()
data class ImageDTO(val imageId: String? = null, val url: String? = null) : MessageDTO()
@Serializable
@SerialName("Xml")
@ -84,42 +85,49 @@ sealed class MessageDTO : DTO
/*
Extend function
*/
fun MessagePacket<*, *>.toDTO() = when (this) {
suspend fun MessagePacket<*, *>.toDTO() = when (this) {
is FriendMessage -> FriendMessagePacketDTO(QQDTO(sender))
is GroupMessage -> GroupMessagePacketDTO(MemberDTO(sender))
else -> IgnoreEventDTO
}.apply {
if (this is MessagePacketDTO) { messageChain = message.toDTOChain() }
// else: `this` is bot event
if (this is MessagePacketDTO) {
// 将MessagePacket中的所有Message转为DTO对象并添加到messageChain
// foreachContent会忽略MessageSource一次主动获取
messageChain = mutableListOf(messageDTO(message[MessageSource])).apply {
message.foreachContent { content -> messageDTO(content).takeUnless { it == UnknownMessageDTO }?.let(::add) }
}
// else: `this` is bot event
}
}
fun MessageChain.toDTOChain() = mutableListOf(this[MessageSource].toDTO()).apply {
foreachContent { content -> content.toDTO().takeUnless { it == UnknownMessageDTO }?.let(::add) }
}
suspend fun MessageChainDTO.toMessageChain(contact: Contact) =
buildMessageChain { this@toMessageChain.forEach { it.toMessage(contact)?.let(::add) } }
fun MessageChainDTO.toMessageChain(contact: Contact) =
buildMessageChain { this@toMessageChain.forEach { add(it.toMessage(contact)) } }
@UseExperimental(ExperimentalUnsignedTypes::class, MiraiExperimentalAPI::class)
fun Message.toDTO() = when (this) {
is MessageSource -> MessageSourceDTO(id)
is At -> AtDTO(target, display)
@UseExperimental(ExperimentalUnsignedTypes::class)
suspend fun MessagePacket<*, *>.messageDTO(message: Message) = when (message) {
is MessageSource -> MessageSourceDTO(message.id)
is At -> AtDTO(message.target, message.display)
is AtAll -> AtAllDTO(0L)
is Face -> FaceDTO(id)
is PlainText -> PlainDTO(stringValue)
is Image -> ImageDTO(imageId)
is XMLMessage -> XmlDTO(stringValue)
is Face -> FaceDTO(message.id)
is PlainText -> PlainDTO(message.stringValue)
is Image -> ImageDTO(message.imageId, message.url())
is XMLMessage -> XmlDTO(message.stringValue)
else -> UnknownMessageDTO
}
@UseExperimental(ExperimentalUnsignedTypes::class, MiraiInternalAPI::class, MiraiExperimentalAPI::class)
fun MessageDTO.toMessage(contact: Contact) = when (this) {
@UseExperimental(ExperimentalUnsignedTypes::class, MiraiInternalAPI::class)
suspend fun MessageDTO.toMessage(contact: Contact) = when (this) {
is AtDTO -> At((contact as Group)[target])
is AtAllDTO -> AtAll
is FaceDTO -> Face(faceId)
is PlainDTO -> PlainText(text)
is ImageDTO -> Image(imageId)
is ImageDTO -> when {
!imageId.isNullOrBlank() -> Image(imageId)
!url.isNullOrBlank() -> contact.uploadImage(URL(url))
else -> null
}
is XmlDTO -> XMLMessage(xml)
is MessageSourceDTO, is UnknownMessageDTO -> PlainText("assert cannot reach")
is MessageSourceDTO, is UnknownMessageDTO -> null
}

View File

@ -13,17 +13,17 @@ import net.mamoe.mirai.api.http.data.common.EventDTO
import net.mamoe.mirai.api.http.data.common.IgnoreEventDTO
import net.mamoe.mirai.api.http.data.common.toDTO
import net.mamoe.mirai.event.events.BotEvent
import net.mamoe.mirai.message.GroupMessage
import net.mamoe.mirai.message.MessagePacket
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.utils.firstKey
import java.util.concurrent.ConcurrentLinkedDeque
class MessageQueue : ConcurrentLinkedDeque<BotEvent>() {
val quoteCacheSize = 4096
val quoteCache = LinkedHashMap<Long, GroupMessage>()
val cacheSize = 4096
val cache = LinkedHashMap<Long, MessagePacket<*, *>>()
fun fetch(size: Int): List<EventDTO> {
suspend fun fetch(size: Int): List<EventDTO> {
var count = size
val ret = ArrayList<EventDTO>(count)
@ -37,18 +37,20 @@ class MessageQueue : ConcurrentLinkedDeque<BotEvent>() {
}
}
// TODO: 等FriendMessage支持quote
if (event is GroupMessage) {
if (event is MessagePacket<*, *>) {
addQuoteCache(event)
}
}
return ret
}
private fun addQuoteCache(msg: GroupMessage) {
quoteCache[msg.message[MessageSource].id] = msg
if (quoteCache.size > quoteCacheSize) {
quoteCache.remove(quoteCache.firstKey())
fun cache(messageId: Long) =
cache[messageId] ?: throw NoSuchElementException()
fun addQuoteCache(msg: MessagePacket<*, *>) {
cache[msg.message[MessageSource].id] = msg
if (cache.size > cacheSize) {
cache.remove(cache.firstKey())
}
}
}

View File

@ -20,15 +20,19 @@ import io.ktor.response.respondText
import io.ktor.routing.post
import io.ktor.routing.routing
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import net.mamoe.mirai.api.http.AuthedSession
import net.mamoe.mirai.api.http.SessionManager
import net.mamoe.mirai.api.http.data.*
import net.mamoe.mirai.api.http.data.common.DTO
import net.mamoe.mirai.api.http.data.common.MessageChainDTO
import net.mamoe.mirai.api.http.data.common.VerifyDTO
import net.mamoe.mirai.api.http.data.common.toMessageChain
import net.mamoe.mirai.api.http.util.toJson
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.message.FriendMessage
import net.mamoe.mirai.message.GroupMessage
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.message.uploadImage
import java.net.URL
@ -42,23 +46,47 @@ fun Application.messageModule() {
call.respondJson(fetch.toJson())
}
suspend fun <C : Contact> sendMessage(
quote: QuoteReplyToSend?,
messageChain: MessageChain,
target: C
): MessageReceipt<out Contact> {
val send = if (quote == null) {
messageChain
} else {
(quote + messageChain).toChain()
}
return target.sendMessage(send)
}
miraiVerify<SendDTO>("/sendFriendMessage") {
val quote = it.quote?.let { q ->
it.session.messageQueue.cache(q).run {
this[MessageSource].quote(sender)
}}
it.session.bot.getFriend(it.target).apply {
sendMessage(it.messageChain.toMessageChain(this)) // this aka QQ
val receipt = sendMessage(quote, it.messageChain.toMessageChain(this), this)
receipt.source.ensureSequenceIdAvailable()
it.session.messageQueue.addQuoteCache(FriendMessage(bot.selfQQ, receipt.source.toChain()))
call.respondDTO(SendRetDTO(messageId = receipt.source.id))
}
}
miraiVerify<SendDTO>("/sendGroupMessage") {
it.session.bot.getGroup(it.target).apply {
sendMessage(it.messageChain.toMessageChain(this)) // this aka Group
}
}
val quote = it.quote?.let { q ->
it.session.messageQueue.cache(q).run {
this[MessageSource].quote(sender)
}}
miraiVerify<SendDTO>("/sendQuoteMessage") {
it.session.messageQueue.quoteCache[it.target]?.apply {
quoteReply(it.messageChain.toMessageChain(group))
} ?: throw NoSuchElementException()
call.respondStateCode(StateCode.Success)
it.session.bot.getGroup(it.target).apply {
val receipt = sendMessage(quote, it.messageChain.toMessageChain(this), this)
receipt.source.ensureSequenceIdAvailable()
it.session.messageQueue.addQuoteCache(GroupMessage("", botPermission, botAsMember, receipt.source.toChain()))
call.respondDTO(SendRetDTO(messageId = receipt.source.id))
}
}
miraiVerify<SendImageDTO>("sendImageMessage") {
@ -101,8 +129,10 @@ fun Application.messageModule() {
}
miraiVerify<RecallDTO>("recall") {
// TODO
call.respond(HttpStatusCode.NotFound, "未完成")
it.session.messageQueue.cache(it.target).apply {
it.session.bot.recall(get(MessageSource))
}
call.respondStateCode(StateCode.Success)
}
}
}
@ -110,6 +140,7 @@ fun Application.messageModule() {
@Serializable
private data class SendDTO(
override val sessionKey: String,
val quote: Long? = null,
val target: Long,
val messageChain: MessageChainDTO
) : VerifyDTO()
@ -125,13 +156,13 @@ private data class SendImageDTO(
@Serializable
private class SendRetDTO(
val messageId: Long,
@Transient val stateCode: StateCode = Success
) : StateCode(stateCode.code, stateCode.msg)
val code: Int = 0,
val msg: String = "success",
val messageId: Long
) : DTO
@Serializable
private data class RecallDTO(
override val sessionKey: String,
val target: Long,
val sender: Long
val target: Long
) : VerifyDTO()

View File

@ -17,8 +17,11 @@ import com.moandjiezana.toml.Toml
import com.moandjiezana.toml.TomlWriter
import kotlinx.serialization.Serializable
import kotlinx.serialization.UnstableDefault
import net.mamoe.mirai.utils.io.encodeToString
import org.yaml.snakeyaml.Yaml
import tornadofx.c
import java.io.File
import java.io.InputStream
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import kotlin.collections.LinkedHashMap
@ -69,6 +72,9 @@ interface Config {
)
}
/**
* create a read-write config
* */
fun load(file: File): Config {
if (!file.exists()) {
file.createNewFile()
@ -86,6 +92,32 @@ interface Config {
else -> error("Unsupported file config type ${file.extension.toLowerCase()}")
}
}
/**
* create a read-only config
*/
fun load(content: String, type: String): Config {
return when (type.toLowerCase()) {
"json" -> JsonConfig(content)
"yml" -> YamlConfig(content)
"yaml" -> YamlConfig(content)
"mirai" -> YamlConfig(content)
"ini" -> TomlConfig(content)
"toml" -> TomlConfig(content)
"properties" -> TomlConfig(content)
"property" -> TomlConfig(content)
"data" -> TomlConfig(content)
else -> error("Unsupported file config type $content")
}
}
/**
* create a read-only config
*/
fun load(inputStream: InputStream, type: String): Config {
return load(inputStream.readBytes().encodeToString(), type)
}
}
}
@ -363,14 +395,23 @@ interface FileConfig : Config {
abstract class FileConfigImpl internal constructor(
private val file: File
private val rawContent: String
) : FileConfig,
ConfigSection {
private val content by lazy {
deserialize(file.readText())
internal var file: File? = null
constructor(file: File) : this(file.readText()) {
this.file = file
}
private val content by lazy {
deserialize(rawContent)
}
override val size: Int get() = content.size
override val entries: MutableSet<MutableMap.MutableEntry<String, Any>> get() = content.entries
override val keys: MutableSet<String> get() = content.keys
@ -384,12 +425,17 @@ abstract class FileConfigImpl internal constructor(
override fun remove(key: String): Any? = content.remove(key)
override fun save() {
if (!file.exists()) {
file.createNewFile()
if (isReadOnly()) {
error("Config is readonly")
}
file.writeText(serialize(content))
if (!((file?.exists())!!)) {
file?.createNewFile()
}
file?.writeText(serialize(content))
}
fun isReadOnly() = file == null
override fun contains(key: String): Boolean {
return content.contains(key)
}
@ -409,8 +455,12 @@ abstract class FileConfigImpl internal constructor(
}
class JsonConfig internal constructor(
file: File
) : FileConfigImpl(file) {
content: String
) : FileConfigImpl(content) {
constructor(file: File) : this(file.readText()) {
this.file = file
}
@UnstableDefault
override fun deserialize(content: String): ConfigSection {
if (content.isEmpty() || content.isBlank() || content == "{}") {
@ -429,7 +479,11 @@ class JsonConfig internal constructor(
}
}
class YamlConfig internal constructor(file: File) : FileConfigImpl(file) {
class YamlConfig internal constructor(content: String) : FileConfigImpl(content) {
constructor(file: File) : this(file.readText()) {
this.file = file
}
override fun deserialize(content: String): ConfigSection {
if (content.isEmpty() || content.isBlank()) {
return ConfigSectionImpl()
@ -447,7 +501,11 @@ class YamlConfig internal constructor(file: File) : FileConfigImpl(file) {
}
class TomlConfig internal constructor(file: File) : FileConfigImpl(file) {
class TomlConfig internal constructor(content: String) : FileConfigImpl(content) {
constructor(file: File) : this(file.readText()) {
this.file = file
}
override fun deserialize(content: String): ConfigSection {
if (content.isEmpty() || content.isBlank()) {
return ConfigSectionImpl()

View File

@ -17,6 +17,7 @@ import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.SimpleLogger
import net.mamoe.mirai.utils.io.encodeToString
import java.io.File
import java.io.InputStream
import java.net.URL
import java.net.URLClassLoader
import java.util.jar.JarFile
@ -91,6 +92,13 @@ abstract class PluginBase(coroutineContext: CoroutineContext) : CoroutineScope {
val logger: MiraiLogger by lazy {
DefaultLogger(pluginDescription.name)
}
fun getResources(fileName: String): InputStream? {
return PluginManager.getFileInJarByName(
this.pluginDescription.name,
fileName
)
}
}
class PluginDescription(
@ -325,6 +333,47 @@ object PluginManager {
it.disable(throwable)
}
}
/**
* 根据插件名字找Jar的文件
* null => 没找到
*/
fun getJarPath(pluginName: String): File? {
File(pluginsPath).listFiles()?.forEach { file ->
if (file != null && file.extension == "jar") {
val jar = JarFile(file)
val pluginYml =
jar.entries().asSequence().filter { it.name.toLowerCase().contains("plugin.yml") }.firstOrNull()
if (pluginYml != null) {
val description =
PluginDescription.readFromContent(
URL("jar:file:" + file.absoluteFile + "!/" + pluginYml.name).openConnection().inputStream.use {
it.readBytes().encodeToString()
})
if (description.name.toLowerCase() == pluginName.toLowerCase()) {
return file
}
}
}
}
return null
}
/**
* 根据插件名字找Jar resources中的文件
* null => 没找到
*/
fun getFileInJarByName(pluginName: String, toFind: String): InputStream? {
val jarFile = getJarPath(pluginName)
if (jarFile == null) {
return null
}
val jar = JarFile(jarFile)
val toFindFile =
jar.entries().asSequence().filter { it.name == toFind }.firstOrNull() ?: return null
return URL("jar:file:" + jarFile.absoluteFile + "!/" + toFindFile.name).openConnection().inputStream
}
}

View File

@ -31,7 +31,7 @@ import net.mamoe.mirai.utils.unsafeWeakRef
* @see QQ.sendMessage 发送群消息, 返回回执此对象
*/
open class MessageReceipt<C : Contact>(
private val source: MessageSource,
val source: MessageSource,
target: C
) {
init {

View File

@ -161,7 +161,7 @@ internal class LockFreeLinkedListTest {
println("Check value")
value shouldBeEqualTo 6
println("Check size")
println(list.getLinkStructure())
// println(list.getLinkStructure())
list.size shouldBeEqualTo 6
}
@ -174,7 +174,7 @@ internal class LockFreeLinkedListTest {
println("Check value")
value shouldBeEqualTo 2
println("Check size")
println(list.getLinkStructure())
// println(list.getLinkStructure())
list.size shouldBeEqualTo 5
}
@ -198,7 +198,7 @@ internal class LockFreeLinkedListTest {
println("Check value")
value shouldBeEqualTo 2
println("Check size")
println(list.getLinkStructure())
// println(list.getLinkStructure())
list.size shouldBeEqualTo 1
}
/*

View File

@ -12,17 +12,17 @@ package net.mamoe.mirai.imageplugin
import kotlinx.coroutines.*
import net.mamoe.mirai.console.plugins.Config
import net.mamoe.mirai.console.plugins.ConfigSection
import net.mamoe.mirai.console.plugins.PluginBase
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.event.events.BotOnlineEvent
import net.mamoe.mirai.event.subscribeAlways
import net.mamoe.mirai.event.subscribeMessages
import net.mamoe.mirai.console.plugins.PluginBase
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.uploadAsImage
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import org.jsoup.Jsoup
import java.io.File
import kotlin.random.Random
import java.net.URL
class ImageSenderMain : PluginBase() {
@ -60,7 +60,6 @@ class ImageSenderMain : PluginBase() {
reply(e.message ?: "unknown error")
}
}
}
}
}
@ -84,16 +83,21 @@ class ImageSenderMain : PluginBase() {
override fun onLoad() {
logger.info("loading local image data")
try {
images = Config.load(this.javaClass.classLoader.getResource("data.yml")!!.path!!)
images = Config.load(getResources(fileName = "data.yml")!!, "yml")
} catch (e: Exception) {
e.printStackTrace()
logger.info("无法加载本地图片")
}
logger.info("本地图片版本" + images.getString("version"))
logger.info("Normal * " + images.getList("normal").size)
logger.info("R18 * " + images.getList("R18").size)
r18 = images.getConfigSectionList("R18")
normal = images.getConfigSectionList("normal")
logger.info("Normal * " + normal.size)
logger.info("R18 * " + r18.size)
}
override fun onDisable() {
}