Bläddra i källkod

redirect from old resp

jiahua.liu 6 år sedan
förälder
incheckning
be31baf4c2

+ 65 - 0
src/main/kotlin/net/mamoe/mirai/api/http/MiraiHttpAPIServer.kt

@@ -0,0 +1,65 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.api.http
+
+import io.ktor.application.Application
+import io.ktor.server.cio.CIO
+import io.ktor.server.engine.applicationEngineEnvironment
+import io.ktor.server.engine.connector
+import io.ktor.server.engine.embeddedServer
+import io.ktor.util.KtorExperimentalAPI
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import net.mamoe.mirai.api.http.route.mirai
+import net.mamoe.mirai.utils.DefaultLogger
+import org.slf4j.helpers.NOPLoggerFactory
+import kotlin.coroutines.CoroutineContext
+
+object MiraiHttpAPIServer : CoroutineScope {
+
+    var logger = DefaultLogger("Mirai HTTP API")
+    override val coroutineContext: CoroutineContext =
+        CoroutineExceptionHandler { _, throwable -> logger.error(throwable) }
+
+    init {
+        SessionManager.authKey = generateSessionKey()//用于验证的key, 使用和SessionKey相同的方法生成, 但意义不同
+    }
+
+    fun setAuthKey(key: String) {
+        SessionManager.authKey = key
+    }
+
+    @UseExperimental(KtorExperimentalAPI::class)
+    fun start(
+        port: Int = 8080,
+        authKey: String,
+        callback: (() -> Unit)? = null
+    ) {
+        require(authKey.length in 8..128) { "Expected authKey length is between 8 to 128" }
+        SessionManager.authKey = authKey
+
+        // TODO: start是无阻塞的,理应获取启动状态后再执行后续代码
+        launch {
+            embeddedServer(CIO, environment = applicationEngineEnvironment {
+                this.parentCoroutineContext = coroutineContext
+                this.log = NOPLoggerFactory().getLogger("NMYSL")
+                this.module(Application::mirai)
+
+                connector {
+                    this.port = port
+                }
+            }).start(wait = true)
+        }
+
+        logger.info("Http api server is running with authKey: ${SessionManager.authKey}")
+        callback?.invoke()
+    }
+}

+ 118 - 0
src/main/kotlin/net/mamoe/mirai/api/http/Session.kt

@@ -0,0 +1,118 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.api.http
+
+import kotlinx.coroutines.*
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.api.http.queue.MessageQueue
+import net.mamoe.mirai.event.Listener
+import net.mamoe.mirai.event.events.BotEvent
+import net.mamoe.mirai.event.subscribeAlways
+import net.mamoe.mirai.event.subscribeMessages
+import net.mamoe.mirai.message.MessagePacket
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+
+tailrec fun generateSessionKey(): String {
+    fun generateRandomSessionKey(): String {
+        val all = "QWERTYUIOPASDFGHJKLZXCVBNM1234567890qwertyuiopasdfghjklzxcvbnm"
+        return buildString(capacity = 8) {
+            repeat(8) {
+                append(all.random())
+            }
+        }
+    }
+
+    val key = generateRandomSessionKey()
+    if (!SessionManager.allSession.containsKey(key)) {
+        return key
+    }
+
+    return generateSessionKey()
+}
+
+internal object SessionManager {
+
+    val allSession: MutableMap<String, Session> = mutableMapOf()
+
+    lateinit var authKey: String
+
+
+    fun createTempSession(): TempSession = TempSession(EmptyCoroutineContext).also { newTempSession ->
+        allSession[newTempSession.key] = newTempSession
+        //设置180000ms后检测并回收
+        newTempSession.launch {
+            delay(180000)
+            allSession[newTempSession.key]?.run {
+                if (this is TempSession)
+                    closeSession(newTempSession.key)
+            }
+        }
+    }
+
+    operator fun get(sessionKey: String) = allSession[sessionKey]
+
+    fun containSession(sessionKey: String): Boolean = allSession.containsKey(sessionKey)
+
+    fun closeSession(sessionKey: String) = allSession.remove(sessionKey)?.also { it.close() }
+
+    fun closeSession(session: Session) = closeSession(session.key)
+
+}
+
+
+/**
+ * @author NaturalHG
+ * 这个用于管理不同Client与Mirai HTTP的会话
+ *
+ * [Session]均为内部操作用类
+ * 需使用[SessionManager]
+ */
+abstract class Session internal constructor(
+    coroutineContext: CoroutineContext
+) : CoroutineScope {
+    val supervisorJob = SupervisorJob(coroutineContext[Job])
+    final override val coroutineContext: CoroutineContext = supervisorJob + coroutineContext
+
+    val key: String = generateSessionKey()
+
+
+    internal open fun close() {
+        supervisorJob.complete()
+    }
+}
+
+
+/**
+ * 任何新链接建立后分配一个[TempSession]
+ *
+ * TempSession在建立180s内没有转变为[AuthedSession]应被清除
+ */
+class TempSession internal constructor(coroutineContext: CoroutineContext) : Session(coroutineContext)
+
+/**
+ * 任何[TempSession]认证后转化为一个[AuthedSession]
+ * 在这一步[AuthedSession]应该已经有assigned的bot
+ */
+class AuthedSession internal constructor(val bot: Bot, coroutineContext: CoroutineContext) : Session(coroutineContext) {
+
+    val messageQueue = MessageQueue()
+    private val _listener: Listener<BotEvent>
+
+    init {
+        _listener = bot.subscribeAlways{ this.run(messageQueue::add) }
+    }
+
+    override fun close() {
+        _listener.complete()
+        super.close()
+    }
+}
+

+ 33 - 0
src/main/kotlin/net/mamoe/mirai/api/http/data/Exception.kt

@@ -0,0 +1,33 @@
+package net.mamoe.mirai.api.http.data
+
+/**
+ * 错误请求. 抛出这个异常后将会返回错误给一个请求
+ */
+@Suppress("unused")
+open class IllegalAccessException : Exception {
+    override val message: String get() = super.message!!
+
+    constructor(message: String) : super(message, null)
+    constructor(cause: Throwable) : super(cause.toString(), cause)
+    constructor(message: String, cause: Throwable?) : super(message, cause)
+}
+
+/**
+ * Session失效或不存在
+ */
+object IllegalSessionException : IllegalAccessException("Session失效或不存在")
+
+/**
+ * Session未激活
+ */
+object NotVerifiedSessionException : IllegalAccessException("Session未激活")
+
+/**
+ * 指定Bot不存在
+ */
+object NoSuchBotException: IllegalAccessException("指定Bot不存在")
+
+/**
+ * 错误参数
+ */
+class IllegalParamException(message: String) : IllegalAccessException(message)

+ 22 - 0
src/main/kotlin/net/mamoe/mirai/api/http/data/StateCode.kt

@@ -0,0 +1,22 @@
+package net.mamoe.mirai.api.http.data
+
+import kotlinx.serialization.Serializable
+import net.mamoe.mirai.api.http.data.common.DTO
+
+@Serializable
+open class StateCode(val code: Int, var msg: String) : DTO {
+    object Success : StateCode(0, "success") // 成功
+    object NoBot : StateCode(2, "指定Bot不存在")
+    object IllegalSession : StateCode(3, "Session失效或不存在")
+    object NotVerifySession : StateCode(4, "Session未认证")
+    object NoElement : StateCode(5, "指定对象不存在")
+    object PermissionDenied : StateCode(10, "无操作权限")
+
+    // KS bug: 主构造器中不能有非字段参数 https://github.com/Kotlin/kotlinx.serialization/issues/575
+    @Serializable
+    class IllegalAccess() : StateCode(400, "") { // 非法访问
+        constructor(msg: String) : this() {
+            this.msg = msg
+        }
+    }
+}

+ 118 - 0
src/main/kotlin/net/mamoe/mirai/api/http/data/common/BotEventDTO.kt

@@ -0,0 +1,118 @@
+package net.mamoe.mirai.api.http.data.common
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import net.mamoe.mirai.contact.MemberPermission
+import net.mamoe.mirai.event.events.BotEvent
+import net.mamoe.mirai.event.events.*
+import net.mamoe.mirai.message.MessagePacket
+import net.mamoe.mirai.utils.MiraiExperimentalAPI
+
+@Serializable
+sealed class BotEventDTO : EventDTO()
+
+@UseExperimental(MiraiExperimentalAPI::class)
+suspend fun BotEvent.toDTO() = when(this) {
+    is MessagePacket<*, *> -> toDTO()
+    else -> when(this) {
+        is BotOnlineEvent -> BotOnlineEventDTO(bot.uin)
+        is BotOfflineEvent.Active -> BotOfflineEventActiveDTO(bot.uin)
+        is BotOfflineEvent.Force -> BotOfflineEventForceDTO(bot.uin, title, message)
+        is BotOfflineEvent.Dropped -> BotOfflineEventDroppedDTO(bot.uin)
+        is BotReloginEvent -> BotReloginEventDTO(bot.uin)
+//        is MessageSendEvent.GroupMessageSendEvent -> {}
+//        is MessageSendEvent.FriendMessageSendEvent -> {}
+//        is BeforeImageUploadEvent -> {}
+//        is ImageUploadEvent.Succeed -> {}
+        is BotGroupPermissionChangeEvent -> BotGroupPermissionChangeEventDTO(origin, new, GroupDTO(group))
+        is BotMuteEvent -> BotMuteEventDTO(durationSeconds, MemberDTO(operator))
+        is BotUnmuteEvent -> BotUnmuteEventDTO(MemberDTO(operator))
+        is BotJoinGroupEvent -> BotJoinGroupEventDTO(GroupDTO(group))
+//        is GroupSettingChangeEvent<*> -> {} // 不知道会改什么
+        is GroupNameChangeEvent -> GroupNameChangeEventDTO(origin, new, GroupDTO(group), isByBot)
+        is GroupEntranceAnnouncementChangeEvent -> GroupEntranceAnnouncementChangeEventDTO(origin, new, GroupDTO(group), operator?.let(::MemberDTO))
+        is GroupMuteAllEvent -> GroupMuteAllEventDTO(origin, new, GroupDTO(group), operator?.let(::MemberDTO))
+        is GroupAllowAnonymousChatEvent -> GroupAllowAnonymousChatEventDTO(origin, new, GroupDTO(group), operator?.let(::MemberDTO))
+        is GroupAllowConfessTalkEvent -> GroupAllowConfessTalkEventDTO(origin, new, GroupDTO(group), isByBot)
+        is GroupAllowMemberInviteEvent -> GroupAllowMemberInviteEventDTO(origin, new, GroupDTO(group), operator?.let(::MemberDTO))
+        is MemberJoinEvent -> MemberJoinEventDTO(MemberDTO(member))
+        is MemberLeaveEvent.Kick -> MemberLeaveEventKickDTO(MemberDTO(member), operator?.let(::MemberDTO))
+        is MemberLeaveEvent.Quit -> MemberLeaveEventQuitDTO(MemberDTO(member))
+        is MemberCardChangeEvent -> MemberCardChangeEventDTO(origin, new, MemberDTO(member), operator?.let(::MemberDTO))
+        is MemberSpecialTitleChangeEvent -> MemberSpecialTitleChangeEventDTO(origin, new, MemberDTO(member))
+        is MemberPermissionChangeEvent -> MemberPermissionChangeEventDTO(origin, new, MemberDTO(member))
+        is MemberMuteEvent -> MemberMuteEventDTO(durationSeconds, MemberDTO(member), operator?.let(::MemberDTO))
+        is MemberUnmuteEvent -> MemberUnmuteEventDTO(MemberDTO(member), operator?.let(::MemberDTO))
+        else -> IgnoreEventDTO
+    }
+}
+
+@Serializable
+@SerialName("BotOnlineEvent")
+data class BotOnlineEventDTO(val qq: Long) : BotEventDTO()
+@Serializable
+@SerialName("BotOfflineEventActive")
+data class BotOfflineEventActiveDTO(val qq: Long) : BotEventDTO()
+@Serializable
+@SerialName("BotOfflineEventForce")
+data class BotOfflineEventForceDTO(val qq: Long, val title: String, val message: String) : BotEventDTO()
+@Serializable
+@SerialName("BotOfflineEventDropped")
+data class BotOfflineEventDroppedDTO(val qq: Long) : BotEventDTO()
+@Serializable
+@SerialName("BotReloginEvent")
+data class BotReloginEventDTO(val qq: Long) : BotEventDTO()
+@Serializable
+@SerialName("BotGroupPermissionChangeEvent")
+data class BotGroupPermissionChangeEventDTO(val origin: MemberPermission, val new: MemberPermission, val group: GroupDTO) : BotEventDTO()
+@Serializable
+@SerialName("BotMuteEvent")
+data class BotMuteEventDTO(val durationSeconds: Int, val operator: MemberDTO) : BotEventDTO()
+@Serializable
+@SerialName("BotUnmuteEvent")
+data class BotUnmuteEventDTO(val operator: MemberDTO) : BotEventDTO()
+@Serializable
+@SerialName("BotJoinGroupEvent")
+data class BotJoinGroupEventDTO(val group: GroupDTO) : BotEventDTO()
+@Serializable
+@SerialName("GroupNameChangeEvent")
+data class GroupNameChangeEventDTO(val origin: String, val new: String, val group: GroupDTO, val isByBot: Boolean) : BotEventDTO()
+@Serializable
+@SerialName("GroupEntranceAnnouncementChangeEvent")
+data class GroupEntranceAnnouncementChangeEventDTO(val origin: String, val new: String, val group: GroupDTO, val operator: MemberDTO?) : BotEventDTO()
+@Serializable
+@SerialName("GroupMuteAllEvent")
+data class GroupMuteAllEventDTO(val origin: Boolean, val new: Boolean, val group: GroupDTO, val operator: MemberDTO?) : BotEventDTO()
+@Serializable
+@SerialName("GroupAllowAnonymousChatEvent")
+data class GroupAllowAnonymousChatEventDTO(val origin: Boolean, val new: Boolean, val group: GroupDTO, val operator: MemberDTO?) : BotEventDTO()
+@Serializable
+@SerialName("GroupAllowConfessTalkEvent")
+data class GroupAllowConfessTalkEventDTO(val origin: Boolean, val new: Boolean, val group: GroupDTO, val isByBot: Boolean) : BotEventDTO()
+@Serializable
+@SerialName("GroupAllowMemberInviteEvent")
+data class GroupAllowMemberInviteEventDTO(val origin: Boolean, val new: Boolean, val group: GroupDTO, val operator: MemberDTO?) : BotEventDTO()
+@Serializable
+@SerialName("MemberJoinEvent")
+data class MemberJoinEventDTO(val member: MemberDTO) : BotEventDTO()
+@Serializable
+@SerialName("MemberLeaveEventKick")
+data class MemberLeaveEventKickDTO(val member: MemberDTO, val operator: MemberDTO?) : BotEventDTO()
+@Serializable
+@SerialName("MemberLeaveEventQuit")
+data class MemberLeaveEventQuitDTO(val member: MemberDTO) : BotEventDTO()
+@Serializable
+@SerialName("MemberCardChangeEvent")
+data class MemberCardChangeEventDTO(val origin: String, val new: String, val member: MemberDTO, val operator: MemberDTO?) : BotEventDTO()
+@Serializable
+@SerialName("MemberSpecialTitleChangeEvent")
+data class MemberSpecialTitleChangeEventDTO(val origin: String, val new: String, val member: MemberDTO) : BotEventDTO()
+@Serializable
+@SerialName("MemberPermissionChangeEvent")
+data class MemberPermissionChangeEventDTO(val origin: MemberPermission, val new: MemberPermission, val member: MemberDTO) : BotEventDTO()
+@Serializable
+@SerialName("MemberMuteEvent")
+data class MemberMuteEventDTO(val durationSeconds: Int, val member: MemberDTO, val operator: MemberDTO?) : BotEventDTO()
+@Serializable
+@SerialName("MemberUnmuteEvent")
+data class MemberUnmuteEventDTO(val member: MemberDTO, val operator: MemberDTO?) : BotEventDTO()

+ 53 - 0
src/main/kotlin/net/mamoe/mirai/api/http/data/common/ContactDTO.kt

@@ -0,0 +1,53 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.api.http.data.common
+
+import kotlinx.serialization.Serializable
+import net.mamoe.mirai.contact.*
+import net.mamoe.mirai.utils.MiraiExperimentalAPI
+
+@Serializable
+abstract class ContactDTO : DTO {
+    abstract val id: Long
+}
+
+@Serializable
+data class QQDTO(
+    override val id: Long,
+    val nickName: String,
+    val remark: String
+) : ContactDTO() {
+    // TODO: queryProfile.nickname & queryRemark.value not support now
+    constructor(qq: QQ) : this(qq.id, "", "")
+}
+
+
+@Serializable
+data class MemberDTO(
+    override val id: Long,
+    val memberName: String,
+    val permission: MemberPermission,
+    val group: GroupDTO
+) : ContactDTO() {
+    constructor(member: Member) : this(
+        member.id, member.nameCardOrNick, member.permission,
+        GroupDTO(member.group)
+    )
+}
+
+@Serializable
+data class GroupDTO(
+    override val id: Long,
+    val name: String,
+    val permission: MemberPermission
+) : ContactDTO() {
+    @UseExperimental(MiraiExperimentalAPI::class)
+    constructor(group: Group) : this(group.id, group.name, group.botPermission)
+}

+ 22 - 0
src/main/kotlin/net/mamoe/mirai/api/http/data/common/DTO.kt

@@ -0,0 +1,22 @@
+package net.mamoe.mirai.api.http.data.common
+
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+import net.mamoe.mirai.api.http.AuthedSession
+
+interface DTO
+
+@Serializable
+data class AuthDTO(val authKey: String) : DTO
+
+@Serializable
+abstract class VerifyDTO : DTO {
+    abstract val sessionKey: String
+    @Transient
+    lateinit var session: AuthedSession // 反序列化验证成功后传入
+}
+
+@Serializable
+abstract class EventDTO : DTO
+
+object IgnoreEventDTO : EventDTO()

+ 133 - 0
src/main/kotlin/net/mamoe/mirai/api/http/data/common/MessageDTO.kt

@@ -0,0 +1,133 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.api.http.data.common
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import net.mamoe.mirai.contact.Contact
+import net.mamoe.mirai.contact.Group
+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.message.uploadImage
+import net.mamoe.mirai.utils.MiraiInternalAPI
+import java.net.URL
+
+/*
+*   DTO data class
+* */
+
+// MessagePacket
+@Serializable
+@SerialName("FriendMessage")
+data class FriendMessagePacketDTO(val sender: QQDTO) : MessagePacketDTO()
+
+@Serializable
+@SerialName("GroupMessage")
+data class GroupMessagePacketDTO(val sender: MemberDTO) : MessagePacketDTO()
+
+
+// Message
+@Serializable
+@SerialName("Source")
+data class MessageSourceDTO(val id: Long) : MessageDTO()
+
+@Serializable
+@SerialName("At")
+data class AtDTO(val target: Long, val display: String = "") : MessageDTO()
+
+@Serializable
+@SerialName("AtAll")
+data class AtAllDTO(val target: Long = 0) : MessageDTO() // target为保留字段
+
+@Serializable
+@SerialName("Face")
+data class FaceDTO(val faceId: Int) : MessageDTO()
+
+@Serializable
+@SerialName("Plain")
+data class PlainDTO(val text: String) : MessageDTO()
+
+@Serializable
+@SerialName("Image")
+data class ImageDTO(val imageId: String? = null, val url: String? = null) : MessageDTO()
+
+@Serializable
+@SerialName("Xml")
+data class XmlDTO(val xml: String) : MessageDTO()
+
+@Serializable
+@SerialName("Unknown")
+object UnknownMessageDTO : MessageDTO()
+
+/*
+*   Abstract Class
+* */
+@Serializable
+sealed class MessagePacketDTO : EventDTO() {
+    lateinit var messageChain: MessageChainDTO
+}
+
+typealias MessageChainDTO = List<MessageDTO>
+
+@Serializable
+sealed class MessageDTO : DTO
+
+
+/*
+    Extend function
+ */
+suspend fun MessagePacket<*, *>.toDTO() = when (this) {
+    is FriendMessage -> FriendMessagePacketDTO(QQDTO(sender))
+    is GroupMessage -> GroupMessagePacketDTO(MemberDTO(sender))
+    else -> IgnoreEventDTO
+}.apply {
+    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
+    }
+}
+
+suspend fun MessageChainDTO.toMessageChain(contact: Contact) =
+    buildMessageChain { [email protected] { it.toMessage(contact)?.let(::add) } }
+
+
+@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(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)
+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 -> when {
+        !imageId.isNullOrBlank() -> Image(imageId)
+        !url.isNullOrBlank() -> contact.uploadImage(URL(url))
+        else -> null
+    }
+    is XmlDTO -> XMLMessage(xml)
+    is MessageSourceDTO, is UnknownMessageDTO -> null
+}
+

+ 56 - 0
src/main/kotlin/net/mamoe/mirai/api/http/queue/MessageQueue.kt

@@ -0,0 +1,56 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.api.http.queue
+
+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.MessagePacket
+import net.mamoe.mirai.message.data.MessageSource
+import net.mamoe.mirai.utils.firstKey
+import java.util.concurrent.ConcurrentLinkedDeque
+
+class MessageQueue : ConcurrentLinkedDeque<BotEvent>() {
+
+    val cacheSize = 4096
+    val cache = LinkedHashMap<Long, MessagePacket<*, *>>()
+
+    suspend fun fetch(size: Int): List<EventDTO> {
+        var count = size
+
+        val ret = ArrayList<EventDTO>(count)
+        while (!this.isEmpty() && count > 0) {
+            val event = pop()
+
+            event.toDTO().also {
+                if (it != IgnoreEventDTO) {
+                    ret.add(it)
+                    count--
+                }
+            }
+
+            if (event is MessagePacket<*, *>) {
+                addQuoteCache(event)
+            }
+        }
+        return ret
+    }
+
+    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())
+        }
+    }
+}

+ 69 - 0
src/main/kotlin/net/mamoe/mirai/api/http/route/AuthRouteModule.kt

@@ -0,0 +1,69 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.api.http.route
+
+import io.ktor.application.Application
+import io.ktor.application.call
+import io.ktor.routing.routing
+import kotlinx.serialization.Serializable
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.api.http.AuthedSession
+import net.mamoe.mirai.api.http.SessionManager
+import net.mamoe.mirai.api.http.data.NoSuchBotException
+import net.mamoe.mirai.api.http.data.StateCode
+import net.mamoe.mirai.api.http.data.common.DTO
+import net.mamoe.mirai.api.http.data.common.VerifyDTO
+import kotlin.coroutines.EmptyCoroutineContext
+
+
+fun Application.authModule() {
+    routing {
+        miraiAuth("/auth") {
+            if (it.authKey != SessionManager.authKey) {
+                call.respondStateCode(StateCode(1, "Auth Key错误"))
+            } else {
+                call.respondDTO(AuthRetDTO(0, SessionManager.createTempSession().key))
+            }
+        }
+
+        miraiVerify<BindDTO>("/verify", verifiedSessionKey = false) {
+            val bot = getBotOrThrow(it.qq)
+            with(SessionManager) {
+                closeSession(it.sessionKey)
+                allSession[it.sessionKey] = AuthedSession(bot, EmptyCoroutineContext)
+            }
+            call.respondStateCode(StateCode.Success)
+        }
+
+        miraiVerify<BindDTO>("/release") {
+            val bot = getBotOrThrow(it.qq)
+            val session = SessionManager[it.sessionKey] as AuthedSession
+            if (bot.uin == session.bot.uin) {
+                SessionManager.closeSession(it.sessionKey)
+                call.respondStateCode(StateCode.Success)
+            } else {
+                throw NoSuchElementException()
+            }
+        }
+
+    }
+}
+
+@Serializable
+private data class AuthRetDTO(val code: Int, val session: String) : DTO
+
+@Serializable
+private data class BindDTO(override val sessionKey: String, val qq: Long) : VerifyDTO()
+
+private fun getBotOrThrow(qq: Long) = try {
+    Bot.getInstance(qq)
+} catch (e: NoSuchElementException) {
+    throw NoSuchBotException
+}

+ 214 - 0
src/main/kotlin/net/mamoe/mirai/api/http/route/BaseRoute.kt

@@ -0,0 +1,214 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.api.http.route
+
+import io.ktor.application.Application
+import io.ktor.application.ApplicationCall
+import io.ktor.application.call
+import io.ktor.application.install
+import io.ktor.features.CallLogging
+import io.ktor.features.DefaultHeaders
+import io.ktor.http.ContentType
+import io.ktor.http.HttpMethod
+import io.ktor.http.HttpStatusCode
+import io.ktor.http.content.PartData
+import io.ktor.request.receive
+import io.ktor.response.defaultTextContentType
+import io.ktor.response.respond
+import io.ktor.response.respondText
+import io.ktor.routing.Route
+import io.ktor.routing.route
+import io.ktor.util.pipeline.ContextDsl
+import io.ktor.util.pipeline.PipelineContext
+import net.mamoe.mirai.api.http.AuthedSession
+import net.mamoe.mirai.api.http.SessionManager
+import net.mamoe.mirai.api.http.TempSession
+import net.mamoe.mirai.api.http.data.*
+import net.mamoe.mirai.api.http.data.common.AuthDTO
+import net.mamoe.mirai.api.http.data.common.DTO
+import net.mamoe.mirai.api.http.data.common.VerifyDTO
+import net.mamoe.mirai.api.http.util.jsonParseOrNull
+import net.mamoe.mirai.api.http.util.toJson
+import net.mamoe.mirai.contact.PermissionDeniedException
+import org.slf4j.Logger
+import org.slf4j.helpers.NOPLogger
+import org.slf4j.helpers.NOPLoggerFactory
+import org.slf4j.impl.SimpleLogger
+import org.slf4j.impl.SimpleLoggerFactory
+
+fun Application.mirai() {
+    install(DefaultHeaders)
+    install(CallLogging) {
+        logger = NOPLoggerFactory().getLogger("NMSL")
+
+    }
+    authModule()
+    messageModule()
+    infoModule()
+    groupManageModule()
+}
+
+/**
+ * Auth,处理http server的验证
+ * 为闭包传入一个AuthDTO对象
+ */
+@ContextDsl
+internal fun Route.miraiAuth(
+    path: String,
+    body: suspend PipelineContext<Unit, ApplicationCall>.(AuthDTO) -> Unit
+): Route {
+    return route(path, HttpMethod.Post) {
+        intercept {
+            val dto = context.receiveDTO<AuthDTO>() ?: throw IllegalParamException("参数格式错误")
+            this.body(dto)
+        }
+    }
+}
+
+/**
+ * Get,用于获取bot的属性
+ * 验证请求参数中sessionKey参数的有效性
+ */
+@ContextDsl
+internal fun Route.miraiGet(
+    path: String,
+    body: suspend PipelineContext<Unit, ApplicationCall>.(AuthedSession) -> Unit
+): Route {
+    return route(path, HttpMethod.Get) {
+        intercept {
+            val sessionKey = call.parameters["sessionKey"] ?: throw IllegalParamException("参数格式错误")
+            if (!SessionManager.containSession(sessionKey)) throw IllegalSessionException
+
+            when(val session = SessionManager[sessionKey]) {
+                is TempSession -> throw NotVerifiedSessionException
+                is AuthedSession -> this.body(session)
+            }
+        }
+    }
+}
+
+/**
+ * Verify,用于处理bot的行为请求
+ * 验证数据传输对象(DTO)中是否包含sessionKey字段
+ * 且验证sessionKey的有效性
+ *
+ * @param verifiedSessionKey 是否验证sessionKey是否被激活
+ *
+ * it 为json解析出的DTO对象
+ */
+@ContextDsl
+internal inline fun <reified T : VerifyDTO> Route.miraiVerify(
+    path: String,
+    verifiedSessionKey: Boolean = true,
+    crossinline body: suspend PipelineContext<Unit, ApplicationCall>.(T) -> Unit
+): Route {
+    return route(path, HttpMethod.Post) {
+        intercept {
+            val dto = context.receiveDTO<T>() ?: throw IllegalParamException("参数格式错误")
+            SessionManager[dto.sessionKey]?.let {
+                when {
+                    it is TempSession && verifiedSessionKey -> throw NotVerifiedSessionException
+                    it is AuthedSession -> dto.session = it
+                }
+            } ?: throw IllegalSessionException
+
+            this.body(dto)
+        }
+    }
+}
+
+/**
+ * 统一捕获并处理异常
+ */
+internal inline fun Route.intercept(crossinline blk: suspend PipelineContext<Unit, ApplicationCall>.() -> Unit) = handle {
+    try {
+        blk(this)
+    } catch (e: IllegalSessionException) {
+        call.respondStateCode(StateCode.IllegalSession)
+    } catch (e: NotVerifiedSessionException) {
+        call.respondStateCode(StateCode.NotVerifySession)
+    } catch (e: NoSuchBotException) {
+        call.respondStateCode(StateCode.NoBot)
+    } catch (e: NoSuchElementException) {
+        call.respondStateCode(StateCode.NoElement)
+    } catch (e: PermissionDeniedException) {
+        call.respondStateCode(StateCode.PermissionDenied)
+    } catch (e: IllegalAccessException) {
+        call.respondStateCode(StateCode(400, e.message), HttpStatusCode.BadRequest)
+    } catch (e: Throwable) {
+        e.printStackTrace()
+        call.respond(HttpStatusCode.InternalServerError, e.message!!)
+    }
+}
+
+/*
+    extend function
+ */
+internal suspend inline fun <reified T : StateCode> ApplicationCall.respondStateCode(code: T, status: HttpStatusCode = HttpStatusCode.OK) = respondJson(code.toJson(StateCode.serializer()), status)
+
+internal suspend inline fun <reified T : DTO> ApplicationCall.respondDTO(dto: T, status: HttpStatusCode = HttpStatusCode.OK) = respondJson(dto.toJson(), status)
+
+internal suspend fun ApplicationCall.respondJson(json: String, status: HttpStatusCode = HttpStatusCode.OK) =
+    respondText(json, defaultTextContentType(ContentType("application", "json")), status)
+
+internal suspend inline fun <reified T : DTO> ApplicationCall.receiveDTO(): T? = receive<String>().jsonParseOrNull()
+
+
+fun PipelineContext<Unit, ApplicationCall>.illegalParam(
+    expectingType: String?,
+    paramName: String,
+    actualValue: String? = call.parameters[paramName]
+): Nothing = throw IllegalParamException("Illegal param. A $expectingType is required for `$paramName` while `$actualValue` is given")
+
+
+@Suppress("IMPLICIT_CAST_TO_ANY")
+@UseExperimental(ExperimentalUnsignedTypes::class)
+internal inline fun <reified R> PipelineContext<Unit, ApplicationCall>.paramOrNull(name: String): R =
+    when (R::class) {
+        Byte::class -> call.parameters[name]?.toByte()
+        Int::class -> call.parameters[name]?.toInt()
+        Short::class -> call.parameters[name]?.toShort()
+        Float::class -> call.parameters[name]?.toFloat()
+        Long::class -> call.parameters[name]?.toLong()
+        Double::class -> call.parameters[name]?.toDouble()
+        Boolean::class -> when (call.parameters[name]) {
+            "true" -> true
+            "false" -> false
+            "0" -> false
+            "1" -> true
+            null -> null
+            else -> illegalParam("boolean", name)
+        }
+
+        String::class -> call.parameters[name]
+
+        UByte::class -> call.parameters[name]?.toUByte()
+        UInt::class -> call.parameters[name]?.toUInt()
+        UShort::class -> call.parameters[name]?.toUShort()
+
+        else -> error(name::class.simpleName + " is not supported")
+    } as R ?: illegalParam(R::class.simpleName, name)
+
+/**
+ * multi part
+ */
+internal fun List<PartData>.value(name: String) =
+    try {
+        (filter { it.name == name }[0] as PartData.FormItem).value
+    } catch (e: Exception) {
+        throw IllegalParamException("参数格式错误")
+    }
+
+internal fun List<PartData>.file(name: String) =
+    try {
+        filter { it.name == name }[0] as? PartData.FileItem
+    } catch (e: Exception) {
+        throw IllegalParamException("参数格式错误")
+    }

+ 11 - 0
src/main/kotlin/net/mamoe/mirai/api/http/route/EventRouteModule.kt

@@ -0,0 +1,11 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.api.http.route
+

+ 152 - 0
src/main/kotlin/net/mamoe/mirai/api/http/route/GroupManageRouteModule.kt

@@ -0,0 +1,152 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.api.http.route
+
+import io.ktor.application.Application
+import io.ktor.application.call
+import io.ktor.routing.routing
+import kotlinx.serialization.Serializable
+import net.mamoe.mirai.api.http.data.StateCode
+import net.mamoe.mirai.api.http.data.common.DTO
+import net.mamoe.mirai.api.http.data.common.VerifyDTO
+import net.mamoe.mirai.contact.Group
+import net.mamoe.mirai.contact.Member
+
+
+fun Application.groupManageModule() {
+    routing {
+
+        /**
+         * 禁言(需要相关权限)
+         */
+        miraiVerify<MuteDTO>("/muteAll") {
+            it.session.bot.getGroup(it.target).isMuteAll = true
+            call.respondStateCode(StateCode.Success)
+        }
+
+        miraiVerify<MuteDTO>("/unmuteAll") {
+            it.session.bot.getGroup(it.target).isMuteAll = false
+            call.respondStateCode(StateCode.Success)
+        }
+
+        miraiVerify<MuteDTO>("/mute") {
+            it.session.bot.getGroup(it.target)[it.memberId].mute(it.time)
+            call.respondStateCode(StateCode.Success)
+        }
+
+        miraiVerify<MuteDTO>("/unmute") {
+            it.session.bot.getGroup(it.target).members[it.memberId].unmute()
+            call.respondStateCode(StateCode.Success)
+        }
+
+        /**
+         * 移出群聊(需要相关权限)
+         */
+        miraiVerify<KickDTO>("/kick") {
+            it.session.bot.getGroup(it.target)[it.memberId].kick(it.msg)
+            call.respondStateCode(StateCode.Success)
+        }
+
+        /**
+         * 群设置(需要相关权限)
+         */
+        miraiGet("/groupConfig") {
+            val group = it.bot.getGroup(paramOrNull("target"))
+            call.respondDTO(GroupDetailDTO(group))
+        }
+
+        miraiVerify<GroupConfigDTO>("/groupConfig") { dto ->
+            val group = dto.session.bot.getGroup(dto.target)
+            with(dto.config) {
+                name?.let { group.name = it }
+                announcement?.let { group.entranceAnnouncement = it }
+                confessTalk?.let { group.isConfessTalkEnabled = it }
+                allowMemberInvite?.let { group.isAllowMemberInvite = it }
+                // TODO: 待core接口实现设置可改
+//                autoApprove?.let { group.autoApprove = it }
+//                anonymousChat?.let { group.anonymousChat = it }
+            }
+            call.respondStateCode(StateCode.Success)
+        }
+
+        /**
+         * 群员信息管理(需要相关权限)
+         */
+        miraiGet("/memberInfo") {
+            val member = it.bot.getGroup(paramOrNull("target"))[paramOrNull("memberId")]
+            call.respondDTO(MemberDetailDTO(member))
+        }
+
+        miraiVerify<MemberInfoDTO>("/memberInfo") { dto ->
+            val member = dto.session.bot.getGroup(dto.target)[dto.memberId]
+            with(dto.info) {
+                name?.let { member.nameCard = it }
+                specialTitle?.let { member.specialTitle = it }
+            }
+            call.respondStateCode(StateCode.Success)
+        }
+
+    }
+}
+
+
+@Serializable
+private data class MuteDTO(
+    override val sessionKey: String,
+    val target: Long,
+    val memberId: Long = 0,
+    val time: Int = 0
+) : VerifyDTO()
+
+@Serializable
+private data class KickDTO(
+    override val sessionKey: String,
+    val target: Long,
+    val memberId: Long,
+    val msg: String = ""
+) : VerifyDTO()
+
+@Serializable
+private data class GroupConfigDTO(
+    override val sessionKey: String,
+    val target: Long,
+    val config: GroupDetailDTO
+) : VerifyDTO()
+
+@Serializable
+private data class GroupDetailDTO(
+    val name: String? = null,
+    val announcement: String? = null,
+    val confessTalk: Boolean? = null,
+    val allowMemberInvite: Boolean? = null,
+    val autoApprove: Boolean? = null,
+    val anonymousChat: Boolean? = null
+) : DTO {
+    constructor(group: Group) : this(
+        group.name, group.entranceAnnouncement, group.isConfessTalkEnabled, group.isAllowMemberInvite,
+        group.isAutoApproveEnabled, group.isAnonymousChatEnabled
+    )
+}
+
+@Serializable
+private data class MemberInfoDTO(
+    override val sessionKey: String,
+    val target: Long,
+    val memberId: Long,
+    val info: MemberDetailDTO
+) : VerifyDTO()
+
+@Serializable
+private data class MemberDetailDTO(
+    val name: String? = null,
+    val specialTitle: String? = null
+) : DTO {
+    constructor(member: Member) : this(member.nameCard, member.specialTitle)
+}

+ 39 - 0
src/main/kotlin/net/mamoe/mirai/api/http/route/InfoRouteModule.kt

@@ -0,0 +1,39 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.api.http.route
+
+import io.ktor.application.Application
+import io.ktor.application.call
+import io.ktor.routing.routing
+import net.mamoe.mirai.api.http.data.common.GroupDTO
+import net.mamoe.mirai.api.http.data.common.MemberDTO
+import net.mamoe.mirai.api.http.data.common.QQDTO
+import net.mamoe.mirai.api.http.util.toJson
+import net.mamoe.mirai.contact.toMutableList
+
+fun Application.infoModule() {
+    routing {
+
+        miraiGet("/friendList") {
+            val ls = it.bot.qqs.toMutableList().map { qq -> QQDTO(qq) }
+            call.respondJson(ls.toJson())
+        }
+
+        miraiGet("/groupList") {
+            val ls = it.bot.groups.toMutableList().map { group -> GroupDTO(group) }
+            call.respondJson(ls.toJson())
+        }
+
+        miraiGet("/memberList") {
+            val ls = it.bot.getGroup(paramOrNull("target")).members.toMutableList().map { member -> MemberDTO(member) }
+            call.respondJson(ls.toJson())
+        }
+    }
+}

+ 168 - 0
src/main/kotlin/net/mamoe/mirai/api/http/route/MessageRouteModule.kt

@@ -0,0 +1,168 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.api.http.route
+
+import io.ktor.application.Application
+import io.ktor.application.call
+import io.ktor.http.HttpStatusCode
+import io.ktor.http.content.readAllParts
+import io.ktor.http.content.streamProvider
+import io.ktor.request.receiveMultipart
+import io.ktor.response.respond
+import io.ktor.response.respondText
+import io.ktor.routing.post
+import io.ktor.routing.routing
+import kotlinx.serialization.Serializable
+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.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
+
+fun Application.messageModule() {
+    routing {
+
+        miraiGet("/fetchMessage") {
+            val count: Int = paramOrNull("count")
+            val fetch = it.messageQueue.fetch(count)
+
+            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 {
+                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") {
+            val quote = it.quote?.let { q ->
+                it.session.messageQueue.cache(q).run {
+                    this[MessageSource].quote(sender)
+                }}
+
+            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") {
+            val bot = it.session.bot
+            val contact = when {
+                it.target != null -> bot[it.target]
+                it.qq != null -> bot.getFriend(it.qq)
+                it.group != null -> bot.getGroup(it.group)
+                else -> throw IllegalParamException("target、qq、group不可全为null")
+            }
+            val ls = it.urls.map { url -> contact.uploadImage(URL(url)) }
+            contact.sendMessage(MessageChain(ls))
+            call.respondJson(ls.map { image -> image.imageId }.toJson())
+        }
+
+        // TODO: 重构
+        post("uploadImage") {
+            val parts = call.receiveMultipart().readAllParts()
+            val sessionKey = parts.value("sessionKey")
+            if (!SessionManager.containSession(sessionKey)) throw IllegalSessionException
+            val session = try {
+                SessionManager[sessionKey] as AuthedSession
+            } catch (e: TypeCastException) {
+                throw NotVerifiedSessionException
+            }
+
+            val type = parts.value("type")
+            parts.file("img")?.apply {
+                val image = streamProvider().use {
+                    when (type) {
+                        "group" -> session.bot.groups.firstOrNull()?.uploadImage(it)
+                        "friend" -> session.bot.qqs.firstOrNull()?.uploadImage(it)
+                        else -> null
+                    }
+                }
+                image?.apply {
+                    call.respondText(imageId)
+                } ?: throw IllegalAccessException("图片上传错误")
+            } ?: throw IllegalAccessException("未知错误")
+        }
+
+        miraiVerify<RecallDTO>("recall") {
+            it.session.messageQueue.cache(it.target).apply {
+                it.session.bot.recall(get(MessageSource))
+            }
+            call.respondStateCode(StateCode.Success)
+        }
+    }
+}
+
+@Serializable
+private data class SendDTO(
+    override val sessionKey: String,
+    val quote: Long? = null,
+    val target: Long,
+    val messageChain: MessageChainDTO
+) : VerifyDTO()
+
+@Serializable
+private data class SendImageDTO(
+    override val sessionKey: String,
+    val target: Long? = null,
+    val qq: Long? = null,
+    val group: Long? = null,
+    val urls: List<String>
+) : VerifyDTO()
+
+@Serializable
+private class SendRetDTO(
+    val code: Int = 0,
+    val msg: String = "success",
+    val messageId: Long
+) : DTO
+
+@Serializable
+private data class RecallDTO(
+    override val sessionKey: String,
+    val target: Long
+) : VerifyDTO()

+ 89 - 0
src/main/kotlin/net/mamoe/mirai/api/http/util/Json.kt

@@ -0,0 +1,89 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.api.http.util
+
+import kotlinx.serialization.*
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.modules.SerializersModule
+import net.mamoe.mirai.api.http.data.common.*
+
+// 解析失败时直接返回null,由路由判断响应400状态
+@UseExperimental(ImplicitReflectionSerializer::class)
+inline fun <reified T : Any> String.jsonParseOrNull(
+    serializer: DeserializationStrategy<T>? = null
+): T? = try {
+    if(serializer == null) MiraiJson.json.parse(this) else Json.parse(this)
+} catch (e: Exception) { null }
+
+
+@UseExperimental(ImplicitReflectionSerializer::class, UnstableDefault::class)
+inline fun <reified T : Any> T.toJson(
+    serializer: SerializationStrategy<T>? = null
+): String = if (serializer == null) MiraiJson.json.stringify(this)
+else MiraiJson.json.stringify(serializer, this)
+
+
+// 序列化列表时,stringify需要使用的泛型是T,而非List<T>
+// 因为使用的stringify的stringify(objs: List<T>)重载
+@UseExperimental(ImplicitReflectionSerializer::class, UnstableDefault::class)
+inline fun <reified T : Any> List<T>.toJson(
+    serializer: SerializationStrategy<List<T>>? = null
+): String = if (serializer == null) MiraiJson.json.stringify(this)
+else MiraiJson.json.stringify(serializer, this)
+
+
+/**
+ * Json解析规则,需要注册支持的多态的类
+ */
+object MiraiJson {
+    val json = Json(context = SerializersModule {
+
+        polymorphic(EventDTO.serializer()) {
+            GroupMessagePacketDTO::class with GroupMessagePacketDTO.serializer()
+            FriendMessagePacketDTO::class with FriendMessagePacketDTO.serializer()
+
+            BotOnlineEventDTO::class with BotOnlineEventDTO.serializer()
+            BotOfflineEventActiveDTO::class with BotOfflineEventActiveDTO.serializer()
+            BotOfflineEventForceDTO::class with BotOfflineEventForceDTO.serializer()
+            BotOfflineEventDroppedDTO::class with BotOfflineEventDroppedDTO.serializer()
+            BotReloginEventDTO::class with BotReloginEventDTO.serializer()
+            BotGroupPermissionChangeEventDTO::class with BotGroupPermissionChangeEventDTO.serializer()
+            BotMuteEventDTO::class with BotMuteEventDTO.serializer()
+            BotUnmuteEventDTO::class with BotUnmuteEventDTO.serializer()
+            BotJoinGroupEventDTO::class with BotJoinGroupEventDTO.serializer()
+            GroupNameChangeEventDTO::class with GroupNameChangeEventDTO.serializer()
+            GroupEntranceAnnouncementChangeEventDTO::class with GroupEntranceAnnouncementChangeEventDTO.serializer()
+            GroupMuteAllEventDTO::class with GroupMuteAllEventDTO.serializer()
+            GroupAllowAnonymousChatEventDTO::class with GroupAllowAnonymousChatEventDTO.serializer()
+            GroupAllowConfessTalkEventDTO::class with GroupAllowConfessTalkEventDTO.serializer()
+            GroupAllowMemberInviteEventDTO::class with GroupAllowMemberInviteEventDTO.serializer()
+            MemberJoinEventDTO::class with MemberJoinEventDTO.serializer()
+            MemberLeaveEventKickDTO::class with MemberLeaveEventKickDTO.serializer()
+            MemberLeaveEventQuitDTO::class with MemberLeaveEventQuitDTO.serializer()
+            MemberCardChangeEventDTO::class with MemberCardChangeEventDTO.serializer()
+            MemberSpecialTitleChangeEventDTO::class with MemberSpecialTitleChangeEventDTO.serializer()
+            MemberPermissionChangeEventDTO::class with MemberPermissionChangeEventDTO.serializer()
+            MemberMuteEventDTO::class with MemberMuteEventDTO.serializer()
+            MemberUnmuteEventDTO::class with MemberUnmuteEventDTO.serializer()
+        }
+
+        // Message Polymorphic
+//        polymorphic(MessageDTO.serializer()) {
+//            MessageSourceDTO::class with MessageSourceDTO.serializer()
+//            AtDTO::class with AtDTO.serializer()
+//            AtAllDTO::class with AtAllDTO.serializer()
+//            FaceDTO::class with FaceDTO.serializer()
+//            PlainDTO::class with PlainDTO.serializer()
+//            ImageDTO::class with ImageDTO.serializer()
+//            XmlDTO::class with XmlDTO.serializer()
+//            UnknownMessageDTO::class with UnknownMessageDTO.serializer()
+//        }
+    })
+}