Commit 76450c87 authored by Him188's avatar Him188 Committed by GitHub

Merge pull request #37 from ryoii/master

http-api
parents 1ae688a0 94c3e2f5
......@@ -6,9 +6,8 @@ Mirai-API-http 提供HTTP API供所有语言使用mirai<br>
### 开始会话-认证(Authorize)
```php
路径: /auth
方法: POST
```
[POST] /auth
```
使用此方法验证你的会话连接, 并将这个会话绑定一个BOT<br>
注意: 每个会话只能绑定一个BOT.
......@@ -25,31 +24,29 @@ Mirai-API-http 提供HTTP API供所有语言使用mirai<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 |
| 名字 | 值 |
| --- | --- |
| 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")
}
}
......
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")
}
}
}
\ No newline at end of file
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()
}
}
package net.mamoe.mirai.api.http.dto
import kotlinx.serialization.Serializable
@Serializable
data class AuthDTO(val authKey: String) : DTO
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)
\ No newline at end of file
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()
}
})
}
\ No newline at end of file
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")
}
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()
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
}
}
\ No newline at end of file
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)
}
}
}
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") {
}
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment