Commit ad710766 authored by jiahua.liu's avatar jiahua.liu

Merge remote-tracking branch 'origin/master'

parents 5c04ba44 76450c87
......@@ -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
......@@ -89,18 +89,18 @@ internal class MemberImpl(
} else if (myPermission == MemberPermission.MEMBER) {
return false
}
try {
return try {
bot.network.run {
val response = TroopManagement.Mute(
TroopManagement.Mute(
client = bot.client,
groupCode = group.id,
memberUin = this@MemberImpl.id,
timeInSecond = durationSeconds
).sendAndExpect<TroopManagement.Mute.Response>()
}
return true
true
} catch (e: Exception) {
return false
false
}
}
......
......@@ -90,8 +90,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
is LoginPacket.LoginPacketResponse.DeviceLockLogin -> {
response = LoginPacket.SubCommand20(
bot.client,
response.t402,
response.t403
response.t402
).sendAndExpect()
continue@mainloop
}
......
......@@ -28,8 +28,8 @@ internal data class RequestPushNotify(
@Suppress("ArrayInDataClass")
@Serializable
internal data class MsgInfo(
@SerialId(0) val lFromUin: Long = 0L,
@SerialId(1) val uMsgTime: Long = 0L,
@SerialId(0) val lFromUin: Long? = 0L,
@SerialId(1) val uMsgTime: Long? = 0L,
@SerialId(2) val shMsgType: Short,
@SerialId(3) val shMsgSeq: Short,
@SerialId(4) val strMsg: String?,
......
......@@ -361,7 +361,7 @@ internal object KnownPacketFactories {
}
@UseExperimental(ExperimentalContracts::class)
internal inline fun <I : IoBuffer, R> I.withUse(block: I.() -> R): R {
internal inline fun <R> IoBuffer.withUse(block: IoBuffer.() -> R): R {
contract {
callsInPlace(block, kotlin.contracts.InvocationKind.EXACTLY_ONCE)
}
......@@ -373,7 +373,7 @@ internal inline fun <I : IoBuffer, R> I.withUse(block: I.() -> R): R {
}
@UseExperimental(ExperimentalContracts::class)
internal inline fun <I : ByteReadPacket, R> I.withUse(block: I.() -> R): R {
internal inline fun <R> ByteReadPacket.withUse(block: ByteReadPacket.() -> R): R {
contract {
callsInPlace(block, kotlin.contracts.InvocationKind.EXACTLY_ONCE)
}
......
......@@ -146,8 +146,7 @@ fun BytePacketBuilder.t116(
fun BytePacketBuilder.t100(
appId: Long = 16,
subAppId: Long = 537062845,
appClientVersion: Int,
sigMap: Int
appClientVersion: Int
) {
writeShort(0x100)
writeShortLVPacket {
......@@ -156,7 +155,7 @@ fun BytePacketBuilder.t100(
writeInt(appId.toInt())
writeInt(subAppId.toInt())
writeInt(appClientVersion)
writeInt(34869472) // 34869472?
writeInt(34869472) // sigMap, 34869472?
} shouldEqualsTo 22
}
......
......@@ -72,8 +72,7 @@ internal object LoginPacket : OutgoingPacketFactory<LoginPacket.LoginPacketRespo
@UseExperimental(MiraiInternalAPI::class)
operator fun invoke(
client: QQAndroidClient,
t402: ByteArray,
t403: ByteArray
t402: ByteArray
): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) {
writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) {
......@@ -96,10 +95,7 @@ internal object LoginPacket : OutgoingPacketFactory<LoginPacket.LoginPacketRespo
private const val subAppId = 537062845L
@UseExperimental(MiraiInternalAPI::class)
operator fun invoke(
client: QQAndroidClient,
t174: ByteArray,
t402: ByteArray,
phoneNumber: String
client: QQAndroidClient
): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId, unknownHex = "01 00 00 00 00 00 00 00 00 00 01 00") {
writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) {
......@@ -163,7 +159,7 @@ internal object LoginPacket : OutgoingPacketFactory<LoginPacket.LoginPacketRespo
if (ConfigManager.get_loginWithPicSt()) appIdList = longArrayOf(1600000226L)
*/
t116(client.miscBitMap, client.subSigMap)
t100(appId, subAppId, client.appClientVersion, client.mainSigMap or 0xC0)
t100(appId, subAppId, client.appClientVersion)
t107(0)
// t108(byteArrayOf())
......@@ -310,7 +306,7 @@ internal object LoginPacket : OutgoingPacketFactory<LoginPacket.LoginPacketRespo
@UseExperimental(MiraiDebugAPI::class)
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): LoginPacketResponse {
val subCommand = readUShort().toInt()
discardExact(2) // subCommand
// println("subCommand=$subCommand")
val type = readUByte()
// println("type=$type")
......@@ -703,7 +699,7 @@ internal object LoginPacket : OutgoingPacketFactory<LoginPacket.LoginPacketRespo
private fun QQAndroidClient.analysisTlv149(t149: ByteArray): LoginPacketResponse.Error {
return t149.read {
val type: Short = readShort()
discardExact(2) //type
val title: String = readUShortLVString()
val content: String = readUShortLVString()
val otherInfo: String = readUShortLVString()
......
package net.mamoe.mirai.qqandroid.network.protocol.packet.login
import kotlinx.io.core.ByteReadPacket
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.network.QQAndroidClient
import net.mamoe.mirai.qqandroid.network.protocol.packet.*
internal object TransEmpPacket : OutgoingPacketFactory<TransEmpPacket.Response>("wtlogin.trans_emp") {
private const val appId = 16L
private const val subAppId = 537062845L
@Suppress("FunctionName")
fun SubCommand1(
client: QQAndroidClient
): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) {
writeOicqRequestPacket(client, EncryptMethodECDH135(client.ecdh), TODO()) {
// oicq.wlogin_sdk.request.trans_emp_1#packTransEmpBody
}
}
object Response : Packet
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
TODO("not implemented")
}
}
\ No newline at end of file
......@@ -175,7 +175,7 @@ fun ByteReadPacket.analysisOneFullPacket(): ByteReadPacket = debugIfFail("Failed
readShort().toInt().takeIf { it != 8001 }?.let {
println("这个包不是 oicqRequest")
return@debugIfFail this
println(" got new protocolVersion=$it")
//println(" got new protocolVersion=$it")
}
val commandId = readUShort().toInt()
println(" commandId=0x${commandId.toShort().toUHexString()}")
......
......@@ -11,7 +11,7 @@ import net.mamoe.mirai.utils.MiraiInternalAPI
*/
class At @MiraiInternalAPI constructor(val target: Long, val display: String) : Message {
@UseExperimental(MiraiInternalAPI::class)
constructor(member: Member) : this(member.id, member.groupCard)
constructor(member: Member) : this(member.id, "@${member.groupCard}")
override fun toString(): String = display
......
......@@ -76,7 +76,7 @@ suspend fun main() {
startsWith("profile", removePrefix = true) {
val account = it.trim()
if (account.isNotEmpty()) {
account.toLong().qq()
bot.getFriend(account.toLong())
} else {
sender
}.queryProfile().toString().reply()
......
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