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> ...@@ -6,9 +6,8 @@ Mirai-API-http 提供HTTP API供所有语言使用mirai<br>
### 开始会话-认证(Authorize) ### 开始会话-认证(Authorize)
```php ```
路径: /auth [POST] /auth
方法: POST
``` ```
使用此方法验证你的会话连接, 并将这个会话绑定一个BOT<br> 使用此方法验证你的会话连接, 并将这个会话绑定一个BOT<br>
注意: 每个会话只能绑定一个BOT. 注意: 每个会话只能绑定一个BOT.
...@@ -25,31 +24,29 @@ Mirai-API-http 提供HTTP API供所有语言使用mirai<br> ...@@ -25,31 +24,29 @@ Mirai-API-http 提供HTTP API供所有语言使用mirai<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 { ...@@ -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")
} }
} }
......
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 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 { ...@@ -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( ...@@ -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()
} }
} }
...@@ -81,19 +87,26 @@ abstract class Session internal constructor( ...@@ -81,19 +87,26 @@ abstract class Session internal constructor(
* *
* TempSession在建立180s内没有转变为[AuthedSession]应被清除 * TempSession在建立180s内没有转变为[AuthedSession]应被清除
*/ */
class TempSession internal constructor(coroutineContext: CoroutineContext) : Session(coroutineContext) { class TempSession internal constructor(coroutineContext: CoroutineContext) : Session(coroutineContext)
}
/** /**
* 任何[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) } // 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 package net.mamoe.mirai.api.http.route
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.ApplicationCall import io.ktor.application.ApplicationCall
...@@ -6,129 +6,131 @@ import io.ktor.application.call ...@@ -6,129 +6,131 @@ import io.ktor.application.call
import io.ktor.application.install import io.ktor.application.install
import io.ktor.features.CallLogging import io.ktor.features.CallLogging
import io.ktor.features.DefaultHeaders import io.ktor.features.DefaultHeaders
import io.ktor.http.ContentType
import io.ktor.http.HttpMethod import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.response.respond 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.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.ContextDsl
import io.ktor.util.pipeline.PipelineContext import io.ktor.util.pipeline.PipelineContext
import io.ktor.util.pipeline.PipelineInterceptor import net.mamoe.mirai.api.http.AuthedSession
import net.mamoe.mirai.Bot import net.mamoe.mirai.api.http.SessionManager
import net.mamoe.mirai.contact.sendMessage import net.mamoe.mirai.api.http.TempSession
import net.mamoe.mirai.utils.DefaultLogger import net.mamoe.mirai.api.http.dto.*
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() { fun Application.mirai() {
install(DefaultHeaders) install(DefaultHeaders)
install(CallLogging) install(CallLogging)
routing { authModule()
mirai("/sendFriendMessage") { messageModule()
// 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")) * Auth,处理http server的验证
call.ok() * 为闭包传入一个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)
} }
mirai("/event/message") {
// TODO: 2019/11/21
Bot.instanceWhose(qq = param("bot"))
} }
}
mirai("/addFriend") { /**
Bot.instanceWhose(qq = param("bot")).addFriend( * Get,用于获取bot的属性
id = param("qq"), * 验证请求参数中sessionKey参数的有效性
message = paramOrNull("message"), */
remark = paramOrNull("remark") @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
call.ok() 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 @ContextDsl
private fun Route.mirai(path: String, body: PipelineInterceptor<Unit, ApplicationCall>): Route { internal inline fun <reified T : VerifyDTO> Route.miraiVerify(
return route(path, HttpMethod.Get) { path: String,
handle { verifiedSessionKey: Boolean = true,
try { crossinline body: suspend PipelineContext<Unit, ApplicationCall>.(T) -> Unit
this.body(this.subject) ): Route {
} catch (e: IllegalAccessException) { return route(path, HttpMethod.Post) {
call.respond(HttpStatusCode.BadRequest, e.message) 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)
} }
} }
} }
suspend inline fun ApplicationCall.ok() = this.respond(HttpStatusCode.OK, "OK")
/** /**
* 错误请求. 抛出这个异常后将会返回错误给一个请求 * 统一捕获并处理异常
*/ */
@Suppress("unused") internal inline fun Route.intercept(crossinline blk: suspend PipelineContext<Unit, ApplicationCall>.() -> Unit) = handle {
open class IllegalAccessException : Exception { try {
override val message: String get() = super.message!! blk(this)
} catch (e: IllegalSessionException) {
constructor(message: String) : super(message, null) call.respondStateCode(StateCode.IllegalSession)
constructor(cause: Throwable) : super(cause.toString(), cause) } catch (e: NotVerifiedSessionException) {
constructor(message: String, cause: Throwable?) : super(message, cause) call.respondStateCode(StateCode.NotVerifySession)
} catch (e: NoSuchElementException) {
call.respondStateCode(StateCode.NoElement)
} catch (e: IllegalAccessException) {
call.respondStateCode(StateCode(400, e.message), HttpStatusCode.BadRequest)
}
} }
/** /*
* 错误参数 extend function
*/ */
class IllegalParamException(message: String) : IllegalAccessException(message) 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( fun PipelineContext<Unit, ApplicationCall>.illegalParam(
expectingType: String?, expectingType: String?,
...@@ -138,11 +140,7 @@ fun PipelineContext<Unit, ApplicationCall>.illegalParam( ...@@ -138,11 +140,7 @@ fun PipelineContext<Unit, ApplicationCall>.illegalParam(
@Suppress("IMPLICIT_CAST_TO_ANY") @Suppress("IMPLICIT_CAST_TO_ANY")
@UseExperimental(ExperimentalUnsignedTypes::class) @UseExperimental(ExperimentalUnsignedTypes::class)
private inline fun <reified R> PipelineContext<Unit, ApplicationCall>.param(name: String): R = this.paramOrNull(name) ?: illegalParam(R::class.simpleName, name) internal inline fun <reified R> PipelineContext<Unit, ApplicationCall>.paramOrNull(name: String): R =
@Suppress("IMPLICIT_CAST_TO_ANY")
@UseExperimental(ExperimentalUnsignedTypes::class)
private inline fun <reified R> PipelineContext<Unit, ApplicationCall>.paramOrNull(name: String): R? =
when (R::class) { when (R::class) {
Byte::class -> call.parameters[name]?.toByte() Byte::class -> call.parameters[name]?.toByte()
Int::class -> call.parameters[name]?.toInt() Int::class -> call.parameters[name]?.toInt()
...@@ -165,7 +163,34 @@ private inline fun <reified R> PipelineContext<Unit, ApplicationCall>.paramOrNul ...@@ -165,7 +163,34 @@ private inline fun <reified R> PipelineContext<Unit, ApplicationCall>.paramOrNul
UInt::class -> call.parameters[name]?.toUInt() UInt::class -> call.parameters[name]?.toUInt()
UShort::class -> call.parameters[name]?.toUShort() 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") else -> error(name::class.simpleName + " is not supported")
} as R? } 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)
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( ...@@ -89,18 +89,18 @@ internal class MemberImpl(
} else if (myPermission == MemberPermission.MEMBER) { } else if (myPermission == MemberPermission.MEMBER) {
return false return false
} }
try { return try {
bot.network.run { bot.network.run {
val response = TroopManagement.Mute( TroopManagement.Mute(
client = bot.client, client = bot.client,
groupCode = group.id, groupCode = group.id,
memberUin = this@MemberImpl.id, memberUin = this@MemberImpl.id,
timeInSecond = durationSeconds timeInSecond = durationSeconds
).sendAndExpect<TroopManagement.Mute.Response>() ).sendAndExpect<TroopManagement.Mute.Response>()
} }
return true true
} catch (e: Exception) { } catch (e: Exception) {
return false false
} }
} }
......
...@@ -90,8 +90,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler ...@@ -90,8 +90,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
is LoginPacket.LoginPacketResponse.DeviceLockLogin -> { is LoginPacket.LoginPacketResponse.DeviceLockLogin -> {
response = LoginPacket.SubCommand20( response = LoginPacket.SubCommand20(
bot.client, bot.client,
response.t402, response.t402
response.t403
).sendAndExpect() ).sendAndExpect()
continue@mainloop continue@mainloop
} }
......
...@@ -28,8 +28,8 @@ internal data class RequestPushNotify( ...@@ -28,8 +28,8 @@ internal data class RequestPushNotify(
@Suppress("ArrayInDataClass") @Suppress("ArrayInDataClass")
@Serializable @Serializable
internal data class MsgInfo( internal data class MsgInfo(
@SerialId(0) val lFromUin: Long = 0L, @SerialId(0) val lFromUin: Long? = 0L,
@SerialId(1) val uMsgTime: Long = 0L, @SerialId(1) val uMsgTime: Long? = 0L,
@SerialId(2) val shMsgType: Short, @SerialId(2) val shMsgType: Short,
@SerialId(3) val shMsgSeq: Short, @SerialId(3) val shMsgSeq: Short,
@SerialId(4) val strMsg: String?, @SerialId(4) val strMsg: String?,
......
...@@ -361,7 +361,7 @@ internal object KnownPacketFactories { ...@@ -361,7 +361,7 @@ internal object KnownPacketFactories {
} }
@UseExperimental(ExperimentalContracts::class) @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 { contract {
callsInPlace(block, kotlin.contracts.InvocationKind.EXACTLY_ONCE) callsInPlace(block, kotlin.contracts.InvocationKind.EXACTLY_ONCE)
} }
...@@ -373,7 +373,7 @@ internal inline fun <I : IoBuffer, R> I.withUse(block: I.() -> R): R { ...@@ -373,7 +373,7 @@ internal inline fun <I : IoBuffer, R> I.withUse(block: I.() -> R): R {
} }
@UseExperimental(ExperimentalContracts::class) @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 { contract {
callsInPlace(block, kotlin.contracts.InvocationKind.EXACTLY_ONCE) callsInPlace(block, kotlin.contracts.InvocationKind.EXACTLY_ONCE)
} }
......
...@@ -146,8 +146,7 @@ fun BytePacketBuilder.t116( ...@@ -146,8 +146,7 @@ fun BytePacketBuilder.t116(
fun BytePacketBuilder.t100( fun BytePacketBuilder.t100(
appId: Long = 16, appId: Long = 16,
subAppId: Long = 537062845, subAppId: Long = 537062845,
appClientVersion: Int, appClientVersion: Int
sigMap: Int
) { ) {
writeShort(0x100) writeShort(0x100)
writeShortLVPacket { writeShortLVPacket {
...@@ -156,7 +155,7 @@ fun BytePacketBuilder.t100( ...@@ -156,7 +155,7 @@ fun BytePacketBuilder.t100(
writeInt(appId.toInt()) writeInt(appId.toInt())
writeInt(subAppId.toInt()) writeInt(subAppId.toInt())
writeInt(appClientVersion) writeInt(appClientVersion)
writeInt(34869472) // 34869472? writeInt(34869472) // sigMap, 34869472?
} shouldEqualsTo 22 } shouldEqualsTo 22
} }
......
...@@ -72,8 +72,7 @@ internal object LoginPacket : OutgoingPacketFactory<LoginPacket.LoginPacketRespo ...@@ -72,8 +72,7 @@ internal object LoginPacket : OutgoingPacketFactory<LoginPacket.LoginPacketRespo
@UseExperimental(MiraiInternalAPI::class) @UseExperimental(MiraiInternalAPI::class)
operator fun invoke( operator fun invoke(
client: QQAndroidClient, client: QQAndroidClient,
t402: ByteArray, t402: ByteArray
t403: ByteArray
): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId -> ): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) { writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) {
writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) { writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) {
...@@ -96,10 +95,7 @@ internal object LoginPacket : OutgoingPacketFactory<LoginPacket.LoginPacketRespo ...@@ -96,10 +95,7 @@ internal object LoginPacket : OutgoingPacketFactory<LoginPacket.LoginPacketRespo
private const val subAppId = 537062845L private const val subAppId = 537062845L
@UseExperimental(MiraiInternalAPI::class) @UseExperimental(MiraiInternalAPI::class)
operator fun invoke( operator fun invoke(
client: QQAndroidClient, client: QQAndroidClient
t174: ByteArray,
t402: ByteArray,
phoneNumber: String
): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId -> ): 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") { 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) { writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) {
...@@ -163,7 +159,7 @@ internal object LoginPacket : OutgoingPacketFactory<LoginPacket.LoginPacketRespo ...@@ -163,7 +159,7 @@ internal object LoginPacket : OutgoingPacketFactory<LoginPacket.LoginPacketRespo
if (ConfigManager.get_loginWithPicSt()) appIdList = longArrayOf(1600000226L) if (ConfigManager.get_loginWithPicSt()) appIdList = longArrayOf(1600000226L)
*/ */
t116(client.miscBitMap, client.subSigMap) t116(client.miscBitMap, client.subSigMap)
t100(appId, subAppId, client.appClientVersion, client.mainSigMap or 0xC0) t100(appId, subAppId, client.appClientVersion)
t107(0) t107(0)
// t108(byteArrayOf()) // t108(byteArrayOf())
...@@ -310,7 +306,7 @@ internal object LoginPacket : OutgoingPacketFactory<LoginPacket.LoginPacketRespo ...@@ -310,7 +306,7 @@ internal object LoginPacket : OutgoingPacketFactory<LoginPacket.LoginPacketRespo
@UseExperimental(MiraiDebugAPI::class) @UseExperimental(MiraiDebugAPI::class)
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): LoginPacketResponse { override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): LoginPacketResponse {
val subCommand = readUShort().toInt() discardExact(2) // subCommand
// println("subCommand=$subCommand") // println("subCommand=$subCommand")
val type = readUByte() val type = readUByte()
// println("type=$type") // println("type=$type")
...@@ -703,7 +699,7 @@ internal object LoginPacket : OutgoingPacketFactory<LoginPacket.LoginPacketRespo ...@@ -703,7 +699,7 @@ internal object LoginPacket : OutgoingPacketFactory<LoginPacket.LoginPacketRespo
private fun QQAndroidClient.analysisTlv149(t149: ByteArray): LoginPacketResponse.Error { private fun QQAndroidClient.analysisTlv149(t149: ByteArray): LoginPacketResponse.Error {
return t149.read { return t149.read {
val type: Short = readShort() discardExact(2) //type
val title: String = readUShortLVString() val title: String = readUShortLVString()
val content: String = readUShortLVString() val content: String = readUShortLVString()
val otherInfo: 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 ...@@ -175,7 +175,7 @@ fun ByteReadPacket.analysisOneFullPacket(): ByteReadPacket = debugIfFail("Failed
readShort().toInt().takeIf { it != 8001 }?.let { readShort().toInt().takeIf { it != 8001 }?.let {
println("这个包不是 oicqRequest") println("这个包不是 oicqRequest")
return@debugIfFail this return@debugIfFail this
println(" got new protocolVersion=$it") //println(" got new protocolVersion=$it")
} }
val commandId = readUShort().toInt() val commandId = readUShort().toInt()
println(" commandId=0x${commandId.toShort().toUHexString()}") println(" commandId=0x${commandId.toShort().toUHexString()}")
......
...@@ -11,7 +11,7 @@ import net.mamoe.mirai.utils.MiraiInternalAPI ...@@ -11,7 +11,7 @@ import net.mamoe.mirai.utils.MiraiInternalAPI
*/ */
class At @MiraiInternalAPI constructor(val target: Long, val display: String) : Message { class At @MiraiInternalAPI constructor(val target: Long, val display: String) : Message {
@UseExperimental(MiraiInternalAPI::class) @UseExperimental(MiraiInternalAPI::class)
constructor(member: Member) : this(member.id, member.groupCard) constructor(member: Member) : this(member.id, "@${member.groupCard}")
override fun toString(): String = display override fun toString(): String = display
......
...@@ -76,7 +76,7 @@ suspend fun main() { ...@@ -76,7 +76,7 @@ suspend fun main() {
startsWith("profile", removePrefix = true) { startsWith("profile", removePrefix = true) {
val account = it.trim() val account = it.trim()
if (account.isNotEmpty()) { if (account.isNotEmpty()) {
account.toLong().qq() bot.getFriend(account.toLong())
} else { } else {
sender sender
}.queryProfile().toString().reply() }.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