mirror of
https://github.com/mamoe/mirai.git
synced 2025-03-13 23:00:14 +08:00
commit
76450c87f1
@ -3,12 +3,11 @@
|
||||
<b>
|
||||
Mirai-API-http 提供HTTP API供所有语言使用mirai<br>
|
||||
</b>
|
||||
|
||||
|
||||
### 开始会话-认证(Authorize)
|
||||
|
||||
```php
|
||||
路径: /auth
|
||||
方法: POST
|
||||
```
|
||||
[POST] /auth
|
||||
```
|
||||
使用此方法验证你的会话连接, 并将这个会话绑定一个BOT<br>
|
||||
注意: 每个会话只能绑定一个BOT.
|
||||
@ -20,36 +19,34 @@ Mirai-API-http 提供HTTP API供所有语言使用mirai<br>
|
||||
| key | String |false|U9HSaDXl39ksd918273hU|MIRAI API HTTP key, HTTP API的核心key|
|
||||
| qq | String |false|1040400290|需要绑定的BOT QQ号|
|
||||
|
||||
|
||||
|
||||
#### 返回(成功):<br>
|
||||
|
||||
| 名字 | 类型 | 举例 | 说明|
|
||||
| --- | --- | --- | --- |
|
||||
| success |Boolean |true|是否验证成功|
|
||||
| code |Int |0|返回状态|
|
||||
| session |String |UANSHDKSLAOISN|你的session key|
|
||||
|
||||
|
||||
#### 返回(失败):<br>
|
||||
|
||||
| name | type | example|note|
|
||||
| --- | --- | --- | --- |
|
||||
| success |Boolean |false|是否验证成功|
|
||||
| session |String |null|你的session key|
|
||||
| error |int |0|错误码|
|
||||
|
||||
#### 错误码:<br>
|
||||
#### 状态码:<br>
|
||||
|
||||
| 代码 | 原因|
|
||||
| --- | --- |
|
||||
| 0 | 错误的MIRAI API HTTP key |
|
||||
| 1 | 试图绑定不存在的bot|
|
||||
|
||||
| 0 | 正常 |
|
||||
| 1 | 错误的MIRAI API HTTP key|
|
||||
| 2 | 试图绑定不存在的bot|
|
||||
|
||||
session key 是使用以下方法必须携带的</br>
|
||||
session key 需要被以cookie的形式上报 <b>cookies</b> :
|
||||
|
||||
| name | value |
|
||||
| --- | --- |
|
||||
| session |your session key here |
|
||||
|
||||
如果出现HTTP 403错误码,代表session key已过期, 需要重新获取
|
||||
|
||||
| 名字 | 值 |
|
||||
| --- | --- |
|
||||
| session |your session key here |
|
||||
|
||||
如果出现HTTP 403错误码,代表session key已过期, 需要重新获取
|
||||
|
||||
### 发送好友消息
|
||||
|
||||
```
|
||||
[POST] /sendFriendMessage
|
||||
```
|
||||
|
||||
|
@ -42,6 +42,7 @@ kotlin {
|
||||
implementation(ktor("server-cio"))
|
||||
implementation(kotlinx("io-jvm", kotlinXIoVersion))
|
||||
implementation(ktor("http-jvm"))
|
||||
implementation("org.slf4j:slf4j-simple:1.7.26")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,171 +0,0 @@
|
||||
package net.mamoe.mirai.api.http
|
||||
|
||||
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.HttpMethod
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.routing.route
|
||||
import io.ktor.routing.routing
|
||||
import io.ktor.server.engine.applicationEngineEnvironment
|
||||
import io.ktor.util.pipeline.ContextDsl
|
||||
import io.ktor.util.pipeline.PipelineContext
|
||||
import io.ktor.util.pipeline.PipelineInterceptor
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.contact.sendMessage
|
||||
import net.mamoe.mirai.utils.DefaultLogger
|
||||
import net.mamoe.mirai.utils.io.hexToBytes
|
||||
import net.mamoe.mirai.utils.io.hexToUBytes
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val logger = DefaultLogger("Mirai HTTP API")
|
||||
//write default first
|
||||
SessionManager.authKey = generateSessionKey()//用于验证的key, 使用和SessionKey相同的方法生成, 但意义不同
|
||||
var port = 8080//start port
|
||||
|
||||
args.forEach {
|
||||
if(it.contains("=")) {
|
||||
when {
|
||||
it.toLowerCase().contains("authkey") -> {
|
||||
SessionManager.authKey = it.split("=")[1].trim()
|
||||
if(it.length !in 8..128){
|
||||
logger.error("Expected authKey length is between 8 to 128")
|
||||
SessionManager.authKey = generateSessionKey()
|
||||
}
|
||||
logger.info("Session Auth Key now is ${SessionManager.authKey}")
|
||||
}
|
||||
it.toLowerCase().contains("port") -> {
|
||||
try {
|
||||
port = it.split("=")[1].trim().toInt()
|
||||
}catch (e:Exception){
|
||||
logger.error("Expected -port=xxxxx, xxxxx to be numbers")
|
||||
}
|
||||
if(port !in 1025..65535){
|
||||
logger.error("Expected -port=xxxxx, xxxxx > 1024 && <65536")
|
||||
port = 8080
|
||||
}
|
||||
logger.info("HTTP API Listening port now is $port")
|
||||
}
|
||||
}
|
||||
}
|
||||
if(it.contains("help")){
|
||||
logger.info("-authkey=XXXXXXXX to use custom Session Auth Key, note that key is case sensitive")
|
||||
logger.info("-port=XXXXX to use custom listener port, default using 8080")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Application(applicationEngineEnvironment {}).apply { mirai() }
|
||||
}
|
||||
|
||||
@UseExperimental(ExperimentalUnsignedTypes::class)
|
||||
fun Application.mirai() {
|
||||
install(DefaultHeaders)
|
||||
install(CallLogging)
|
||||
|
||||
routing {
|
||||
mirai("/sendFriendMessage") {
|
||||
// TODO: 2019/11/21 解析图片消息等为 Message
|
||||
Bot.instanceWhose(qq = param("bot")).getFriend(param("qq")).sendMessage(param<String>("message"))
|
||||
call.ok()
|
||||
}
|
||||
|
||||
mirai("/sendGroupMessage") {
|
||||
Bot.instanceWhose(qq = param("bot")).getGroup(param<Long>("group")).sendMessage(param<String>("message"))
|
||||
call.ok()
|
||||
}
|
||||
|
||||
mirai("/event/message") {
|
||||
// TODO: 2019/11/21
|
||||
Bot.instanceWhose(qq = param("bot"))
|
||||
}
|
||||
|
||||
mirai("/addFriend") {
|
||||
Bot.instanceWhose(qq = param("bot")).addFriend(
|
||||
id = param("qq"),
|
||||
message = paramOrNull("message"),
|
||||
remark = paramOrNull("remark")
|
||||
)
|
||||
|
||||
call.ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ContextDsl
|
||||
private fun Route.mirai(path: String, body: PipelineInterceptor<Unit, ApplicationCall>): Route {
|
||||
return route(path, HttpMethod.Get) {
|
||||
handle {
|
||||
try {
|
||||
this.body(this.subject)
|
||||
} catch (e: IllegalAccessException) {
|
||||
call.respond(HttpStatusCode.BadRequest, e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend inline fun ApplicationCall.ok() = this.respond(HttpStatusCode.OK, "OK")
|
||||
|
||||
/**
|
||||
* 错误请求. 抛出这个异常后将会返回错误给一个请求
|
||||
*/
|
||||
@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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误参数
|
||||
*/
|
||||
class IllegalParamException(message: String) : IllegalAccessException(message)
|
||||
|
||||
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)
|
||||
private inline fun <reified R> PipelineContext<Unit, ApplicationCall>.param(name: String): R = this.paramOrNull(name) ?: illegalParam(R::class.simpleName, name)
|
||||
|
||||
@Suppress("IMPLICIT_CAST_TO_ANY")
|
||||
@UseExperimental(ExperimentalUnsignedTypes::class)
|
||||
private 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()
|
||||
|
||||
UByteArray::class -> call.parameters[name]?.hexToUBytes()
|
||||
ByteArray::class -> call.parameters[name]?.hexToBytes()
|
||||
else -> error(name::class.simpleName + " is not supported")
|
||||
} as R?
|
@ -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
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import java.lang.StringBuilder
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.api.http.queue.MessageQueue
|
||||
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.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(session: Session) = closeSession(session.key)
|
||||
@ -69,7 +75,7 @@ abstract class Session internal constructor(
|
||||
val key:String = generateSessionKey()
|
||||
|
||||
|
||||
internal fun close(){
|
||||
internal open fun close(){
|
||||
supervisorJob.complete()
|
||||
}
|
||||
}
|
||||
@ -81,19 +87,26 @@ abstract class Session internal constructor(
|
||||
*
|
||||
* TempSession在建立180s内没有转变为[AuthedSession]应被清除
|
||||
*/
|
||||
class TempSession internal constructor(coroutineContext: CoroutineContext) : Session(coroutineContext) {
|
||||
|
||||
}
|
||||
class TempSession internal constructor(coroutineContext: CoroutineContext) : Session(coroutineContext)
|
||||
|
||||
/**
|
||||
* 任何[TempSession]认证后转化为一个[AuthedSession]
|
||||
* 在这一步[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) } // this aka messagePacket
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
_listener.complete()
|
||||
super.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,6 @@
|
||||
package net.mamoe.mirai.api.http.dto
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class AuthDTO(val authKey: 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,54 @@
|
||||
package net.mamoe.mirai.api.http.dto
|
||||
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
|
||||
interface DTO
|
||||
|
||||
// 解析失败时直接返回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(MessagePacketDTO.serializer()) {
|
||||
GroupMessagePacketDTO::class with GroupMessagePacketDTO.serializer()
|
||||
FriendMessagePacketDTO::class with FriendMessagePacketDTO.serializer()
|
||||
UnKnownMessagePacketDTO::class with UnKnownMessagePacketDTO.serializer()
|
||||
}
|
||||
polymorphic(MessageDTO.serializer()) {
|
||||
AtDTO::class with AtDTO.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()
|
||||
}
|
||||
})
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
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.*
|
||||
import net.mamoe.mirai.utils.MiraiInternalAPI
|
||||
|
||||
/*
|
||||
* DTO data class
|
||||
* */
|
||||
|
||||
// MessagePacket
|
||||
@Serializable
|
||||
@SerialName("FriendMessage")
|
||||
data class FriendMessagePacketDTO(val sender: QQDTO) : MessagePacketDTO()
|
||||
|
||||
@Serializable
|
||||
@SerialName("GroupMessage")
|
||||
data class GroupMessagePacketDTO(val sender: MemberDTO) : MessagePacketDTO()
|
||||
|
||||
@Serializable
|
||||
@SerialName("UnKnownMessage")
|
||||
data class UnKnownMessagePacketDTO(val msg: String) : MessagePacketDTO()
|
||||
|
||||
// Message
|
||||
@Serializable
|
||||
@SerialName("At")
|
||||
data class AtDTO(val target: Long, val display: String) : MessageDTO()
|
||||
@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 path: String) : MessageDTO()
|
||||
@Serializable
|
||||
@SerialName("Xml")
|
||||
data class XmlDTO(val xml: String) : MessageDTO()
|
||||
@Serializable
|
||||
@SerialName("Unknown")
|
||||
data class UnknownMessageDTO(val text: String) : MessageDTO()
|
||||
|
||||
/*
|
||||
* Abstract Class
|
||||
* */
|
||||
@Serializable
|
||||
sealed class MessagePacketDTO : DTO {
|
||||
lateinit var messageChain : MessageChainDTO
|
||||
}
|
||||
|
||||
typealias MessageChainDTO = Array<MessageDTO>
|
||||
|
||||
@Serializable
|
||||
sealed class MessageDTO : DTO
|
||||
|
||||
|
||||
/*
|
||||
Extend function
|
||||
*/
|
||||
suspend fun MessagePacket<*, *>.toDTO(): MessagePacketDTO = when (this) {
|
||||
is FriendMessage -> FriendMessagePacketDTO(QQDTO(sender))
|
||||
is GroupMessage -> GroupMessagePacketDTO(MemberDTO(sender, senderName))
|
||||
else -> UnKnownMessagePacketDTO("UnKnown Message Packet")
|
||||
}.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 -> AtDTO(target, display)
|
||||
is Face -> FaceDTO(id.value.toInt())
|
||||
is PlainText -> PlainDTO(stringValue)
|
||||
is Image -> ImageDTO(this.toString())
|
||||
is XMLMessage -> XmlDTO(stringValue)
|
||||
else -> UnknownMessageDTO("未知消息类型")
|
||||
}
|
||||
|
||||
@UseExperimental(ExperimentalUnsignedTypes::class, MiraiInternalAPI::class)
|
||||
fun MessageDTO.toMessage() = when (this) {
|
||||
is AtDTO -> At(target, display)
|
||||
is FaceDTO -> Face(FaceId(faceID.toUByte()))
|
||||
is PlainDTO -> PlainText(text)
|
||||
is ImageDTO -> PlainText("[暂时不支持图片]")
|
||||
is XmlDTO -> XMLMessage(xml)
|
||||
is UnknownMessageDTO -> PlainText("assert cannot reach")
|
||||
}
|
||||
|
||||
|
||||
|
@ -0,0 +1,40 @@
|
||||
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()
|
||||
|
||||
|
||||
@Serializable
|
||||
open class StateCode(val code: Int, var msg: String) {
|
||||
object Success : StateCode(0, "success") // 成功
|
||||
object NoBot : StateCode(2, "指定Bot不存在")
|
||||
object IllegalSession : StateCode(3, "Session失效或不存在")
|
||||
object NotVerifySession : StateCode(4, "Session未认证")
|
||||
object NoElement : StateCode(5, "指定对象不存在")
|
||||
|
||||
// KS bug: 主构造器中不能有非字段参数 https://github.com/Kotlin/kotlinx.serialization/issues/575
|
||||
@Serializable
|
||||
class IllegalAccess() : StateCode(400, "") { // 非法访问
|
||||
constructor(msg: String) : this() {
|
||||
this.msg = 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.respondStateCode(StateCode(1, "Auth Key错误"))
|
||||
} else {
|
||||
call.respondStateCode(StateCode(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.respondStateCode(StateCode.Success)
|
||||
} catch (e: NoSuchElementException) {
|
||||
call.respondStateCode(StateCode.NoBot)
|
||||
}
|
||||
}
|
||||
|
||||
miraiVerify<BindDTO>("/release") {
|
||||
SessionManager.closeSession(it.sessionKey)
|
||||
call.respondStateCode(StateCode.Success)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,196 @@
|
||||
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.respondStateCode(StateCode.IllegalSession)
|
||||
} catch (e: NotVerifiedSessionException) {
|
||||
call.respondStateCode(StateCode.NotVerifySession)
|
||||
} catch (e: NoSuchElementException) {
|
||||
call.respondStateCode(StateCode.NoElement)
|
||||
} catch (e: IllegalAccessException) {
|
||||
call.respondStateCode(StateCode(400, e.message), HttpStatusCode.BadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
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)
|
||||
|
||||
|
||||
/**
|
||||
* 错误请求. 抛出这个异常后将会返回错误给一个请求
|
||||
*/
|
||||
@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,37 @@
|
||||
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.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.getFriend(it.target).sendMessage(it.messageChain.toMessageChain())
|
||||
call.respondStateCode(StateCode.Success)
|
||||
}
|
||||
|
||||
miraiVerify<SendDTO>("/sendGroupMessage") {
|
||||
it.session.bot.getGroup(it.target).sendMessage(it.messageChain.toMessageChain())
|
||||
call.respondStateCode(StateCode.Success)
|
||||
}
|
||||
|
||||
miraiVerify<VerifyDTO>("/event/message") {
|
||||
|
||||
}
|
||||
|
||||
miraiVerify<VerifyDTO>("/addFriend") {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user