mirror of
https://github.com/mamoe/mirai.git
synced 2025-04-01 20:50:15 +08:00
base framework
This commit is contained in:
parent
30d41f6e9d
commit
4b9900e0bd
@ -3,12 +3,11 @@
|
|||||||
<b>
|
<b>
|
||||||
Mirai-API-http 提供HTTP API供所有语言使用mirai<br>
|
Mirai-API-http 提供HTTP API供所有语言使用mirai<br>
|
||||||
</b>
|
</b>
|
||||||
|
|
||||||
### 开始会话-认证(Authorize)
|
### 开始会话-认证(Authorize)
|
||||||
|
|
||||||
```php
|
```
|
||||||
路径: /auth
|
[POST] /auth
|
||||||
方法: POST
|
|
||||||
```
|
```
|
||||||
使用此方法验证你的会话连接, 并将这个会话绑定一个BOT<br>
|
使用此方法验证你的会话连接, 并将这个会话绑定一个BOT<br>
|
||||||
注意: 每个会话只能绑定一个BOT.
|
注意: 每个会话只能绑定一个BOT.
|
||||||
@ -20,36 +19,34 @@ Mirai-API-http 提供HTTP API供所有语言使用mirai<br>
|
|||||||
| key | String |false|U9HSaDXl39ksd918273hU|MIRAI API HTTP key, HTTP API的核心key|
|
| key | String |false|U9HSaDXl39ksd918273hU|MIRAI API HTTP key, HTTP API的核心key|
|
||||||
| qq | String |false|1040400290|需要绑定的BOT QQ号|
|
| qq | String |false|1040400290|需要绑定的BOT QQ号|
|
||||||
|
|
||||||
|
|
||||||
#### 返回(成功):<br>
|
#### 返回(成功):<br>
|
||||||
|
|
||||||
| 名字 | 类型 | 举例 | 说明|
|
| 名字 | 类型 | 举例 | 说明|
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| success |Boolean |true|是否验证成功|
|
| code |Int |0|返回状态|
|
||||||
| session |String |UANSHDKSLAOISN|你的session key|
|
| session |String |UANSHDKSLAOISN|你的session key|
|
||||||
|
|
||||||
|
#### 状态码:<br>
|
||||||
#### 返回(失败):<br>
|
|
||||||
|
|
||||||
| name | type | example|note|
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| success |Boolean |false|是否验证成功|
|
|
||||||
| session |String |null|你的session key|
|
|
||||||
| error |int |0|错误码|
|
|
||||||
|
|
||||||
#### 错误码:<br>
|
|
||||||
|
|
||||||
| 代码 | 原因|
|
| 代码 | 原因|
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 0 | 错误的MIRAI API HTTP key |
|
| 0 | 正常 |
|
||||||
| 1 | 试图绑定不存在的bot|
|
| 1 | 错误的MIRAI API HTTP key|
|
||||||
|
| 2 | 试图绑定不存在的bot|
|
||||||
|
|
||||||
session key 是使用以下方法必须携带的</br>
|
session key 是使用以下方法必须携带的</br>
|
||||||
session key 需要被以cookie的形式上报 <b>cookies</b> :
|
session key 需要被以cookie的形式上报 <b>cookies</b> :
|
||||||
|
|
||||||
| name | value |
|
| 名字 | 值 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| session |your session key here |
|
| session |your session key here |
|
||||||
|
|
||||||
如果出现HTTP 403错误码,代表session key已过期, 需要重新获取
|
如果出现HTTP 403错误码,代表session key已过期, 需要重新获取
|
||||||
|
|
||||||
|
### 发送好友消息
|
||||||
|
|
||||||
|
```
|
||||||
|
[POST] /sendFriendMessage
|
||||||
|
```
|
||||||
|
|
||||||
|
@ -42,6 +42,7 @@ kotlin {
|
|||||||
implementation(ktor("server-cio"))
|
implementation(ktor("server-cio"))
|
||||||
implementation(kotlinx("io-jvm", kotlinXIoVersion))
|
implementation(kotlinx("io-jvm", kotlinXIoVersion))
|
||||||
implementation(ktor("http-jvm"))
|
implementation(ktor("http-jvm"))
|
||||||
|
implementation("org.slf4j:slf4j-simple:1.7.26")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
package net.mamoe.mirai.api.http
|
||||||
|
|
||||||
|
import io.ktor.application.Application
|
||||||
|
import io.ktor.server.cio.CIO
|
||||||
|
import io.ktor.server.engine.embeddedServer
|
||||||
|
import io.ktor.util.KtorExperimentalAPI
|
||||||
|
import net.mamoe.mirai.api.http.route.mirai
|
||||||
|
import net.mamoe.mirai.utils.DefaultLogger
|
||||||
|
|
||||||
|
object MiraiHttpAPIServer {
|
||||||
|
|
||||||
|
private val logger = DefaultLogger("Mirai HTTP API")
|
||||||
|
|
||||||
|
init {
|
||||||
|
SessionManager.authKey = generateSessionKey()//用于验证的key, 使用和SessionKey相同的方法生成, 但意义不同
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseExperimental(KtorExperimentalAPI::class)
|
||||||
|
fun start(
|
||||||
|
port: Int = 8080,
|
||||||
|
authKey: String? = null,
|
||||||
|
callback: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
authKey?.apply {
|
||||||
|
if (authKey.length in 8..128) {
|
||||||
|
SessionManager.authKey = authKey
|
||||||
|
} else {
|
||||||
|
logger.error("Expected authKey length is between 8 to 128")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: start是无阻塞的,理应获取启动状态后再执行后续代码
|
||||||
|
try {
|
||||||
|
embeddedServer(CIO, port, module = Application::mirai).start()
|
||||||
|
|
||||||
|
logger.info("Http api server is running with authKey: ${SessionManager.authKey}")
|
||||||
|
callback?.invoke()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error("Http api server launch error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,11 @@
|
|||||||
package net.mamoe.mirai.api.http
|
package net.mamoe.mirai.api.http
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.serialization.json.Json
|
import net.mamoe.mirai.Bot
|
||||||
import kotlinx.serialization.json.JsonConfiguration
|
import net.mamoe.mirai.api.http.queue.MessageQueue
|
||||||
import java.lang.StringBuilder
|
import net.mamoe.mirai.event.Listener
|
||||||
|
import net.mamoe.mirai.event.subscribeMessages
|
||||||
|
import net.mamoe.mirai.message.MessagePacket
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
@ -44,6 +46,10 @@ object SessionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(sessionKey: String) = allSession.remove(sessionKey)?.also {it.close() }
|
||||||
|
|
||||||
fun closeSession(session: Session) = closeSession(session.key)
|
fun closeSession(session: Session) = closeSession(session.key)
|
||||||
@ -69,7 +75,7 @@ abstract class Session internal constructor(
|
|||||||
val key:String = generateSessionKey()
|
val key:String = generateSessionKey()
|
||||||
|
|
||||||
|
|
||||||
internal fun close(){
|
internal open fun close(){
|
||||||
supervisorJob.complete()
|
supervisorJob.complete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -89,11 +95,20 @@ class TempSession internal constructor(coroutineContext: CoroutineContext) : Ses
|
|||||||
* 任何[TempSession]认证后转化为一个[AuthedSession]
|
* 任何[TempSession]认证后转化为一个[AuthedSession]
|
||||||
* 在这一步[AuthedSession]应该已经有assigned的bot
|
* 在这一步[AuthedSession]应该已经有assigned的bot
|
||||||
*/
|
*/
|
||||||
class AuthedSession internal constructor(val botNumber:Int, coroutineContext: CoroutineContext):Session(coroutineContext){
|
class AuthedSession internal constructor(val bot: Bot, coroutineContext: CoroutineContext):Session(coroutineContext){
|
||||||
|
|
||||||
|
val messageQueue = MessageQueue()
|
||||||
|
private val _listener : Listener<MessagePacket<*, *>>
|
||||||
|
|
||||||
|
init {
|
||||||
|
bot.subscribeMessages {
|
||||||
|
_listener = always { this.run(messageQueue::add) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
_listener.complete()
|
||||||
|
super.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
|||||||
|
package net.mamoe.mirai.api.http.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AuthDTO(val authKey: String) : DTO
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AuthResDTO(val code: Int, val session: String) : DTO
|
@ -0,0 +1,39 @@
|
|||||||
|
package net.mamoe.mirai.api.http.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import net.mamoe.mirai.contact.Group
|
||||||
|
import net.mamoe.mirai.contact.Member
|
||||||
|
import net.mamoe.mirai.contact.MemberPermission
|
||||||
|
import net.mamoe.mirai.contact.QQ
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
abstract class ContactDTO : DTO {
|
||||||
|
abstract val id: Long
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class QQDTO(
|
||||||
|
override val id: Long,
|
||||||
|
val nickName: String,
|
||||||
|
val remark: String
|
||||||
|
) : ContactDTO()
|
||||||
|
|
||||||
|
suspend fun QQDTO(qq: QQ): QQDTO = QQDTO(qq.id, qq.queryProfile().nickname, qq.queryRemark().value)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MemberDTO(
|
||||||
|
override val id: Long,
|
||||||
|
val memberName: String = "",
|
||||||
|
val group: GroupDTO,
|
||||||
|
val permission: MemberPermission
|
||||||
|
) : ContactDTO()
|
||||||
|
|
||||||
|
fun MemberDTO(member: Member, name: String = ""): MemberDTO = MemberDTO(member.id, name, GroupDTO(member.group), member.permission)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GroupDTO(
|
||||||
|
override val id: Long,
|
||||||
|
val name: String
|
||||||
|
) : ContactDTO()
|
||||||
|
|
||||||
|
fun GroupDTO(group: Group): GroupDTO = GroupDTO(group.id, group.name)
|
@ -0,0 +1,42 @@
|
|||||||
|
package net.mamoe.mirai.api.http.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.*
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
|
|
||||||
|
interface DTO
|
||||||
|
|
||||||
|
object MiraiJson {
|
||||||
|
val json = Json(context = SerializersModule {
|
||||||
|
polymorphic(MessagePacketDTO.serializer()) {
|
||||||
|
GroupMessagePacketDTO::class with GroupMessagePacketDTO.serializer()
|
||||||
|
FriendMessagePacketDTO::class with FriendMessagePacketDTO.serializer()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析失败时直接返回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 MiraiJson.json.parse(serializer, this)
|
||||||
|
} catch (e: Exception) { throw e }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@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 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 Json.stringify(serializer, this)
|
@ -0,0 +1,86 @@
|
|||||||
|
package net.mamoe.mirai.api.http.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import net.mamoe.mirai.message.FriendMessage
|
||||||
|
import net.mamoe.mirai.message.GroupMessage
|
||||||
|
import net.mamoe.mirai.message.MessagePacket
|
||||||
|
import net.mamoe.mirai.message.data.*
|
||||||
|
|
||||||
|
/*
|
||||||
|
DTO data class
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("FriendMessage")
|
||||||
|
data class FriendMessagePacketDTO(val sender: QQDTO) : MessagePacketDTO()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("GroupMessage")
|
||||||
|
data class GroupMessagePacketDTO(val sender: MemberDTO) : MessagePacketDTO()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MessageDTO(val type: MessageType, val data: String) : DTO
|
||||||
|
|
||||||
|
typealias MessageChainDTO = Array<MessageDTO>
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
abstract class MessagePacketDTO : DTO {
|
||||||
|
lateinit var messageChain : MessageChainDTO
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val EMPTY = @SerialName("UnknownMessage") object : MessagePacketDTO() {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Extend function
|
||||||
|
*/
|
||||||
|
suspend fun MessagePacket<*, *>.toDTO(): MessagePacketDTO = when (this) {
|
||||||
|
is FriendMessage -> FriendMessagePacketDTO(QQDTO(sender))
|
||||||
|
is GroupMessage -> GroupMessagePacketDTO(MemberDTO(sender, senderName))
|
||||||
|
else -> MessagePacketDTO.EMPTY
|
||||||
|
}.apply { messageChain = Array(message.size){ message[it].toDTO() }}
|
||||||
|
|
||||||
|
fun MessageChainDTO.toMessageChain() =
|
||||||
|
MessageChain().apply { this@toMessageChain.forEach { add(it.toMessage()) } }
|
||||||
|
|
||||||
|
@UseExperimental(ExperimentalUnsignedTypes::class)
|
||||||
|
fun Message.toDTO() = when (this) {
|
||||||
|
is At -> MessageDTO(MessageType.AT, target.toString())
|
||||||
|
is Face -> MessageDTO(MessageType.FACE, id.value.toString())
|
||||||
|
is PlainText -> MessageDTO(MessageType.PLAIN, stringValue)
|
||||||
|
// is Image -> MessageDTO(MessageType.IMAGE, ???)
|
||||||
|
is Image -> MessageDTO(MessageType.IMAGE, "NOT SUPPORT IMAGE NOW")
|
||||||
|
is XMLMessage -> MessageDTO(MessageType.XML, stringValue)
|
||||||
|
else -> MessageDTO(MessageType.UNKNOWN, "not support type")
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseExperimental(ExperimentalUnsignedTypes::class)
|
||||||
|
fun MessageDTO.toMessage() = when (type) {
|
||||||
|
MessageType.AT -> At(data.toLong())
|
||||||
|
MessageType.FACE -> Face(FaceId(data.toUByte()))
|
||||||
|
MessageType.PLAIN -> PlainText(data)
|
||||||
|
// MessageType.IMAGE -> Image(???)
|
||||||
|
MessageType.IMAGE -> PlainText(data)
|
||||||
|
MessageType.XML -> XMLMessage(data)
|
||||||
|
MessageType.UNKNOWN -> PlainText(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Enum
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: will be replace by [net.mamoe.mirai.message.MessageType]
|
||||||
|
enum class MessageType {
|
||||||
|
AT,
|
||||||
|
FACE,
|
||||||
|
PLAIN,
|
||||||
|
IMAGE,
|
||||||
|
XML,
|
||||||
|
UNKNOWN,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
|||||||
|
package net.mamoe.mirai.api.http.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
import net.mamoe.mirai.api.http.AuthedSession
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
abstract class VerifyDTO : DTO {
|
||||||
|
abstract val sessionKey: String
|
||||||
|
@Transient lateinit var session: AuthedSession // 反序列化验证后传入
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BindDTO(override val sessionKey: String, val qq: Long) : VerifyDTO()
|
||||||
|
|
||||||
|
|
||||||
|
// 写成data class并继承DTO接口是为了返回时的形式统一
|
||||||
|
@Serializable
|
||||||
|
open class StateCodeDTO(val code: Int, val msg: String) : DTO {
|
||||||
|
companion object {
|
||||||
|
val SUCCESS = StateCodeDTO(0, "success") // 成功
|
||||||
|
// val AUTH_WRONG = CodeDTO(1) // AuthKey错误, @see AuthResDTO
|
||||||
|
val NO_BOT = StateCodeDTO(2, "指定Bot不存在")
|
||||||
|
val ILLEGAL_SESSION = StateCodeDTO(3, "Session失效或不存在")
|
||||||
|
val NOT_VERIFIED_SESSION = StateCodeDTO(3, "Session未认证")
|
||||||
|
}
|
||||||
|
class ILLEGAL_ACCESS(msg: String) : StateCodeDTO(400, msg) // 非法访问
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SendDTO(
|
||||||
|
override val sessionKey: String,
|
||||||
|
val target: Long,
|
||||||
|
val messageChain: MessageChainDTO
|
||||||
|
) : VerifyDTO()
|
@ -0,0 +1,17 @@
|
|||||||
|
package net.mamoe.mirai.api.http.queue
|
||||||
|
|
||||||
|
import net.mamoe.mirai.message.MessagePacket
|
||||||
|
import java.util.concurrent.ConcurrentLinkedDeque
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
class MessageQueue : ConcurrentLinkedDeque<MessagePacket<*, *>>() {
|
||||||
|
|
||||||
|
fun fetch(size: Int): List<MessagePacket<*, *>> {
|
||||||
|
var count = size
|
||||||
|
val ret = ArrayList<MessagePacket<*, *>>(count)
|
||||||
|
while (!this.isEmpty() && count-- > 0) {
|
||||||
|
ret.add(this.pop())
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
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.Bot
|
||||||
|
import net.mamoe.mirai.api.http.AuthedSession
|
||||||
|
import net.mamoe.mirai.api.http.SessionManager
|
||||||
|
import net.mamoe.mirai.api.http.dto.*
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
|
|
||||||
|
fun Application.authModule() {
|
||||||
|
routing {
|
||||||
|
miraiAuth("/auth") {
|
||||||
|
if (it.authKey != SessionManager.authKey) {
|
||||||
|
call.respondDTO(AuthResDTO(1, ""))
|
||||||
|
} else {
|
||||||
|
call.respondDTO(AuthResDTO(0, SessionManager.createTempSession().key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
miraiVerify<BindDTO>("/verify", verifiedSessionKey = false) {
|
||||||
|
try {
|
||||||
|
val bot = Bot.instanceWhose(it.qq)
|
||||||
|
with(SessionManager) {
|
||||||
|
closeSession(it.sessionKey)
|
||||||
|
allSession[it.sessionKey] = AuthedSession(bot, EmptyCoroutineContext)
|
||||||
|
}
|
||||||
|
call.respondDTO(StateCodeDTO.SUCCESS)
|
||||||
|
} catch (e: NoSuchElementException) {
|
||||||
|
call.respondDTO(StateCodeDTO.NO_BOT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
miraiVerify<BindDTO>("/release") {
|
||||||
|
SessionManager.closeSession(it.sessionKey)
|
||||||
|
call.respondDTO(StateCodeDTO.SUCCESS)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,191 @@
|
|||||||
|
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.request.receive
|
||||||
|
import io.ktor.response.defaultTextContentType
|
||||||
|
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.dto.*
|
||||||
|
|
||||||
|
fun Application.mirai() {
|
||||||
|
install(DefaultHeaders)
|
||||||
|
install(CallLogging)
|
||||||
|
|
||||||
|
authModule()
|
||||||
|
messageModule()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.respondDTO(StateCodeDTO.ILLEGAL_SESSION)
|
||||||
|
} catch (e: NotVerifiedSessionException) {
|
||||||
|
call.respondDTO(StateCodeDTO.NOT_VERIFIED_SESSION)
|
||||||
|
} catch (e: IllegalAccessException) {
|
||||||
|
call.respondDTO(StateCodeDTO.ILLEGAL_ACCESS(e.message), HttpStatusCode.BadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
extend function
|
||||||
|
*/
|
||||||
|
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>().apply(::println).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)
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误请求. 抛出这个异常后将会返回错误给一个请求
|
||||||
|
*/
|
||||||
|
@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未激活")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误参数
|
||||||
|
*/
|
||||||
|
class IllegalParamException(message: String) : IllegalAccessException(message)
|
||||||
|
|
@ -0,0 +1,2 @@
|
|||||||
|
package net.mamoe.mirai.api.http.route
|
||||||
|
|
@ -0,0 +1,39 @@
|
|||||||
|
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.AuthedSession
|
||||||
|
import net.mamoe.mirai.api.http.SessionManager
|
||||||
|
import net.mamoe.mirai.api.http.dto.*
|
||||||
|
|
||||||
|
fun Application.messageModule() {
|
||||||
|
routing {
|
||||||
|
|
||||||
|
miraiGet("/fetchMessage") {
|
||||||
|
val count: Int = paramOrNull("count")
|
||||||
|
val fetch = it.messageQueue.fetch(count)
|
||||||
|
val ls = Array(fetch.size) { index -> fetch[index].toDTO() }
|
||||||
|
|
||||||
|
call.respondJson(ls.toList().toJson())
|
||||||
|
}
|
||||||
|
|
||||||
|
miraiVerify<SendDTO>("/sendFriendMessage") {
|
||||||
|
it.session.bot.getQQ(it.target).sendMessage(it.messageChain.toMessageChain())
|
||||||
|
call.respondDTO(StateCodeDTO.SUCCESS)
|
||||||
|
}
|
||||||
|
|
||||||
|
miraiVerify<SendDTO>("/sendGroupMessage") {
|
||||||
|
it.session.bot.getGroup(it.target).sendMessage(it.messageChain.toMessageChain())
|
||||||
|
call.respondDTO(StateCodeDTO.SUCCESS)
|
||||||
|
}
|
||||||
|
|
||||||
|
miraiVerify<VerifyDTO>("/event/message") {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
miraiVerify<VerifyDTO>("/addFriend") {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user