Quellcode durchsuchen

Serializer impl and test

Serializer impl and test

Serializer impl and test

Serializer impl and test

tmp
ryoii vor 4 Jahren
Ursprung
Commit
fda8fa6f39
89 geänderte Dateien mit 2802 neuen und 3148 gelöschten Zeilen
  1. 2 1
      .gitignore
  2. 1 1
      CHANGELOG.md
  3. 0 27
      build.gradle
  4. 29 0
      build.gradle.kts
  5. 11 8
      gradle.properties
  6. 1 1
      gradle/wrapper/gradle-wrapper.properties
  7. 3 0
      launcher.properties
  8. 26 18
      mirai-api-http/build.gradle.kts
  9. 24 89
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/HttpApiPluginBase.kt
  10. 66 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/MahPluginImpl.kt
  11. 0 112
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/MiraiHttpAPIServer.kt
  12. 0 188
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/Session.kt
  13. 24 2
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/MahAdapter.kt
  14. 28 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/MahAdapterFactory.kt
  15. 112 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/MahKtorAdapter.kt
  16. 5 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/common/README.md
  17. 2 3
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/common/StateCode.kt
  18. 1 1
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/common/exception.kt
  19. 42 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/common/exceptionHandle.kt
  20. 21 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/HttpAdapter.kt
  21. 34 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/about.kt
  22. 41 61
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/base.kt
  23. 194 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/command.kt
  24. 59 67
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/dsl.kt
  25. 61 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/event.kt
  26. 117 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/group.kt
  27. 73 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/info.kt
  28. 242 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/message.kt
  29. 60 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/verify.kt
  30. 14 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/session/HttpAuthedSession.kt
  31. 38 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/session/UnreadQueue.kt
  32. 5 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/README.md
  33. 80 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/convertor/convertor.kt
  34. 139 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/convertor/event.kt
  35. 58 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/convertor/message.kt
  36. 23 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/dto/auth.kt
  37. 4 13
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/dto/base.kt
  38. 3 13
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/dto/contact.kt
  39. 1 146
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/dto/event.kt
  40. 121 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/dto/message.kt
  41. 111 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/dto/parameter.kt
  42. 38 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/dto/restful.kt
  43. 26 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/serializer/extensions.kt
  44. 40 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/serializer/internalSerializer.kt
  45. 61 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/serializer/json.kt
  46. 110 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/webhook/ReportService.kt
  47. 20 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/ws/WebsocketAdapter.kt
  48. 86 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/ws/router/base.kt
  49. 0 29
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/cache/Cache.kt
  50. 0 73
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/cache/FixedCache.kt
  51. 0 8
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/cache/IndexLinkedHashMap.kt
  52. 0 89
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/cache/LinkedCache.kt
  53. 0 20
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/command/RegisterCommand.kt
  54. 0 104
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/config/Setting.kt
  55. 83 6
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/context/MahContext.kt
  56. 15 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/context/cache/MessageSourceCache.kt
  57. 11 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/context/serializer/InternalSerializerHolder.kt
  58. 51 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/context/session/manager/api.kt
  59. 18 13
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/context/session/manager/default.kt
  60. 9 11
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/context/session/manager/util.kt
  61. 23 32
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/context/session/session.kt
  62. 0 35
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/Config.kt
  63. 0 257
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/common/MessageDTO.kt
  64. 0 53
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/common/RestfulResult.kt
  65. 0 25
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/queue/CacheQueue.kt
  66. 0 81
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/queue/MessageQueue.kt
  67. 0 80
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/AuthRouteModule.kt
  68. 0 197
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/CommandRouteModule.kt
  69. 0 79
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/ConfRouteModule.kt
  70. 0 96
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/EventRouteModule.kt
  71. 0 186
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/GroupManageRouteModule.kt
  72. 0 79
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/InfoRouteModule.kt
  73. 0 359
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/MessageRouteModule.kt
  74. 0 134
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/WebSocketRouteModule.kt
  75. 0 38
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/service/MiraiApiHttpService.kt
  76. 0 41
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/service/MiraiApiHttpServices.kt
  77. 0 74
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/service/heartbeat/HeartBeatService.kt
  78. 0 111
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/service/report/ReportService.kt
  79. 123 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/setting/MainSetting.kt
  80. 0 70
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/util/Json.kt
  81. 11 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/util/bot.kt
  82. 41 0
      mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/util/extends.kt
  83. 2 2
      mirai-api-http/src/test/kotlin/mirai/RunMirai.kt
  84. 44 0
      mirai-api-http/src/test/kotlin/net/mamoe/mirai/api/http/adapter/launch/HttpAdapterLaunch.kt
  85. 19 0
      mirai-api-http/src/test/kotlin/net/mamoe/mirai/api/http/adapter/launch/LaunchTester.kt
  86. 56 0
      mirai-api-http/src/test/kotlin/net/mamoe/mirai/api/http/adapter/serialization/SerializationTest.kt
  87. 24 0
      mirai-api-http/src/test/kotlin/net/mamoe/mirai/api/http/adapter/serialization/factory.kt
  88. 0 15
      settings.gradle
  89. 15 0
      settings.gradle.kts

+ 2 - 1
.gitignore

@@ -9,4 +9,5 @@ build/
 
 # config
 device.json
-/config/
+launcher.properties
+/config/

+ 1 - 1
CHANGELOG.md

@@ -127,7 +127,7 @@
 
 * 修复配置文件读取错误的问题 @HoshinoTented
 
-> 由于某些原因,如果配置文件无法加载,请将配置文件名称修改为 net.mamoe.mirai.api.http.config.Setting.yaml
+> 由于某些原因,如果配置文件无法加载,请将配置文件名称修改为 net.mamoe.mirai.api.http.setting.Setting.yaml
 
 
 

+ 0 - 27
build.gradle

@@ -1,27 +0,0 @@
-buildscript {
-    repositories {
-        mavenLocal()
-        maven { url 'https://mirrors.huaweicloud.com/repository/maven' }
-        maven { url = "https://dl.bintray.com/kotlin/kotlin-eap" }
-        jcenter()
-        mavenCentral()
-    }
-
-    dependencies {
-        classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
-        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
-        // classpath 'com.github.jengelman.gradle.plugins:shadow:5.2.0'
-    }
-}
-
-allprojects {
-    group = "net.mamoe"
-
-    repositories {
-        mavenLocal()
-        maven { url "https://mirrors.huaweicloud.com/repository/maven" }
-        maven { url = "https://dl.bintray.com/kotlin/kotlin-eap" }
-        jcenter()
-        mavenCentral()
-    }
-}

+ 29 - 0
build.gradle.kts

@@ -0,0 +1,29 @@
+buildscript {
+
+    repositories {
+        mavenLocal()
+        maven(url = "https://maven.aliyun.com/repository/public")
+        maven(url = "https://dl.bintray.com/kotlin/kotlin-eap")
+        mavenCentral()
+        jcenter()
+    }
+
+    val kotlinVersion: String by project.extra
+
+    dependencies {
+        classpath("org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion")
+        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
+    }
+}
+
+allprojects {
+    group = "net.mamoe"
+
+    repositories {
+        mavenLocal()
+        maven(url = "https://maven.aliyun.com/repository/public")
+        maven(url = "https://dl.bintray.com/kotlin/kotlin-eap")
+        mavenCentral()
+        jcenter()
+    }
+}

+ 11 - 8
gradle.properties

@@ -1,14 +1,17 @@
-# build
-httpVersion=1.9.7
 # style guide
 kotlin.code.style=official
 # config
 kotlin.incremental.multiplatform=true
 kotlin.parallel.tasks.in.project=true
+
+
+# build
+httpVersion=1.9.7
+
 # kotlin
-kotlinVersion=1.4.21
-# kotlin libraries
-serializationVersion=1.0.1
-UpcoroutinesVersion=1.4.2
-# utility
-ktorVersion=1.5.0
+kotlinVersion=1.4.30
+serializationVersion=1.1.0
+ktorVersion=1.5.1
+
+# mirai
+miraiVersion=2.5.2

+ 1 - 1
gradle/wrapper/gradle-wrapper.properties

@@ -1,5 +1,5 @@
 #Wed Mar 04 22:27:09 CST 2020
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStorePath=wrapper/dists

+ 3 - 0
launcher.properties

@@ -0,0 +1,3 @@
+enable=false
+qq=
+password=

+ 26 - 18
mirai-api-http/build.gradle.kts

@@ -1,25 +1,18 @@
 plugins {
-    id("kotlinx-serialization")
     kotlin("jvm")
-    id("net.mamoe.mirai-console") version "2.0-RC-dev-1"
+    kotlin("plugin.serialization")
+    id("net.mamoe.mirai-console") version "2.6-RC"
 }
 
-val httpVersion: String by rootProject.ext
-
-val ktorVersion: String by rootProject.ext
-val serializationVersion: String by rootProject.ext
-
-fun kotlinx(id: String, version: String) =
-    "org.jetbrains.kotlinx:kotlinx-$id:$version"
-
-
+val ktorVersion: String by rootProject.extra
+fun kotlinx(id: String, version: String) = "org.jetbrains.kotlinx:kotlinx-$id:$version"
 fun ktor(id: String, version: String = this@Build_gradle.ktorVersion) = "io.ktor:ktor-$id:$version"
 
-
 kotlin {
     sourceSets["test"].apply {
         dependencies {
             api("org.slf4j:slf4j-simple:1.7.26")
+            api(kotlin("test-junit"))
         }
     }
 
@@ -28,25 +21,24 @@ kotlin {
         languageSettings.useExperimentalAnnotation("kotlin.Experimental")
 
         dependencies {
-            api(kotlinx("serialization-json", serializationVersion))
-            implementation("net.mamoe:mirai-core-utils:${mirai.coreVersion}")
 
             api(ktor("server-cio"))
             api(ktor("http-jvm"))
             api(ktor("websockets"))
             api("org.yaml:snakeyaml:1.25")
 
-            implementation(ktor("server-core"))
-            implementation(ktor("http"))
+            api(ktor("server-core"))
+            api(ktor("http"))
         }
     }
 }
 
+val httpVersion: String by rootProject.extra
 project.version = httpVersion
 
 description = "Mirai HTTP API plugin"
 
-internal val EXCLUDED_FILES = listOf(
+internal val excluded = listOf(
     "kotlin-stdlib-.*",
     "kotlin-reflect-.*",
     "kotlinx-serialization-json.*",
@@ -58,7 +50,7 @@ internal val EXCLUDED_FILES = listOf(
 mirai {
     this.configureShadow {
         exclude { elm ->
-            EXCLUDED_FILES.any { it.matches(elm.path) }
+            excluded.any { it.matches(elm.path) }
         }
     }
     publishing {
@@ -80,3 +72,19 @@ tasks.create("buildCiJar", Jar::class) {
     }
 }
 
+dependencies {
+    implementation(kotlin("stdlib-jdk8"))
+}
+
+repositories {
+    mavenCentral()
+}
+
+tasks {
+    compileKotlin {
+        kotlinOptions.jvmTarget = "1.8"
+    }
+    compileTestKotlin {
+        kotlinOptions.jvmTarget = "1.8"
+    }
+}

+ 24 - 89
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/HttpApiPluginBase.kt

@@ -9,114 +9,49 @@
 
 package net.mamoe.mirai.api.http
 
-import kotlinx.coroutines.async
-import net.mamoe.mirai.api.http.config.Setting
-import net.mamoe.mirai.api.http.service.MiraiApiHttpServices
+import net.mamoe.mirai.api.http.adapter.MahAdapter
+import net.mamoe.mirai.api.http.adapter.MahAdapterFactory
+import net.mamoe.mirai.api.http.context.session.manager.DefaultSessionManager
+import net.mamoe.mirai.api.http.setting.MainSetting
 import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription
 import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin
-import java.io.File
-
-internal typealias CommandSubscriber = suspend (String, Long, Long, List<String>) -> Unit
 
+/**
+ * Mirai Console 插件定义
+ *
+ * 主要职责为读取配置文件 [MainSetting] 和 启动具体实现 [MahPluginImpl]
+ */
 object HttpApiPluginBase : KotlinPlugin(
-    JvmPluginDescription(id = "net.mamoe.mirai-api-http", version = "1.9.7") {
+    JvmPluginDescription(id = "net.mamoe.mirai-api-http", version = "2.0-RC1") {
         author("ryoii")
         info("Mirai HTTP API Server Plugin")
     }
 ) {
-    var services: MiraiApiHttpServices = MiraiApiHttpServices(this)
-
     override fun onEnable() {
-        Setting.reload()
+        MainSetting.reload()
 
-        with(Setting) {
+        with(MainSetting) {
 
-            if (authKey.startsWith("INITKEY")) {
+            if (verifyKey.startsWith("INITKEY")) {
                 logger.warning("USING INITIAL KEY, please edit the key")
             }
 
-            logger.info("Starting Mirai HTTP Server in $host:$port")
-            services.onLoad()
-
-            MiraiHttpAPIServer.start(host, port, authKey)
+            // 创建上下文启动 mah 插件
+            MahPluginImpl.start {
+                sessionManager = DefaultSessionManager(verifyKey)
+                enableVerify = [email protected]
+                singleMode = [email protected]
+                localMode = false
 
-            services.onEnable()
+                parseAdapter(modules).forEach(::plus)
+            }
         }
     }
 
     override fun onDisable() {
-        MiraiHttpAPIServer.stop()
-
-        services.onDisable()
-    }
-
-    private val subscribers = mutableListOf<CommandSubscriber>()
-
-    internal fun subscribeCommand(subscriber: CommandSubscriber): CommandSubscriber =
-        subscriber.also { subscribers.add(it) }
-
-    internal fun unSubscribeCommand(subscriber: CommandSubscriber) = subscribers.remove(subscriber)
-
-    // TODO: 解决Http-api插件卸载后,注册的command将失效
-    internal fun registerCommand(
-        names: Array<out String>,
-        description: String,
-        usage: String,
-    ) {
-//        CommandManager.INSTANCE.run {
-//            object : SimpleCommand(HttpApiPluginBase, *names, description = description) {
-//                override val usage: String = usage
-//
-//                @Handler
-//                suspend fun onCommand(target: User, message: String) {
-//                    // TODO
-//                }
-//            }
-//        }
-
-        /* registerCommand {
-        this.name = name
-        this.alias = alias
-        this.description = description
-        this.usage = usage
-
-        this.onCommand {
-                // do nothing
-                true
-            }
-        }*/
+        MahPluginImpl.stop()
     }
 
-//    override suspend fun onCommand(command: Command, sender: CommandSender, args: List<String>) {
-//        launch {
-//            val (from: Long, group: Long) = when (sender) {
-//                is MemberCommandSender -> sender.user.id to sender.user.id
-//                is FriendCommandSender -> sender.user.id to 0L
-//                else -> 0L to 0L // 考虑保留对其他Sender类型的扩展,先统一默认为ConsoleSender
-//            }
-//
-//            subscribers.forEach {
-//                it(command.names.first(), from, group, args)
-//            }
-//        }
-//    }
-
-    private val imageFold: File = File(dataFolder, "images").apply { mkdirs() }
-
-    internal fun image(imageName: String) = File(imageFold, imageName)
-
-    fun saveImageAsync(name: String, data: ByteArray) =
-        async {
-            image(name).apply { writeBytes(data) }
-        }
-
-    private val voiceFold: File = File(dataFolder, "voices").apply { mkdirs() }
-
-    internal fun voice(voiceName: String) = File(voiceFold, voiceName)
-
-    fun saveVoiceAsync(name: String, data: ByteArray) =
-        async {
-            voice(name).apply { writeBytes(data) }
-        }
-
+    private fun parseAdapter(modules: List<String>): List<MahAdapter> =
+        modules.mapNotNull { MahAdapterFactory.build(it) }
 }

+ 66 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/MahPluginImpl.kt

@@ -0,0 +1,66 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.api.http
+
+import io.ktor.util.*
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import net.mamoe.mirai.api.http.adapter.MahAdapter
+import net.mamoe.mirai.api.http.adapter.http.HttpAdapter
+import net.mamoe.mirai.api.http.context.MahContext
+import net.mamoe.mirai.api.http.context.MahContextBuilder
+import net.mamoe.mirai.api.http.context.MahContextHolder
+import net.mamoe.mirai.utils.MiraiLogger
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * Mah 插件的具体实现,与 Console 插件接口解耦令其可独立调试
+ */
+object MahPluginImpl : CoroutineScope {
+    private const val DEFAULT_LOGGER_NAME = "Mirai HTTP API"
+
+    var logger = MiraiLogger.create(DEFAULT_LOGGER_NAME)
+    override val coroutineContext: CoroutineContext =
+        CoroutineExceptionHandler { _, throwable -> logger.error(throwable) }
+
+    @OptIn(KtorExperimentalAPI::class)
+    fun start(builder: MahContextBuilder) {
+
+        builder.run {
+            MahContext().apply {
+                MahContextHolder.mahContext = this
+                invoke()
+            }
+        }
+
+        MahContextHolder.mahContext.adapters.forEach {
+            it.initAdapter()
+        }
+        MahContextHolder.mahContext.adapters.forEach {
+            it.enable()
+        }
+
+        with(MahContextHolder.mahContext) {
+            logger.info("********************************************************")
+            if (enableVerify) {
+                logger.info("Http api server is running with verifyKey: ${sessionManager.verifyKey}")
+            } else {
+                logger.info("Http api server is running out of verify mode")
+            }
+            val list = adapters.joinToString(prefix = "[", separator = ",", postfix = "]") { it.name }
+            logger.info("adaptors: $list")
+            logger.info("********************************************************")
+        }
+
+    }
+
+    fun stop() = MahContextHolder.mahContext.adapters.forEach(MahAdapter::disable)
+}

+ 0 - 112
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/MiraiHttpAPIServer.kt

@@ -1,112 +0,0 @@
-/*
- * Copyright 2020 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/master/LICENSE
- */
-
-package net.mamoe.mirai.api.http
-
-import io.ktor.application.*
-import io.ktor.server.cio.*
-import io.ktor.server.engine.*
-import io.ktor.util.*
-import kotlinx.coroutines.CoroutineExceptionHandler
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-import net.mamoe.mirai.api.http.context.session.SessionManager
-import net.mamoe.mirai.api.http.context.session.manager.generateSessionKey
-import net.mamoe.mirai.api.http.route.mirai
-import net.mamoe.mirai.console.plugin.PluginManager
-import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.description
-import net.mamoe.mirai.utils.MiraiLogger
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
-import java.io.OutputStream
-import java.io.PrintStream
-import kotlin.coroutines.CoroutineContext
-
-object MiraiHttpAPIServer : CoroutineScope {
-    private const val DEFAULT_LOGGER_NAME = "Mirai HTTP API"
-
-    var logger = MiraiLogger.create(DEFAULT_LOGGER_NAME)
-    override val coroutineContext: CoroutineContext =
-        CoroutineExceptionHandler { _, throwable -> logger.error(throwable) }
-
-    lateinit var server: ApplicationEngine
-
-    init {
-        SessionManager.authKey = generateSessionKey()//用于验证的key, 使用和SessionKey相同的方法生成, 但意义不同
-    }
-
-    fun setAuthKey(key: String) {
-        SessionManager.authKey = key
-    }
-
-    @OptIn(KtorExperimentalAPI::class)
-    fun start(
-        host: String = "0.0.0.0",
-        port: Int = 8080,
-        authKey: String,
-        callback: (() -> Unit)? = null
-    ) {
-        require(authKey.length in 8..128) { "Expected authKey length is between 8 to 128" }
-        SessionManager.authKey = authKey
-
-        // TODO: start是无阻塞的,理应获取启动状态后再执行后续代码
-        launch {
-
-            val err = System.err
-
-            val logger = if (PluginManager.plugins.any {
-                    // plugin mode
-                    it.description.id == "net.mamoe.mirai.mirai-slf4j-bridge"
-                } || runCatching {
-                    // library mode
-                    Class.forName(
-                        "org.slf4j.impl.StaticLoggerBinder",
-                        false,
-                        Logger::class.java.classLoader
-                    )
-                }.isSuccess)
-                LoggerFactory.getLogger(DEFAULT_LOGGER_NAME)
-            else synchronized(err) {
-                try {
-                    System.setErr(PrintStream(object : OutputStream() {
-                        // noop
-                        override fun write(b: Int) {}
-                        override fun write(b: ByteArray) {}
-                        override fun write(b: ByteArray, off: Int, len: Int) {}
-                    })) // ignore slf4j's log
-
-                    // 使用 LoggerFactory 获取 logger, 以允许 log4j impl 已安装的情况下打印日志
-                    LoggerFactory.getLogger(DEFAULT_LOGGER_NAME)
-                } finally {
-                    System.setErr(err)
-                }
-            }
-            server = embeddedServer(CIO, environment = applicationEngineEnvironment {
-                this.parentCoroutineContext = coroutineContext
-                // ktor 500 internal error 错误通过此 logger 打印
-                // 而不是 CoroutineExceptionHandler
-                this.log = logger
-                this.module(Application::mirai)
-
-                connector {
-                    this.host = host
-                    this.port = port
-                }
-            })
-            server.start(true)
-
-
-        }
-
-        logger.info("Http api server is running with authKey: ${SessionManager.authKey}")
-        callback?.invoke()
-    }
-
-    fun stop() = server.stop(5000, 5000)
-}

+ 0 - 188
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/Session.kt

@@ -1,188 +0,0 @@
-/*
- * Copyright 2020 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/master/LICENSE
- */
-
-package net.mamoe.mirai.api.http
-
-import kotlinx.coroutines.*
-import net.mamoe.mirai.Bot
-import net.mamoe.mirai.api.http.config.Setting
-import net.mamoe.mirai.api.http.data.Config
-import net.mamoe.mirai.api.http.queue.CacheQueue
-import net.mamoe.mirai.api.http.queue.MessageQueue
-import net.mamoe.mirai.event.Listener
-import net.mamoe.mirai.event.events.BotEvent
-import net.mamoe.mirai.event.events.MessageEvent
-import net.mamoe.mirai.event.subscribeAlways
-import net.mamoe.mirai.utils.currentTimeSeconds
-import kotlin.coroutines.CoroutineContext
-import kotlin.coroutines.EmptyCoroutineContext
-
-tailrec fun generateSessionKey(): String {
-    fun generateRandomSessionKey(): String {
-        val all = "QWERTYUIOPASDFGHJKLZXCVBNM1234567890qwertyuiopasdfghjklzxcvbnm"
-        return buildString(capacity = 8) {
-            repeat(8) {
-                append(all.random())
-            }
-        }
-    }
-
-    val key = generateRandomSessionKey()
-    if (!SessionManager.allSession.containsKey(key)) {
-        return key
-    }
-
-    return generateSessionKey()
-}
-
-internal object SessionManager {
-
-    val allSession: MutableMap<String, Session> = mutableMapOf()
-
-    lateinit var authKey: String
-
-
-    fun createTempSession(): TempSession = TempSession(EmptyCoroutineContext).also { newTempSession ->
-        allSession[newTempSession.key] = newTempSession
-        //设置180000ms后检测并回收
-        newTempSession.launch {
-            delay(180000)
-            allSession[newTempSession.key]?.run {
-                if (this is TempSession)
-                    closeSession(newTempSession.key)
-            }
-        }
-    }
-
-    fun createAuthedSession(bot: Bot, originKey: String): AuthedSession = AuthedSession(bot, originKey, EmptyCoroutineContext).also { session ->
-        closeSession(originKey)
-        allSession[originKey] = session
-    }
-
-    operator fun get(sessionKey: String) = allSession[sessionKey]?.also {
-        if (it is AuthedSession) it.latestUsed = currentTimeSeconds()
-    }
-
-
-    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)
-
-}
-
-
-/**
- * @author NaturalHG
- * 这个用于管理不同Client与Mirai HTTP的会话
- *
- * [Session]均为内部操作用类
- * 需使用[SessionManager]
- */
-abstract class Session internal constructor(
-    coroutineContext: CoroutineContext,
-    open val key: String = generateSessionKey()
-) : CoroutineScope {
-    val supervisorJob = SupervisorJob(coroutineContext[Job])
-    final override val coroutineContext: CoroutineContext = supervisorJob + coroutineContext
-
-    internal open fun close() {
-        supervisorJob.complete()
-    }
-}
-
-
-/**
- * 任何新链接建立后分配一个[TempSession]
- *
- * TempSession在建立180s内没有转变为[AuthedSession]应被清除
- */
-class TempSession internal constructor(coroutineContext: CoroutineContext) : Session(coroutineContext)
-
-/**
- * 任何[TempSession]认证后转化为一个[AuthedSession]
- * 在这一步[AuthedSession]应该已经有assigned的bot
- */
-class AuthedSession internal constructor(val bot: Bot, originKey: String, coroutineContext: CoroutineContext) : Session(coroutineContext) {
-
-    companion object {
-        const val CHECK_TIME = 1800L // 1800s aka 30min
-    }
-
-    override val key = originKey
-
-    val messageQueue = MessageQueue()
-    val cacheQueue = CacheQueue()
-    val config = Config(
-        session = this,
-        cacheSize = Setting.cacheSize,
-        enableWebsocket = Setting.enableWebsocket
-    )
-    private var _listener: Listener<BotEvent>
-    private val _cache: Listener<MessageEvent>
-    private val releaseJob: Job //手动释放将会在下一次检查时回收Session
-
-    internal var latestUsed = currentTimeSeconds()
-
-    init {
-        _listener = bot.eventChannel.subscribeAlways{
-            if (this.bot === [email protected]) {
-                this.run(messageQueue::add)
-            }
-        }
-        _cache = bot.eventChannel.subscribeAlways {
-            if (this.bot === [email protected]) {
-                cacheQueue.add(this.source)
-            }
-        }
-
-        if (config.enableWebsocket) {
-            _listener.complete()
-        }
-
-        releaseJob = launch {
-            while (true) {
-                delay(CHECK_TIME * 1000)
-                if (!config.enableWebsocket && currentTimeSeconds() - latestUsed >= CHECK_TIME) {
-                    SessionManager.closeSession(this@AuthedSession)
-                    break
-                }
-            }
-        }
-    }
-
-    fun enableWebSocket() {
-        if (_listener.isActive) {
-            _listener.complete()
-            messageQueue.clear()
-        }
-    }
-
-    fun disableWebSocket() {
-        if (_listener.isCompleted) {
-            _listener = bot.eventChannel.subscribeAlways{
-                if (this.bot === [email protected]) {
-                    this.run(messageQueue::add)
-                }
-            }
-        }
-    }
-
-    override fun close() {
-        _listener.complete()
-        _cache.complete()
-
-        messageQueue.clear()
-        cacheQueue.clear()
-
-        super.close()
-    }
-}
-

+ 24 - 2
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/MahAdapter.kt

@@ -1,14 +1,36 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
 package net.mamoe.mirai.api.http.adapter
 
+import net.mamoe.mirai.event.events.BotEvent
+
 /**
  * Mah 接口规范,用于处理接收、发送消息后的处理逻辑
  * 不同接口格式请实现该接口
  */
-interface MahAdapter {
+abstract class MahAdapter(val name: String = "Abstract MahAdapter") {
 
     /**
      * 初始化
      */
-    fun initAdapter()
+    abstract fun initAdapter()
+
+    /**
+     * 启用
+     */
+    abstract fun enable()
+
+    /**
+     * 停止
+     */
+    abstract fun disable()
 
+    abstract suspend fun onReceiveBotEvent(event: BotEvent)
 }

+ 28 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/MahAdapterFactory.kt

@@ -0,0 +1,28 @@
+package net.mamoe.mirai.api.http.adapter
+
+import net.mamoe.mirai.api.http.adapter.http.HttpAdapter
+import net.mamoe.mirai.api.http.adapter.ws.WebsocketAdapter
+
+/**
+ * Adapter 工厂
+ * <P>
+ * 对于需要可初始化的 adapter 必须通过 register 静态注册
+ */
+object MahAdapterFactory {
+
+    private val registered: MutableMap<String, Class<out MahAdapter>> = mutableMapOf()
+
+    init {
+        register("http", HttpAdapter::class.java)
+        register("ws", WebsocketAdapter::class.java)
+    }
+
+    fun register(name: String, adapter: Class<out MahAdapter>) = registered.put(name, adapter)
+
+    fun build(name: String): MahAdapter? {
+        val clazz = registered[name] ?: return null
+        val noArgsConstructor = clazz.getConstructor() ?: return null
+
+        return kotlin.runCatching { noArgsConstructor.newInstance() }.getOrNull()
+    }
+}

+ 112 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/MahKtorAdapter.kt

@@ -0,0 +1,112 @@
+package net.mamoe.mirai.api.http.adapter
+
+import io.ktor.application.*
+import io.ktor.server.cio.*
+import io.ktor.server.engine.*
+import kotlinx.coroutines.CoroutineExceptionHandler
+import net.mamoe.mirai.utils.MiraiLogger
+import org.slf4j.LoggerFactory
+
+/**
+ * 使用 ktor 提供服务的 adapter,会提供 ktor 的 server 进行复用
+ */
+abstract class MahKtorAdapter(name: String) : MahAdapter(name) {
+
+    companion object {
+        /**
+         * 缓存已注册特定端口的 ktor server 配置
+         */
+        private val SERVER_CACHE = mutableMapOf<String, KtorServerConfiguration>()
+
+        /**
+         * 从缓存中构建 ktor server
+         */
+        internal fun buildKtorServer(key: String): ApplicationEngine? {
+            val conf = SERVER_CACHE[key] ?: throw IllegalStateException("No such key")
+            if (conf.initialized) return null
+
+            conf.initialized = true
+
+            return embeddedServer(CIO, applicationEngineEnvironment {
+
+                val serverName = conf.bindingAdapters
+                    .joinToString(prefix = "MahKtorAdapter[", separator = ",", postfix = "]") { it.name }
+
+                val coroutineLogger = MiraiLogger.create(serverName)
+
+                parentCoroutineContext = CoroutineExceptionHandler { _, throwable ->
+                    coroutineLogger.error(throwable)
+                }
+
+                log = LoggerFactory.getLogger(serverName)
+
+                modules.addAll(conf.modules)
+
+                connector {
+                    host = conf.host
+                    port = conf.port
+                }
+            })
+        }
+    }
+
+
+    abstract fun MahKtorAdapterInitBuilder.initKtorAdapter()
+
+    /**
+     * 将 Adapter 绑定的 ktor server 配置进行缓存
+     */
+    final override fun initAdapter(): Unit = with(MahKtorAdapterInitBuilder()) {
+        initKtorAdapter()
+
+        findKtorServerBuilder(host, port).let { serverBuilder ->
+            serverBuilder.bindingAdapters.add(this@MahKtorAdapter)
+            serverBuilder.addModules(modules)
+        }
+    }
+
+    /**
+     * enable 和 disable 转为统一处理
+     */
+    final override fun enable() {
+        SERVER_CACHE.forEach { entry ->
+            buildKtorServer(entry.key)?.start(wait = false)
+        }
+    }
+
+    final override fun disable() {}
+
+    /**
+     * 查找可复用的 ktor server
+     */
+    private fun findKtorServerBuilder(host: String, port: Int): KtorServerConfiguration {
+        val key = "$host:$port"
+        var config = SERVER_CACHE[key]
+        if (config == null) {
+            config = KtorServerConfiguration(host, port)
+            SERVER_CACHE[key] = config
+        }
+        return config
+    }
+}
+
+/**
+ * ktor server 配置
+ */
+private class KtorServerConfiguration(val host: String, val port: Int, var initialized: Boolean = false) {
+    val bindingAdapters: MutableList<MahKtorAdapter> = mutableListOf()
+    val modules: MutableList<Application.() -> Unit> = mutableListOf()
+
+    fun addModules(modules: List<Application.() -> Unit>) = this.modules.addAll(modules)
+}
+
+/**
+ * 单个 ktor adapter 初始化参数接收
+ */
+class MahKtorAdapterInitBuilder {
+    var host: String = ""
+    var port: Int = -1
+    internal val modules: MutableList<Application.() -> Unit> = mutableListOf()
+
+    fun module(module: Application.() -> Unit) = modules.add(module)
+}

+ 5 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/common/README.md

@@ -0,0 +1,5 @@
+## 公用数据
+
+定义了通用的异常类型、状态码
+
+外部定义 `adpater` 时可引用,尽可能维持一致性

+ 2 - 3
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/StateCode.kt → mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/common/StateCode.kt

@@ -7,10 +7,10 @@
  * https://github.com/mamoe/mirai/blob/master/LICENSE
  */
 
-package net.mamoe.mirai.api.http.data
+package net.mamoe.mirai.api.http.adapter.common
 
 import kotlinx.serialization.Serializable
-import net.mamoe.mirai.api.http.data.common.DTO
+import net.mamoe.mirai.api.http.adapter.internal.dto.DTO
 import java.io.File
 
 @Serializable
@@ -33,7 +33,6 @@ open class StateCode(val code: Int, var msg: String) : DTO {
     }
 
     // KS bug: 主构造器中不能有非字段参数 https://github.com/Kotlin/kotlinx.serialization/issues/575
-    @Serializable
     class IllegalAccess() : StateCode(400, "") { // 非法访问
         constructor(msg: String) : this() {
             this.msg = msg

+ 1 - 1
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/Exception.kt → mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/common/exception.kt

@@ -7,7 +7,7 @@
  * https://github.com/mamoe/mirai/blob/master/LICENSE
  */
 
-package net.mamoe.mirai.api.http.data
+package net.mamoe.mirai.api.http.adapter.common
 
 /**
  * 错误请求. 抛出这个异常后将会返回错误给一个请求

+ 42 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/common/exceptionHandle.kt

@@ -0,0 +1,42 @@
+package net.mamoe.mirai.api.http.adapter.common
+
+import io.ktor.application.*
+import io.ktor.http.*
+import io.ktor.response.*
+import io.ktor.routing.*
+import io.ktor.util.pipeline.*
+import net.mamoe.mirai.api.http.HttpApiPluginBase
+import net.mamoe.mirai.api.http.adapter.http.router.respondStateCode
+import net.mamoe.mirai.contact.BotIsBeingMutedException
+import net.mamoe.mirai.contact.MessageTooLargeException
+import net.mamoe.mirai.contact.PermissionDeniedException
+
+/**
+ * 统一捕获并处理异常
+ */
+internal inline fun Route.handleException(crossinline blk: suspend PipelineContext<Unit, ApplicationCall>.() -> Unit) = handle {
+    try {
+        blk(this)
+    } catch (e: NoSuchBotException) { // Bot不存在
+        call.respondStateCode(StateCode.NoBot)
+    } catch (e: IllegalSessionException) { // Session过期
+        call.respondStateCode(StateCode.IllegalSession)
+    } catch (e: NotVerifiedSessionException) { // Session未认证
+        call.respondStateCode(StateCode.NotVerifySession)
+    } catch (e: NoSuchElementException) { // 指定对象不存在
+        call.respondStateCode(StateCode.NoElement)
+    } catch (e: NoSuchFileException) { // 文件不存在
+        call.respondStateCode(StateCode.NoFile(e.file))
+    } catch (e: PermissionDeniedException) { // 缺少权限
+        call.respondStateCode(StateCode.PermissionDenied)
+    } catch (e: BotIsBeingMutedException) { // Bot被禁言
+        call.respondStateCode(StateCode.BotMuted)
+    } catch (e: MessageTooLargeException) { // 消息过长
+        call.respondStateCode(StateCode.MessageTooLarge)
+    } catch (e: IllegalAccessException) { // 错误访问
+        call.respondStateCode(StateCode.IllegalAccess(e.message), HttpStatusCode.BadRequest)
+    } catch (e: Throwable) {
+        HttpApiPluginBase.logger.error(e)
+        call.respond(HttpStatusCode.InternalServerError, e.message!!)
+    }
+}

+ 21 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/HttpAdapter.kt

@@ -0,0 +1,21 @@
+package net.mamoe.mirai.api.http.adapter.http
+
+import io.ktor.application.*
+import net.mamoe.mirai.api.http.adapter.MahAdapterFactory
+import net.mamoe.mirai.api.http.adapter.MahKtorAdapter
+import net.mamoe.mirai.api.http.adapter.MahKtorAdapterInitBuilder
+import net.mamoe.mirai.api.http.adapter.http.router.httpModule
+import net.mamoe.mirai.event.events.BotEvent
+
+class HttpAdapter : MahKtorAdapter("http") {
+
+    override fun MahKtorAdapterInitBuilder.initKtorAdapter() {
+        host = "localhost"
+        port = 8080
+        module(Application::httpModule)
+    }
+
+    override suspend fun onReceiveBotEvent(event: BotEvent) {
+        TODO("Not yet implemented")
+    }
+}

+ 34 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/about.kt

@@ -0,0 +1,34 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.api.http.adapter.http.router
+
+import io.ktor.application.*
+import io.ktor.routing.*
+import net.mamoe.mirai.api.http.HttpApiPluginBase
+import net.mamoe.mirai.api.http.adapter.internal.dto.StringMapRestfulResult
+
+private val mahVersion by lazy {
+    val desc = HttpApiPluginBase.description
+    desc.javaClass.fields.first { it.name == "version" }.get(desc).toString()
+}
+
+/**
+ * 配置路由
+ */
+internal fun Application.aboutRouter() = routing {
+
+    /**
+     * 获取API-HTTP插件信息
+     */
+    get("/about") {
+        val data = mapOf("version" to mahVersion)
+        call.respondDTO(StringMapRestfulResult(data = data))
+    }
+}

+ 41 - 61
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/base.kt

@@ -9,77 +9,55 @@
 
 package net.mamoe.mirai.api.http.adapter.http.router
 
-import io.ktor.application.Application
-import io.ktor.application.ApplicationCall
-import io.ktor.application.call
-import io.ktor.application.install
-import io.ktor.features.CORS
-import io.ktor.features.CallLogging
-import io.ktor.features.DefaultHeaders
-import io.ktor.features.maxAgeDuration
-import io.ktor.http.ContentType
-import io.ktor.http.HttpMethod
-import io.ktor.http.HttpStatusCode
-import io.ktor.http.content.PartData
-import io.ktor.request.contentCharset
-import io.ktor.request.receiveChannel
-import io.ktor.response.defaultTextContentType
-import io.ktor.response.respond
-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 io.ktor.utils.io.readRemaining
-import io.ktor.utils.io.streams.inputStream
-import io.ktor.websocket.WebSockets
-//import net.mamoe.mirai.api.http.context.session.manager.AuthedSession
-import net.mamoe.mirai.api.http.HttpApiPluginBase
-//import net.mamoe.mirai.api.http.context.session.manager.TempSession
-import net.mamoe.mirai.api.http.config.Setting
-import net.mamoe.mirai.api.http.data.*
-import net.mamoe.mirai.api.http.data.common.DTO
-import net.mamoe.mirai.api.http.data.common.VerifyDTO
-import net.mamoe.mirai.api.http.route.*
-import net.mamoe.mirai.api.http.util.jsonParseOrNull
-import net.mamoe.mirai.api.http.util.toJson
-import net.mamoe.mirai.contact.BotIsBeingMutedException
-import net.mamoe.mirai.contact.MessageTooLargeException
-import net.mamoe.mirai.contact.PermissionDeniedException
-import org.slf4j.helpers.NOPLoggerFactory
-import kotlin.time.DurationUnit
+import io.ktor.application.*
+import io.ktor.features.*
+import io.ktor.http.*
+import io.ktor.http.content.*
+import io.ktor.request.*
+import io.ktor.response.*
+import io.ktor.util.pipeline.*
+import io.ktor.utils.io.*
+import io.ktor.utils.io.streams.*
+import net.mamoe.mirai.api.http.adapter.common.IllegalParamException
+import net.mamoe.mirai.api.http.adapter.common.StateCode
+import net.mamoe.mirai.api.http.adapter.internal.dto.DTO
+import net.mamoe.mirai.api.http.adapter.internal.serializer.jsonParseOrNull
+import net.mamoe.mirai.api.http.adapter.internal.serializer.toJson
 import kotlin.time.ExperimentalTime
-import kotlin.time.toDuration
 
 @OptIn(ExperimentalTime::class)
-fun Application.httpRoute() {
+fun Application.httpModule() {
     install(DefaultHeaders)
-    install(CORS) {
-        method(HttpMethod.Options)
-        allowNonSimpleContentTypes = true
-        maxAgeDuration = 1.toDuration(DurationUnit.DAYS)
-
-        Setting.cors.forEach {
-            host(it, schemes = listOf("http", "https"))
-        }
-    }
-    authModule()
-    commandModule()
-    messageModule()
-    eventRouteModule()
-    infoModule()
-    groupManageModule()
-    configRouteModule()
+//    install(CORS) {
+//        method(HttpMethod.Options)
+//        allowNonSimpleContentTypes = true
+//        maxAgeDuration = 1.toDuration(DurationUnit.DAYS)
+//
+////        MainSetting.cors.forEach {
+////            host(it, schemes = listOf("http", "https"))
+////        }
+//    }
+    authRouter()
+    messageRouter()
+    eventRouter()
+    infoRouter()
+    groupManageRouter()
+    aboutRouter()
 }
 
 
-
 /*
     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 : StateCode> ApplicationCall.respondStateCode(
+    code: T,
+    status: HttpStatusCode = HttpStatusCode.OK
+) = respondJson(code.toJson(), status)
 
-internal suspend inline fun <reified T : DTO> ApplicationCall.respondDTO(dto: T, status: HttpStatusCode = HttpStatusCode.OK) = respondJson(dto.toJson(), 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)
@@ -96,7 +74,9 @@ 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")
+): Nothing = throw IllegalParamException(
+    "Illegal param. A $expectingType is required for `$paramName` while `$actualValue` is given"
+)
 
 
 @OptIn(ExperimentalUnsignedTypes::class)

+ 194 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/command.kt

@@ -0,0 +1,194 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.api.http.adapter.http.router
+//
+//import io.ktor.application.*
+//import io.ktor.http.*
+//import io.ktor.http.cio.websocket.*
+//import io.ktor.response.*
+//import io.ktor.routing.*
+//import io.ktor.websocket.*
+//import kotlinx.serialization.Serializable
+//import net.mamoe.mirai.Bot
+//import net.mamoe.mirai.api.http.HttpApiPluginBase
+//import net.mamoe.mirai.api.http.adapter.http.router.handleException
+//import net.mamoe.mirai.api.http.adapter.http.router.httpAuth
+//import net.mamoe.mirai.api.http.adapter.common.IllegalParamException
+//import net.mamoe.mirai.api.http.adapter.internal.dto.DTO
+//import net.mamoe.mirai.api.http.util.toJson
+//import net.mamoe.mirai.console.command.CommandSender
+//import net.mamoe.mirai.console.permission.AbstractPermitteeId
+//import net.mamoe.mirai.console.permission.PermitteeId
+//import net.mamoe.mirai.console.util.ConsoleExperimentalApi
+//import net.mamoe.mirai.contact.Contact
+//import net.mamoe.mirai.contact.User
+//import net.mamoe.mirai.message.MessageReceipt
+//import net.mamoe.mirai.message.data.Message
+//import kotlin.coroutines.CoroutineContext
+//import kotlin.coroutines.EmptyCoroutineContext
+//
+///**
+// * 命令行路由
+// */
+//@OptIn(ConsoleExperimentalApi::class)
+//fun Application.commandModule() {
+//
+//    routing {
+//        /**
+//         * 注册命令
+//         */
+//        httpAuth<PostCommandDTO>("/command/register") {
+////            if (it.authKey != SessionManager.authKey) {
+////                call.respondStateCode(StateCode.AuthKeyFail)
+////            } else {
+////                val names = ArrayList<String>(1 + it.alias.size).apply {
+////                    add(it.name)
+////                    addAll(it.alias)
+////                }
+////
+//////                RegisterCommand(it.description, it.usage, *names.toTypedArray()).register(true)
+////                call.respondStateCode(StateCode(-1, "未支持操作"))
+////            }
+//        }
+//
+//        /**
+//         * 执行命令
+//         */
+//        httpAuth<PostCommandDTO>("/command/send") {
+////            if (it.authKey != SessionManager.authKey) {
+////                call.respondStateCode(StateCode.AuthKeyFail)
+////            } else {
+////                val sender = HttpCommandSender(call)
+////
+////                CommandManager.run {
+////                    when (val result = executeCommand(sender, PlainText("${it.name} ${it.args.joinToString(" ")}"))) {
+////                        is CommandExecuteResult.Success -> if (!sender.consume) call.respondText("")
+////                        else -> call.respondStateCode(StateCode.NoElement)
+////                    }
+////                }
+////            }
+//        }
+//
+//        /**
+//         * 获取Manager
+//         */
+//        route("/managers", HttpMethod.Get) {
+//            handleException {
+//                val qq = call.parameters["qq"] ?: throw IllegalParamException("参数格式错误")
+//                val managers = listOf<Long>()
+//                call.respondJson(managers.toJson())
+//            }
+//        }
+//
+//        /**
+//         * 广播命令
+//         */
+//        webSocket("/command") {
+//            // 校验Auth key
+//            val authKey = call.parameters["authKey"]
+////            if (authKey == null) {
+////                outgoing.send(Frame.Text(StateCode(400, "参数格式错误").toJson(StateCode.serializer())))
+////                close(CloseReason(CloseReason.Codes.NORMAL, "参数格式错误"))
+////                return@webSocket
+////            }
+////            if (authKey != SessionManager.authKey) {
+////                outgoing.send(Frame.Text(StateCode.AuthKeyFail.toJson(StateCode.serializer())))
+////                close(CloseReason(CloseReason.Codes.NORMAL, "Auth Key错误"))
+////                return@webSocket
+////            }
+//
+//            // 订阅onCommand事件
+//            val subscriber = HttpApiPluginBase.subscribeCommand { name, friend, group, args ->
+//                outgoing.send(Frame.Text(CommandDTO(name, friend, group, args).toJson()))
+//            }
+//
+//            try {
+//                // 阻塞websocket
+//                for (frame in incoming) {
+//                    /* do nothing */
+//                    HttpApiPluginBase.logger.info("command websocket send $frame")
+//                }
+//            } finally {
+//                HttpApiPluginBase.unSubscribeCommand(subscriber)
+//            }
+//        }
+//    }
+//}
+//
+//// TODO: 将command输出返回给请求
+//class HttpCommandSender(
+//    private val call: ApplicationCall,
+//    override val coroutineContext: CoroutineContext = EmptyCoroutineContext
+//) : CommandSender {
+//    override val bot: Bot? = null
+//    override val name: String = "Mirai Http Api"
+//    override val permitteeId: PermitteeId
+//        get() = object : PermitteeId {
+//            override val directParents: Array<out PermitteeId>
+//                get() = arrayOf(AbstractPermitteeId.Console)
+//
+//            override fun asString(): String = "http-api"
+//        }
+//
+//
+//    override val subject: Contact? = null
+//    override val user: User? = null
+//
+//    var consume = false
+//
+//    override suspend fun sendMessage(message: String): MessageReceipt<Contact>? {
+////        appendMessage(message)
+//        if (!consume) {
+//            call.respondText(message)
+//            consume = true
+//        }
+//
+//        return null
+//    }
+//
+//    override suspend fun sendMessage(message: Message): MessageReceipt<Contact>? {
+////        appendMessage(messageChain.toString())
+//        if (!consume) {
+//            call.respondText(message.toString())
+//            consume = true
+//        }
+//
+//        return null
+//    }
+//
+//    /*override suspend fun catchExecutionException(e: Throwable) {
+//        // Nothing
+//    }*/
+//
+//
+////    override suspend fun flushMessage() {
+////        if (builder.isNotEmpty()) {
+////            call.respondText(builder.toString().removeSuffix("\n"))
+////        }
+////    }
+//}
+//
+//@Serializable
+//data class CommandDTO(
+//    val name: String,
+//    val friend: Long,
+//    val group: Long,
+//    val args: List<String>,
+//) : DTO
+//
+//@Serializable
+//private data class PostCommandDTO(
+//    val authKey: String,
+//    val name: String,
+//    val alias: List<String> = emptyList(),
+//    val description: String = "",
+//    val usage: String = "",
+//    val args: List<String> = emptyList(),
+//) : DTO

+ 59 - 67
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/dsl.kt

@@ -4,36 +4,43 @@ import io.ktor.application.*
 import io.ktor.http.*
 import io.ktor.http.content.*
 import io.ktor.request.*
-import io.ktor.response.*
 import io.ktor.routing.*
 import io.ktor.util.pipeline.*
-import net.mamoe.mirai.api.http.HttpApiPluginBase
+import net.mamoe.mirai.api.http.adapter.common.*
+import net.mamoe.mirai.api.http.adapter.common.handleException
+import net.mamoe.mirai.api.http.adapter.http.session.HttpAuthedSession
+import net.mamoe.mirai.api.http.adapter.internal.dto.VerifyDTO
+import net.mamoe.mirai.api.http.adapter.internal.dto.BindDTO
 import net.mamoe.mirai.api.http.context.MahContextHolder
-import net.mamoe.mirai.api.http.context.session.AuthedSession
 import net.mamoe.mirai.api.http.context.session.TempSession
-import net.mamoe.mirai.api.http.data.*
-import net.mamoe.mirai.api.http.data.common.DTO
-import net.mamoe.mirai.api.http.data.common.VerifyDTO
-import net.mamoe.mirai.contact.BotIsBeingMutedException
-import net.mamoe.mirai.contact.MessageTooLargeException
-import net.mamoe.mirai.contact.PermissionDeniedException
+import net.mamoe.mirai.api.http.adapter.internal.dto.AuthedDTO
+import net.mamoe.mirai.api.http.context.session.IAuthedSession
+
+private typealias PC = PipelineContext<Unit, ApplicationCall>
+
+@ContextDsl
+internal inline fun Route.routeWithHandle(path: String, method: HttpMethod, crossinline blk: suspend PC.() -> Unit) =
+    route(path, method) { handleException { blk() } }
 
 /**
  * Auth,处理http server的验证
  * 为闭包传入一个AuthDTO对象
  */
 @ContextDsl
-internal inline fun <reified T : DTO> Route.httpAuth(
-    path: String,
-    crossinline body: suspend PipelineContext<Unit, ApplicationCall>.(T) -> Unit
-): Route {
-    return route(path, HttpMethod.Post) {
-        handleException {
-            val dto = context.receiveDTO<T>() ?: throw IllegalParamException("参数格式错误")
-            this.body(dto)
-        }
+internal inline fun Route.httpVerify(path: String, crossinline body: suspend PC.(VerifyDTO) -> Unit) =
+    routeWithHandle(path, HttpMethod.Post) {
+        val dto = context.receiveDTO<VerifyDTO>() ?: throw IllegalParamException("参数格式错误")
+        this.body(dto)
     }
-}
+
+
+@ContextDsl
+internal inline fun Route.httpBind(path: String, crossinline body: suspend PC.(BindDTO) -> Unit) =
+    routeWithHandle(path, HttpMethod.Post) {
+        val dto = context.receiveDTO<BindDTO>() ?: throw IllegalParamException("参数格式错误")
+        body(dto)
+    }
+
 
 /**
  * Verify,用于处理bot的行为请求
@@ -45,26 +52,14 @@ internal inline fun <reified T : DTO> Route.httpAuth(
  * it 为json解析出的DTO对象
  */
 @ContextDsl
-internal inline fun <reified T : VerifyDTO> Route.httpVerify(
+internal inline fun <reified T : AuthedDTO> Route.httpAuthedPost(
     path: String,
-    verifiedSessionKey: Boolean = true,
-    crossinline body: suspend PipelineContext<Unit, ApplicationCall>.(T) -> Unit
-): Route {
-    return route(path, HttpMethod.Post) {
-        handleException {
-            val dto = context.receiveDTO<T>() ?: throw IllegalParamException("参数格式错误")
-            val session = MahContextHolder.mahContext.sessionManager[dto.sessionKey]
-                ?: throw IllegalSessionException
+    crossinline body: suspend PC.(T) -> Unit
+) = routeWithHandle(path, HttpMethod.Post) {
+    val dto = context.receiveDTO<T>() ?: throw IllegalParamException("参数格式错误")
 
-            with(session) {
-                when {
-                    this is TempSession && verifiedSessionKey -> throw NotVerifiedSessionException
-                    this is AuthedSession -> dto.session = this
-                }
-            }
-            this.body(dto)
-        }
-    }
+    getAuthedSession(dto.sessionKey).also { dto.session = it }
+    this.body(dto)
 }
 
 /**
@@ -72,42 +67,39 @@ internal inline fun <reified T : VerifyDTO> Route.httpVerify(
  * 验证请求参数中sessionKey参数的有效性
  */
 @ContextDsl
-internal fun Route.httpGet(
-    path: String,
-    body: suspend PipelineContext<Unit, ApplicationCall>.(AuthedSession) -> Unit
-): Route {
-    return route(path, HttpMethod.Get) {
-        handleException {
-            val sessionKey = call.parameters["sessionKey"] ?: throw IllegalParamException("参数格式错误")
-            val session = MahContextHolder.mahContext.sessionManager[sessionKey]
-                ?: throw IllegalSessionException
+internal fun Route.httpAuthedGet(path: String, body: suspend PC.(HttpAuthedSession) -> Unit) =
+    routeWithHandle(path, HttpMethod.Get) {
+        val sessionKey = call.parameters["sessionKey"] ?: throw IllegalParamException("参数格式错误")
 
-            when (session) {
-                is TempSession -> throw NotVerifiedSessionException
-                is AuthedSession -> this.body(session)
-            }
-        }
+        this.body(getAuthedSession(sessionKey))
     }
-}
 
 @ContextDsl
-internal inline fun Route.httpMultiPart(
+internal inline fun Route.httpAuthedMultiPart(
     path: String,
-    crossinline body: suspend PipelineContext<Unit, ApplicationCall>.(AuthedSession, List<PartData>) -> Unit
-) : Route {
-    return route(path, HttpMethod.Post) {
-        handleException {
-            val parts = call.receiveMultipart().readAllParts()
-            val sessionKey = call.parameters["sessionKey"] ?: throw IllegalParamException("参数格式错误")
-            val session = MahContextHolder.mahContext.sessionManager[sessionKey]
-                ?: throw IllegalSessionException
+    crossinline body: suspend PC.(HttpAuthedSession, List<PartData>) -> Unit
+) = routeWithHandle(path, HttpMethod.Post) {
+    val parts = call.receiveMultipart().readAllParts()
+    val sessionKey = call.parameters["sessionKey"] ?: throw IllegalParamException("参数格式错误")
 
-            when (session) {
-                is TempSession -> throw NotVerifiedSessionException
-                is AuthedSession -> this.body(session, parts)
-            }
-        }
-    }
+    this.body(getAuthedSession(sessionKey), parts)
 }
 
+/**
+ * 获取 session 并进行类型校验
+ */
+private fun getAuthedSession(sessionKey: String): HttpAuthedSession =
+    when (val session = MahContextHolder[sessionKey]) {
+        is HttpAuthedSession -> session
+        is IAuthedSession -> proxyAuthedSession(session)
+        is TempSession -> throw NotVerifiedSessionException
+        else -> throw IllegalSessionException
+    }
 
+/**
+ * 置换全局 session 为代理对象
+ */
+private fun proxyAuthedSession(authedSession: IAuthedSession): HttpAuthedSession =
+    HttpAuthedSession(authedSession).also {
+        MahContextHolder.sessionManager[authedSession.key] = it
+    }

+ 61 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/event.kt

@@ -0,0 +1,61 @@
+package net.mamoe.mirai.api.http.adapter.http.router
+
+import io.ktor.application.*
+import io.ktor.routing.*
+import kotlinx.serialization.Serializable
+import net.mamoe.mirai.LowLevelApi
+import net.mamoe.mirai.Mirai
+import net.mamoe.mirai.api.http.adapter.common.StateCode
+import net.mamoe.mirai.api.http.adapter.internal.dto.AuthedDTO
+import net.mamoe.mirai.utils.MiraiExperimentalApi
+
+
+@OptIn(MiraiExperimentalApi::class, LowLevelApi::class)
+internal fun Application.eventRouter() = routing {
+
+    httpAuthedPost<EventRespDTO>("/resp/newFriendRequestEvent") {
+        Mirai.solveNewFriendRequestEvent(
+            it.session.bot,
+            eventId = it.eventId,
+            fromId = it.fromId,
+            fromNick = "",
+            accept = it.operate == 0,
+            blackList = it.operate == 2
+        )
+        call.respondStateCode(StateCode.Success)
+    }
+
+    httpAuthedPost<EventRespDTO>("/resp/memberJoinRequestEvent") {
+        Mirai.solveMemberJoinRequestEvent(
+            it.session.bot,
+            eventId = it.eventId,
+            fromId = it.fromId,
+            fromNick = "",
+            groupId = it.groupId,
+            accept = if (it.operate == 0) true else if (it.operate % 2 == 0) null else false,
+            blackList = it.operate == 3 || it.operate == 4
+        )
+        call.respondStateCode(StateCode.Success)
+    }
+
+    httpAuthedPost<EventRespDTO>("/resp/botInvitedJoinGroupRequestEvent") {
+        Mirai.solveBotInvitedJoinGroupRequestEvent(
+            it.session.bot,
+            eventId = it.eventId,
+            invitorId = it.fromId,
+            groupId = it.groupId,
+            accept = it.operate == 0
+        )
+        call.respondStateCode(StateCode.Success)
+    }
+
+}
+
+@Serializable
+private data class EventRespDTO(
+    val eventId: Long,
+    val fromId: Long,
+    val groupId: Long,
+    val operate: Int,
+    val message: String
+) : AuthedDTO()

+ 117 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/group.kt

@@ -0,0 +1,117 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.api.http.adapter.http.router
+
+import io.ktor.application.*
+import io.ktor.routing.*
+import net.mamoe.mirai.api.http.adapter.common.StateCode
+import net.mamoe.mirai.api.http.adapter.internal.dto.*
+import net.mamoe.mirai.api.http.adapter.internal.dto.KickDTO
+import net.mamoe.mirai.api.http.adapter.internal.dto.MuteDTO
+import net.mamoe.mirai.api.http.adapter.internal.dto.QuitDTO
+
+/**
+ * 群管理路由
+ */
+internal fun Application.groupManageRouter() = routing {
+
+    /**
+     * 禁言所有人(需要相关权限)
+     */
+    httpAuthedPost<MuteDTO>("/muteAll") {
+        it.session.bot.getGroupOrFail(it.target).settings.isMuteAll = true
+        call.respondStateCode(StateCode.Success)
+    }
+
+    /**
+     * 取消禁言所有人(需要相关权限)
+     */
+    httpAuthedPost<MuteDTO>("/unmuteAll") {
+        it.session.bot.getGroupOrFail(it.target).settings.isMuteAll = false
+        call.respondStateCode(StateCode.Success)
+    }
+
+    /**
+     * 禁言指定群成员(需要相关权限)
+     */
+    httpAuthedPost<MuteDTO>("/mute") {
+        it.session.bot.getGroupOrFail(it.target).getOrFail(it.memberId).mute(it.time)
+        call.respondStateCode(StateCode.Success)
+    }
+
+    /**
+     * 取消禁言指定群成员(需要相关权限)
+     */
+    httpAuthedPost<MuteDTO>("/unmute") {
+        it.session.bot.getGroupOrFail(it.target).getOrFail(it.memberId).unmute()
+        call.respondStateCode(StateCode.Success)
+    }
+
+    /**
+     * 移出群聊(需要相关权限)
+     */
+    httpAuthedPost<KickDTO>("/kick") {
+        it.session.bot.getGroupOrFail(it.target).getOrFail(it.memberId).kick(it.msg)
+        call.respondStateCode(StateCode.Success)
+    }
+
+    /**
+     * Bot退出群聊(Bot不能为群主)
+     */
+    httpAuthedPost<QuitDTO>("/quit") {
+        val succeed = it.session.bot.getGroupOrFail(it.target).quit()
+        call.respondStateCode(
+            if (succeed) StateCode.Success
+            else StateCode.PermissionDenied
+        )
+    }
+
+    /**
+     * 获取群设置(需要相关权限)
+     */
+    httpAuthedGet("/groupConfig") {
+        val group = it.bot.getGroupOrFail(paramOrNull("target"))
+        call.respondDTO(GroupDetailDTO(group))
+    }
+
+    /**
+     * 修改群设置(需要相关权限)
+     */
+    httpAuthedPost<GroupConfigDTO>("/groupConfig") { dto ->
+        val group = dto.session.bot.getGroupOrFail(dto.target)
+        with(dto.config) {
+            name?.let { group.name = it }
+            announcement?.let { group.settings.entranceAnnouncement = it }
+            allowMemberInvite?.let { group.settings.isAllowMemberInvite = it }
+            // TODO: 待core接口实现设置可改
+            //    confessTalk?.let { group.settings.isConfessTalkEnabled = it }
+            //    autoApprove?.let { group.autoApprove = it }
+            //    anonymousChat?.let { group.anonymousChat = it }
+        }
+        call.respondStateCode(StateCode.Success)
+    }
+
+    /**
+     * 群员信息管理(需要相关权限)
+     */
+    httpAuthedGet("/memberInfo") {
+        val member = it.bot.getGroupOrFail(paramOrNull("target")).getOrFail(paramOrNull("memberId"))
+        call.respondDTO(MemberDetailDTO(member))
+    }
+
+    httpAuthedPost<MemberInfoDTO>("/memberInfo") { dto ->
+        val member = dto.session.bot.getGroupOrFail(dto.target).getOrFail(dto.memberId)
+        with(dto.info) {
+            name?.let { member.nameCard = it }
+            specialTitle?.let { member.specialTitle = it }
+        }
+        call.respondStateCode(StateCode.Success)
+    }
+}

+ 73 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/info.kt

@@ -0,0 +1,73 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.api.http.adapter.http.router
+
+import io.ktor.application.*
+import io.ktor.routing.*
+import net.mamoe.mirai.api.http.adapter.common.StateCode
+import net.mamoe.mirai.api.http.adapter.internal.dto.ListRestfulResult
+import net.mamoe.mirai.api.http.adapter.internal.dto.GroupDTO
+import net.mamoe.mirai.api.http.adapter.internal.dto.MemberDTO
+import net.mamoe.mirai.api.http.adapter.internal.dto.QQDTO
+import net.mamoe.mirai.api.http.adapter.internal.serializer.toJson
+
+/**
+ * 基本信息路由
+ */
+internal fun Application.infoRouter() = routing {
+
+    /**
+     * 查询好友列表
+     */
+    httpAuthedGet("/friendList") {
+        val data = it.bot.friends.toList().map { qq -> QQDTO(qq) }
+        call.respondDTO(ListRestfulResult(data = data))
+    }
+
+    /**
+     * 查询QQ群列表
+     */
+    httpAuthedGet("/groupList") {
+        val data = it.bot.groups.toList().map { group -> GroupDTO(group) }
+        call.respondDTO(ListRestfulResult(data = data))
+    }
+
+    /**
+     * 查询QQ群成员列表
+     */
+    httpAuthedGet("/memberList") {
+        val data = it.bot.getGroupOrFail(paramOrNull("target")).members.toList().map { member -> MemberDTO(member) }
+        call.respondDTO(ListRestfulResult(data = data))
+    }
+
+    /**
+     * 查询机器人个人信息
+     */
+    httpAuthedGet("/botProfile") {
+        // TODO: 等待queryProfile()支持
+        call.respondStateCode(StateCode.NoOperateSupport)
+    }
+
+    /**
+     * 查询好友个人信息
+     */
+    httpAuthedGet("/friendProfile") {
+        // TODO: 等待queryProfile()支持
+        call.respondStateCode(StateCode.NoOperateSupport)
+    }
+
+    /**
+     * 查询QQ群成员个人信息
+     */
+    httpAuthedGet("/memberProfile") {
+        // TODO: 等待queryProfile()支持
+        call.respondStateCode(StateCode.NoOperateSupport)
+    }
+}

+ 242 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/message.kt

@@ -0,0 +1,242 @@
+package net.mamoe.mirai.api.http.adapter.http.router
+
+import io.ktor.application.*
+import io.ktor.http.content.*
+import io.ktor.routing.*
+import net.mamoe.mirai.api.http.adapter.common.IllegalParamException
+import net.mamoe.mirai.api.http.adapter.common.StateCode
+import net.mamoe.mirai.api.http.adapter.internal.convertor.toDTO
+import net.mamoe.mirai.api.http.adapter.internal.convertor.toMessageChain
+import net.mamoe.mirai.api.http.adapter.internal.dto.*
+import net.mamoe.mirai.api.http.adapter.internal.serializer.toJson
+import net.mamoe.mirai.contact.Contact
+import net.mamoe.mirai.contact.Contact.Companion.uploadImage
+import net.mamoe.mirai.message.MessageReceipt
+import net.mamoe.mirai.message.data.*
+import net.mamoe.mirai.message.data.Image.Key.queryUrl
+import net.mamoe.mirai.message.data.MessageSource.Key.quote
+import net.mamoe.mirai.message.data.MessageSource.Key.recall
+import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
+import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
+import net.mamoe.mirai.utils.MiraiExperimentalApi
+import java.net.URL
+
+/**
+ * 消息路由
+ */
+@OptIn(MiraiExperimentalApi::class)
+internal fun Application.messageRouter() = routing {
+
+    /**
+     * 获取未读消息剩余消息数量
+     */
+    httpAuthedGet("/countMessage") {
+        val count = it.sourceCache.size
+        call.respondDTO(IntRestfulResult(data = count))
+    }
+
+    /**
+     * 获取指定条数最老的消息并从未读消息中删除获取的消息
+     */
+    httpAuthedGet("/fetchMessage") {
+        val count: Int = paramOrNull("count")
+        val data = it.unreadQueue.fetch(count)
+
+        call.respondDTO(EventListRestfulResult(data = data))
+    }
+
+    /**
+     * 获取指定条数最新的消息并从未读消息删除获取的消息
+     */
+    httpAuthedGet("/fetchLatestMessage") {
+        val count: Int = paramOrNull("count")
+        val data = it.unreadQueue.fetchLatest(count)
+
+        call.respondDTO(EventListRestfulResult(data = data))
+    }
+
+    /**
+     * 获取指定条数最老的消息,和 `/fetchMessage` 不一样,这个方法不会删除消息
+     */
+    httpAuthedGet("/peakMessage") {
+        val count: Int = paramOrNull("count")
+        val data = it.unreadQueue.peek(count)
+
+        call.respondDTO(EventListRestfulResult(data = data))
+    }
+
+    /**
+     * 获取指定条数最新的消息,和 `/fetchLatestMessage` 不一样,这个方法不会删除消息
+     */
+    httpAuthedGet("/peekLatestMessage") {
+        val count: Int = paramOrNull("count")
+        val data = it.unreadQueue.peekLatest(count)
+
+        call.respondDTO(EventListRestfulResult(data = data))
+    }
+
+    /**
+     * 获取指定ID消息(从CacheQueue获取)
+     */
+    httpAuthedGet("/messageFromId") {
+        val id: Int = paramOrNull("id")
+        val source = it.sourceCache[id]
+
+        val dto = when (source) {
+            is OnlineMessageSource.Outgoing.ToGroup -> GroupMessagePacketDTO(MemberDTO(source.target.botAsMember))
+            is OnlineMessageSource.Outgoing.ToFriend -> FriendMessagePacketDTO(QQDTO(source.sender.asFriend))
+            is OnlineMessageSource.Outgoing.ToTemp -> TempMessagePacketDto(MemberDTO(source.target))
+            is OnlineMessageSource.Outgoing.ToStranger -> StrangerMessagePacketDto(QQDTO(source.target))
+
+            is OnlineMessageSource.Incoming.FromGroup -> GroupMessagePacketDTO(MemberDTO(source.sender))
+            is OnlineMessageSource.Incoming.FromFriend -> FriendMessagePacketDTO(QQDTO(source.sender))
+            is OnlineMessageSource.Incoming.FromTemp -> TempMessagePacketDto(MemberDTO(source.sender))
+            is OnlineMessageSource.Incoming.FromStranger -> StrangerMessagePacketDto(QQDTO(source.sender))
+        }
+
+        dto.messageChain = messageChainOf(source, source.originalMessage).toDTO { d -> d != UnknownMessageDTO }
+
+        call.respondDTO(EventRestfulResult(data = dto))
+    }
+
+    /**
+     * 发送消息
+     */
+    suspend fun <C : Contact> sendMessage(
+        quote: QuoteReply?,
+        messageChain: MessageChain,
+        target: C
+    ): MessageReceipt<Contact> {
+        val send = if (quote == null) {
+            messageChain
+        } else {
+            ((quote + messageChain) as Iterable<Message>).toMessageChain()
+        }
+        return target.sendMessage(send)
+    }
+
+    /**
+     * 发送消息给好友
+     */
+    httpAuthedPost<SendDTO>("/sendFriendMessage") {
+        val quote = it.quote?.let { q -> it.session.sourceCache[q].quote() }
+        val bot = it.session.bot
+
+        fun findQQ(qq: Long): Contact = bot.getFriend(qq)
+            ?: bot.getStranger(qq)
+            ?: throw NoSuchElementException("friend $qq not found")
+
+        val qq = when {
+            it.target != null -> findQQ(it.target)
+            it.qq != null -> findQQ(it.qq)
+            else -> throw NoSuchElementException()
+        }
+
+        val receipt = sendMessage(quote, it.messageChain.toMessageChain(qq), qq)
+        it.session.sourceCache.offer(receipt.source)
+
+        call.respondDTO(SendRetDTO(messageId = receipt.source.ids.firstOrNull() ?: 0))
+    }
+
+    /**
+     * 发送消息到QQ群
+     */
+    httpAuthedPost<SendDTO>("/sendGroupMessage") {
+        val quote = it.quote?.let { q -> it.session.sourceCache[q].quote() }
+
+        val bot = it.session.bot
+        val group = when {
+            it.target != null -> bot.getGroupOrFail(it.target)
+            it.group != null -> bot.getGroupOrFail(it.group)
+            else -> throw NoSuchElementException()
+        }
+
+        val receipt = sendMessage(quote, it.messageChain.toMessageChain(group), group)
+        it.session.sourceCache.offer(receipt.source)
+
+        call.respondDTO(SendRetDTO(messageId = receipt.source.ids.firstOrNull() ?: 0))
+    }
+
+    /**
+     * 发送消息给临时会话
+     */
+    httpAuthedPost<SendDTO>("/sendTempMessage") {
+        val quote = it.quote?.let { q -> it.session.sourceCache[q].quote() }
+
+        val bot = it.session.bot
+        val member = when {
+            it.qq != null && it.group != null -> bot.getGroupOrFail(it.group).getOrFail(it.qq)
+            else -> throw NoSuchElementException()
+        }
+
+        val receipt = sendMessage(quote, it.messageChain.toMessageChain(member), member)
+        it.session.sourceCache.offer(receipt.source)
+
+        call.respondDTO(SendRetDTO(messageId = receipt.source.ids.firstOrNull() ?: 0))
+    }
+
+    /**
+     * 发送图片消息
+     */
+    httpAuthedPost<SendImageDTO>("sendImageMessage") {
+        val bot = it.session.bot
+        val contact = when {
+            it.target != null -> bot.getFriend(it.target) ?: bot.getGroupOrFail(it.target)
+            it.qq != null && it.group != null -> bot.getGroupOrFail(it.group).getOrFail(it.qq)
+            it.qq != null -> bot.getFriendOrFail(it.qq)
+            it.group != null -> bot.getGroupOrFail(it.group)
+            else -> throw IllegalParamException("target、qq、group不可全为null")
+        }
+        val ls = it.urls.map { url -> URL(url).openStream().use { stream -> stream.uploadAsImage(contact) } }
+        val receipt = contact.sendMessage(buildMessageChain { addAll(ls) })
+
+        it.session.sourceCache.offer(receipt.source)
+        call.respondJson(ls.map { image -> image.imageId }.toJson())
+    }
+
+    httpAuthedMultiPart("uploadImage") { session, parts ->
+        val type = parts.value("type")
+        parts.file("img")?.apply {
+
+            val image = streamProvider().use {
+                when (type) {
+                    "group" -> session.bot.groups.firstOrNull()?.uploadImage(it)
+                    "friend",
+                    "temp"
+                    -> session.bot.friends.firstOrNull()?.uploadImage(it)
+                    else -> null
+                }
+            }
+
+            image?.apply { call.respondDTO(UploadImageRetDTO(imageId, queryUrl())) }
+                ?: throw IllegalAccessException("图片上传错误")
+
+        } ?: throw IllegalAccessException("未知错误")
+    }
+
+    httpAuthedMultiPart("uploadVoice") { session, parts ->
+        val type = parts.value("type")
+        parts.file("voice")?.apply {
+
+            val voice = streamProvider().use {
+                when (type) {
+                    "group" -> session.bot.groups.firstOrNull()?.uploadVoice(it.toExternalResource())
+                    else -> null
+                }
+            }
+
+            voice?.apply { call.respondDTO(UploadVoiceRetDTO(fileName, url)) }
+                ?: throw IllegalAccessException("语音上传错误")
+
+        } ?: throw IllegalAccessException("未知错误")
+    }
+
+    /**
+     * 撤回消息
+     */
+    httpAuthedPost<RecallDTO>("recall") {
+        it.session.sourceCache[it.target].recall()
+
+        call.respondStateCode(StateCode.Success)
+    }
+}

+ 60 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/router/verify.kt

@@ -0,0 +1,60 @@
+package net.mamoe.mirai.api.http.adapter.http.router
+
+import io.ktor.application.*
+import io.ktor.routing.*
+import net.mamoe.mirai.api.http.adapter.common.StateCode
+import net.mamoe.mirai.api.http.adapter.internal.dto.VerifyRetDTO
+import net.mamoe.mirai.api.http.context.MahContextHolder
+import net.mamoe.mirai.api.http.context.session.AuthedSession
+import net.mamoe.mirai.api.http.util.getBotOrThrow
+
+/**
+ * 授权路由
+ */
+internal fun Application.authRouter() = routing {
+
+    /**
+     * 进行认证
+     */
+    httpVerify("/verify") {
+        if (!MahContextHolder.mahContext.enableVerify) {
+            call.respondStateCode(StateCode.NoOperateSupport)
+            return@httpVerify
+        }
+        if (it.verifyKey != MahContextHolder.mahContext.sessionManager.verifyKey) {
+            call.respondStateCode(StateCode.AuthKeyFail)
+        } else {
+            call.respondDTO(VerifyRetDTO(0, MahContextHolder.sessionManager.createTempSession().key))
+        }
+    }
+
+    /**
+     * 验证并分配session
+     */
+    httpBind("/bind") {
+        if (MahContextHolder.mahContext.singleMode) {
+            call.respondStateCode(StateCode.NoOperateSupport)
+            return@httpBind
+        }
+        val bot = getBotOrThrow(it.qq)
+        if (MahContextHolder[it.sessionKey] !is AuthedSession) {
+            MahContextHolder.sessionManager.authSession(bot, it.sessionKey)
+        }
+        call.respondStateCode(StateCode.Success)
+    }
+
+    /**
+     * 释放session
+     */
+    httpBind("/release") {
+        val bot = getBotOrThrow(it.qq)
+        val session = MahContextHolder[it.sessionKey] as AuthedSession
+        if (bot.id == session.bot.id) {
+            MahContextHolder.sessionManager.closeSession(it.sessionKey)
+            call.respondStateCode(StateCode.Success)
+        } else {
+            throw NoSuchElementException()
+        }
+    }
+
+}

+ 14 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/session/HttpAuthedSession.kt

@@ -0,0 +1,14 @@
+package net.mamoe.mirai.api.http.adapter.http.session
+
+import net.mamoe.mirai.api.http.context.session.IAuthedSession
+
+/**
+ * 代理到 AuthSession
+ *
+ * 使用增强设计模式添加未读消息队列
+ *
+ * @author ryoii
+ */
+class HttpAuthedSession(authedSession: IAuthedSession) : IAuthedSession by authedSession {
+    val unreadQueue: UnreadQueue = UnreadQueue()
+}

+ 38 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/http/session/UnreadQueue.kt

@@ -0,0 +1,38 @@
+package net.mamoe.mirai.api.http.adapter.http.session
+
+import net.mamoe.mirai.api.http.adapter.internal.dto.EventDTO
+import java.util.concurrent.ConcurrentLinkedDeque
+
+/**
+ * 未读消息队列
+ */
+class UnreadQueue : ConcurrentLinkedDeque<EventDTO>() {
+
+    fun fetch(size: Int): List<EventDTO> {
+        var cnt = size
+
+        val ret = ArrayList<EventDTO>(cnt)
+        while (isNotEmpty() && cnt > 0) {
+            ret.add(pop())
+            cnt--
+        }
+
+        return ret
+    }
+
+    fun fetchLatest(size: Int): List<EventDTO> {
+        var cnt = size
+
+        val ret = ArrayList<EventDTO>(cnt)
+        while (isNotEmpty() && cnt > 0) {
+            ret.add(removeLast())
+            cnt--
+        }
+
+        return ret
+    }
+
+    fun peek(size: Int): List<EventDTO> = asSequence().take(size).toList()
+
+    fun peekLatest(size: Int): List<EventDTO> = reversed().asSequence().take(size).toList()
+}

+ 5 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/README.md

@@ -0,0 +1,5 @@
+## 内部公用数据传输对象
+
+用于 `http adapter`, `ws adapter` 的公用参数实体
+
+全为内部实现,外部 `adpater` 应定义自己的参数对象

+ 80 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/convertor/convertor.kt

@@ -0,0 +1,80 @@
+package net.mamoe.mirai.api.http.adapter.internal.convertor
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import net.mamoe.mirai.api.http.adapter.internal.dto.*
+import net.mamoe.mirai.api.http.util.FaceMap
+import net.mamoe.mirai.api.http.util.PokeMap
+import net.mamoe.mirai.api.http.util.toHexArray
+import net.mamoe.mirai.contact.Contact
+import net.mamoe.mirai.contact.Group
+import net.mamoe.mirai.event.events.BotEvent
+import net.mamoe.mirai.event.events.MessageEvent
+import net.mamoe.mirai.message.data.*
+import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
+import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
+import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsVoice
+import net.mamoe.mirai.utils.MiraiExperimentalApi
+import net.mamoe.mirai.utils.MiraiInternalApi
+import java.io.File
+import java.net.URL
+
+internal suspend fun BotEvent.toDTO(): EventDTO = when (this) {
+    is MessageEvent -> toDTO()
+    else -> convertBotEvent()
+}
+
+internal suspend fun MessageChainDTO.toMessageChain(contact: Contact) =
+    buildMessageChain { [email protected] { it.toMessage(contact)?.let(::add) } }
+
+
+@OptIn(MiraiInternalApi::class, MiraiExperimentalApi::class)
+internal suspend fun MessageDTO.toMessage(contact: Contact) = when (this) {
+    is AtDTO -> (contact as Group).getOrFail(target).at()
+    is AtAllDTO -> AtAll
+    is FaceDTO -> when {
+        faceId >= 0 -> Face(faceId)
+        name.isNotEmpty() -> Face(FaceMap[name])
+        else -> Face(255)
+    }
+    is PlainDTO -> PlainText(text)
+    is ImageDTO -> imageLikeToMessage(contact)
+    is FlashImageDTO -> imageLikeToMessage(contact)?.flash()
+    is VoiceDTO -> voiceLikeToMessage(contact)
+    is XmlDTO -> SimpleServiceMessage(60, xml)
+    is JsonDTO -> SimpleServiceMessage(1, json)
+    is AppDTO -> LightApp(content)
+    is PokeMessageDTO -> PokeMap[name]
+    // ignore
+    is QuoteDTO,
+    is MessageSourceDTO,
+    is UnknownMessageDTO
+    -> null
+}
+
+private suspend fun ImageLikeDTO.imageLikeToMessage(contact: Contact) = when {
+    !imageId.isNullOrBlank() -> Image(imageId!!)
+    !url.isNullOrBlank() -> withContext(Dispatchers.IO) { url!!.openStream().uploadAsImage(contact) }
+    !path.isNullOrBlank() -> with(File(path!!)) {
+        if (exists()) {
+            inputStream().use { uploadAsImage(contact) }
+        } else throw NoSuchFileException(this)
+    }
+    else -> null
+}
+
+@MiraiInternalApi
+private suspend fun VoiceLikeDTO.voiceLikeToMessage(contact: Contact) = when {
+    contact !is Group -> null
+    !voiceId.isNullOrBlank() -> Voice(voiceId!!, voiceId!!.substringBefore(".").toHexArray(), 0, 0, "")
+    !url.isNullOrBlank() -> withContext(Dispatchers.IO) { url!!.openStream().toExternalResource().uploadAsVoice(contact) }
+    !path.isNullOrBlank() -> with(File(path!!)) {
+        if (exists()) {
+            inputStream().toExternalResource().use { it.uploadAsVoice(contact) }
+        } else throw NoSuchFileException(this)
+    }
+    else -> null
+}
+
+// TODO: fix memory leak
+private fun String.openStream() = URL(this).openStream()

+ 139 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/convertor/event.kt

@@ -0,0 +1,139 @@
+package net.mamoe.mirai.api.http.adapter.internal.convertor
+
+import net.mamoe.mirai.api.http.adapter.internal.dto.*
+import net.mamoe.mirai.event.events.*
+import net.mamoe.mirai.utils.MiraiExperimentalApi
+
+@OptIn(MiraiExperimentalApi::class)
+internal fun BotEvent.convertBotEvent() = when (this) {
+    is BotOnlineEvent -> BotOnlineEventDTO(bot.id)
+    is BotOfflineEvent.Active -> BotOfflineEventActiveDTO(bot.id)
+    is BotOfflineEvent.Force -> BotOfflineEventForceDTO(bot.id, title, message)
+    is BotOfflineEvent.Dropped -> BotOfflineEventDroppedDTO(bot.id)
+    is BotReloginEvent -> BotReloginEventDTO(bot.id)
+    is MessageRecallEvent.GroupRecall -> GroupRecallEventDTO(
+        authorId,
+        messageIds.firstOrNull() ?: 0,
+        messageTime.toLong() and 0xFFFF,
+        GroupDTO(group),
+        operator?.let(::MemberDTO)
+    )
+    is MessageRecallEvent.FriendRecall -> FriendRecallEventDTO(
+        authorId,
+        messageIds.firstOrNull() ?: 0,
+        messageTime.toLong() and 0xFFFF,
+        operatorId
+    )
+    is BotGroupPermissionChangeEvent -> BotGroupPermissionChangeEventDTO(
+        origin,
+        new,
+        new,
+        GroupDTO(group)
+    )
+    is BotMuteEvent -> BotMuteEventDTO(durationSeconds, MemberDTO(operator))
+    is BotUnmuteEvent -> BotUnmuteEventDTO(MemberDTO(operator))
+    is BotJoinGroupEvent -> BotJoinGroupEventDTO(GroupDTO(group))
+    is BotLeaveEvent.Active -> BotLeaveEventActiveDTO(GroupDTO(group))
+    is BotLeaveEvent.Kick -> BotLeaveEventKickDTO(GroupDTO(group))
+    is GroupNameChangeEvent -> GroupNameChangeEventDTO(
+        origin,
+        new,
+        new,
+        GroupDTO(group),
+        operator?.let(::MemberDTO)
+    )
+    is GroupEntranceAnnouncementChangeEvent -> GroupEntranceAnnouncementChangeEventDTO(
+        origin,
+        new,
+        new,
+        GroupDTO(group),
+        operator?.let(::MemberDTO)
+    )
+    is GroupMuteAllEvent -> GroupMuteAllEventDTO(
+        origin,
+        new,
+        new,
+        GroupDTO(group),
+        operator?.let(::MemberDTO)
+    )
+    is GroupAllowAnonymousChatEvent -> GroupAllowAnonymousChatEventDTO(
+        origin,
+        new,
+        new,
+        GroupDTO(group),
+        operator?.let(::MemberDTO)
+    )
+    is GroupAllowConfessTalkEvent -> GroupAllowConfessTalkEventDTO(
+        origin,
+        new,
+        new,
+        GroupDTO(group),
+        isByBot
+    )
+    is GroupAllowMemberInviteEvent -> GroupAllowMemberInviteEventDTO(
+        origin,
+        new,
+        new,
+        GroupDTO(group),
+        operator?.let(::MemberDTO)
+    )
+    is MemberJoinEvent -> MemberJoinEventDTO(MemberDTO(member))
+    is MemberLeaveEvent.Kick -> MemberLeaveEventKickDTO(
+        MemberDTO(member),
+        operator?.let(::MemberDTO)
+    )
+    is MemberLeaveEvent.Quit -> MemberLeaveEventQuitDTO(MemberDTO(member))
+    is MemberCardChangeEvent -> MemberCardChangeEventDTO(
+        origin,
+        new,
+        new,
+        MemberDTO(member),
+        null // TODO: core改动,暂时使用null
+        //  operator?.let(::MemberDTO)
+    )
+    is MemberSpecialTitleChangeEvent -> MemberSpecialTitleChangeEventDTO(
+        origin,
+        new,
+        new,
+        MemberDTO(member)
+    )
+    is MemberPermissionChangeEvent -> MemberPermissionChangeEventDTO(
+        origin,
+        new,
+        new,
+        MemberDTO(member)
+    )
+    is MemberMuteEvent -> MemberMuteEventDTO(
+        durationSeconds,
+        MemberDTO(member),
+        operator?.let(::MemberDTO)
+    )
+    is MemberUnmuteEvent -> MemberUnmuteEventDTO(
+        MemberDTO(member),
+        operator?.let(::MemberDTO)
+    )
+    is NewFriendRequestEvent -> NewFriendRequestEventDTO(
+        eventId,
+        message,
+        fromId,
+        fromGroupId,
+        fromNick
+    )
+    is MemberJoinRequestEvent -> MemberJoinRequestEventDTO(
+        eventId,
+        message,
+        fromId,
+        groupId,
+        groupName,
+        fromNick
+    )
+    is BotInvitedJoinGroupRequestEvent -> BotInvitedJoinGroupRequestEventDTO(
+        eventId,
+        "",
+        invitorId,
+        groupId,
+        groupName,
+        invitorNick
+    )
+    else -> IgnoreEventDTO
+}

+ 58 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/convertor/message.kt

@@ -0,0 +1,58 @@
+package net.mamoe.mirai.api.http.adapter.internal.convertor
+
+import net.mamoe.mirai.api.http.adapter.internal.dto.*
+import net.mamoe.mirai.api.http.util.FaceMap
+import net.mamoe.mirai.api.http.util.PokeMap
+import net.mamoe.mirai.contact.Group
+import net.mamoe.mirai.event.events.FriendMessageEvent
+import net.mamoe.mirai.event.events.GroupMessageEvent
+import net.mamoe.mirai.event.events.GroupTempMessageEvent
+import net.mamoe.mirai.event.events.MessageEvent
+import net.mamoe.mirai.message.data.*
+import net.mamoe.mirai.message.data.Image.Key.queryUrl
+import net.mamoe.mirai.utils.MiraiExperimentalApi
+
+internal suspend fun MessageEvent.toDTO() = when (this) {
+    is FriendMessageEvent -> FriendMessagePacketDTO(QQDTO(sender))
+    is GroupMessageEvent -> GroupMessagePacketDTO(MemberDTO(sender))
+    is GroupTempMessageEvent -> TempMessagePacketDto(MemberDTO(sender))
+    else -> IgnoreEventDTO
+}.apply {
+    if (this is MessagePacketDTO) {
+        messageChain = message.toDTO { it != UnknownMessageDTO }
+    }
+}
+
+internal suspend fun MessageChain.toDTO(filter: (MessageDTO) -> Boolean): MessageChainDTO =
+    mutableListOf<MessageDTO>().apply {
+        this@toDTO[MessageSource]?.let { add(it.toDTO()) }
+        this@toDTO[QuoteReply]?.let { add(it.toDTO()) }
+
+        [email protected] { content ->
+            content.toDTO().takeIf(filter)?.let { add(it) }
+        }
+    }
+
+@OptIn(MiraiExperimentalApi::class)
+internal suspend fun Message.toDTO() = when (this) {
+    is MessageSource -> MessageSourceDTO(ids.firstOrNull() ?: 0, time)
+    is At -> AtDTO(target, "")
+    is AtAll -> AtAllDTO(0L)
+    is Face -> FaceDTO(id, FaceMap[id])
+    is PlainText -> PlainDTO(content)
+    is Image -> ImageDTO(imageId, queryUrl())
+    is FlashImage -> FlashImageDTO(image.imageId, image.queryUrl())
+    is Voice -> VoiceDTO(fileName, url)
+    is ServiceMessage -> XmlDTO(content)
+    is LightApp -> AppDTO(content)
+    is QuoteReply -> QuoteDTO(source.ids.firstOrNull() ?: 0, source.fromId, source.targetId,
+        groupId = when {
+            source is OfflineMessageSource && (source as OfflineMessageSource).kind == MessageSourceKind.GROUP ||
+                    source is OnlineMessageSource && (source as OnlineMessageSource).subject is Group -> source.targetId
+            else -> 0L
+        },
+        // 避免套娃
+        origin = source.originalMessage.toDTO { it != UnknownMessageDTO && it !is QuoteDTO })
+    is PokeMessage -> PokeMessageDTO(PokeMap[pokeType])
+    else -> UnknownMessageDTO
+}

+ 23 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/dto/auth.kt

@@ -0,0 +1,23 @@
+package net.mamoe.mirai.api.http.adapter.internal.dto
+
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+import net.mamoe.mirai.api.http.adapter.http.session.HttpAuthedSession
+import net.mamoe.mirai.api.http.context.MahContext
+
+@Serializable
+data class VerifyDTO(val verifyKey: String) : DTO
+
+@Serializable
+data class VerifyRetDTO(val code: Int, val session: String) : DTO
+
+@Serializable
+abstract class AuthedDTO : DTO {
+    val sessionKey: String = MahContext.SINGLE_SESSION_KEY
+
+    @Transient
+    lateinit var session: HttpAuthedSession // 反序列化验证成功后传入
+}
+
+@Serializable
+data class BindDTO(val qq: Long) : AuthedDTO()

+ 4 - 13
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/common/DTO.kt → mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/dto/base.kt

@@ -7,25 +7,16 @@
  * https://github.com/mamoe/mirai/blob/master/LICENSE
  */
 
-package net.mamoe.mirai.api.http.data.common
+package net.mamoe.mirai.api.http.adapter.internal.dto
 
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.Transient
-import net.mamoe.mirai.api.http.context.session.manager.AuthedSession
+import net.mamoe.mirai.api.http.context.MahContext
+import net.mamoe.mirai.api.http.context.session.AuthedSession
 
 interface DTO
 
-@Serializable
-data class AuthDTO(val authKey: String) : DTO
-
-@Serializable
-abstract class VerifyDTO : DTO {
-    abstract val sessionKey: String
-    @Transient
-    lateinit var session: AuthedSession // 反序列化验证成功后传入
-}
-
 @Serializable
 abstract class EventDTO : DTO
 
-object IgnoreEventDTO : EventDTO()
+object IgnoreEventDTO : EventDTO()

+ 3 - 13
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/common/ContactDTO.kt → mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/dto/contact.kt

@@ -1,13 +1,4 @@
-/*
- * Copyright 2020 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/master/LICENSE
- */
-
-package net.mamoe.mirai.api.http.data.common
+package net.mamoe.mirai.api.http.adapter.internal.dto
 
 import kotlinx.serialization.Serializable
 import net.mamoe.mirai.contact.*
@@ -23,9 +14,8 @@ data class QQDTO(
     val nickname: String,
     val remark: String
 ) : ContactDTO() {
-    // TODO: queryProfile.nickname & queryRemark.value not support now
-    constructor(qq: Friend) : this(qq.id, qq.nick, "")
-    constructor(qq: Stranger) : this(qq.id, qq.nick, "")
+    constructor(qq: Friend) : this(qq.id, qq.nick, qq.remark)
+    constructor(qq: Stranger) : this(qq.id, qq.nick, qq.remark)
 }
 
 

+ 1 - 146
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/common/BotEventDTO.kt → mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/dto/event.kt

@@ -1,13 +1,4 @@
-/*
- * Copyright 2020 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/master/LICENSE
- */
-
-package net.mamoe.mirai.api.http.data.common
+package net.mamoe.mirai.api.http.adapter.internal.dto
 
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
@@ -17,142 +8,6 @@ import net.mamoe.mirai.event.events.*
 @Serializable
 sealed class BotEventDTO : EventDTO()
 
-suspend fun BotEvent.toDTO() = when (this) {
-    is MessageEvent -> toDTO()
-    else -> when (this) {
-        is BotOnlineEvent -> BotOnlineEventDTO(bot.id)
-        is BotOfflineEvent.Active -> BotOfflineEventActiveDTO(bot.id)
-        is BotOfflineEvent.Force -> BotOfflineEventForceDTO(bot.id, title, message)
-        is BotOfflineEvent.Dropped -> BotOfflineEventDroppedDTO(bot.id)
-        is BotReloginEvent -> BotReloginEventDTO(bot.id)
-        is MessageRecallEvent.GroupRecall -> GroupRecallEventDTO(
-            authorId,
-            messageIds.firstOrNull() ?: 0,
-            messageTime.toLong() and 0xFFFF,
-            GroupDTO(group),
-            operator?.let(::MemberDTO)
-        )
-        is MessageRecallEvent.FriendRecall -> FriendRecallEventDTO(
-            authorId,
-            messageIds.firstOrNull() ?: 0,
-            messageTime.toLong() and 0xFFFF,
-            operatorId
-        )
-        is BotGroupPermissionChangeEvent -> BotGroupPermissionChangeEventDTO(
-            origin,
-            new,
-            new,
-            GroupDTO(group)
-        )
-        is BotMuteEvent -> BotMuteEventDTO(durationSeconds, MemberDTO(operator))
-        is BotUnmuteEvent -> BotUnmuteEventDTO(MemberDTO(operator))
-        is BotJoinGroupEvent -> BotJoinGroupEventDTO(GroupDTO(group))
-        is BotLeaveEvent.Active -> BotLeaveEventActiveDTO(GroupDTO(group))
-        is BotLeaveEvent.Kick -> BotLeaveEventKickDTO(GroupDTO(group))
-        is GroupNameChangeEvent -> GroupNameChangeEventDTO(
-            origin,
-            new,
-            new,
-            GroupDTO(group),
-            operator?.let(::MemberDTO)
-        )
-        is GroupEntranceAnnouncementChangeEvent -> GroupEntranceAnnouncementChangeEventDTO(
-            origin,
-            new,
-            new,
-            GroupDTO(group),
-            operator?.let(::MemberDTO)
-        )
-        is GroupMuteAllEvent -> GroupMuteAllEventDTO(
-            origin,
-            new,
-            new,
-            GroupDTO(group),
-            operator?.let(::MemberDTO)
-        )
-        is GroupAllowAnonymousChatEvent -> GroupAllowAnonymousChatEventDTO(
-            origin,
-            new,
-            new,
-            GroupDTO(group),
-            operator?.let(::MemberDTO)
-        )
-        is GroupAllowConfessTalkEvent -> GroupAllowConfessTalkEventDTO(
-            origin,
-            new,
-            new,
-            GroupDTO(group),
-            isByBot
-        )
-        is GroupAllowMemberInviteEvent -> GroupAllowMemberInviteEventDTO(
-            origin,
-            new,
-            new,
-            GroupDTO(group),
-            operator?.let(::MemberDTO)
-        )
-        is MemberJoinEvent -> MemberJoinEventDTO(MemberDTO(member))
-        is MemberLeaveEvent.Kick -> MemberLeaveEventKickDTO(
-            MemberDTO(member),
-            operator?.let(::MemberDTO)
-        )
-        is MemberLeaveEvent.Quit -> MemberLeaveEventQuitDTO(MemberDTO(member))
-        is MemberCardChangeEvent -> MemberCardChangeEventDTO(
-            origin,
-            new,
-            new,
-            MemberDTO(member),
-            null // TODO: core改动,暂时使用null
-            //  operator?.let(::MemberDTO)
-        )
-        is MemberSpecialTitleChangeEvent -> MemberSpecialTitleChangeEventDTO(
-            origin,
-            new,
-            new,
-            MemberDTO(member)
-        )
-        is MemberPermissionChangeEvent -> MemberPermissionChangeEventDTO(
-            origin,
-            new,
-            new,
-            MemberDTO(member)
-        )
-        is MemberMuteEvent -> MemberMuteEventDTO(
-            durationSeconds,
-            MemberDTO(member),
-            operator?.let(::MemberDTO)
-        )
-        is MemberUnmuteEvent -> MemberUnmuteEventDTO(
-            MemberDTO(member),
-            operator?.let(::MemberDTO)
-        )
-        is NewFriendRequestEvent -> NewFriendRequestEventDTO(
-            eventId,
-            message,
-            fromId,
-            fromGroupId,
-            fromNick
-        )
-        is MemberJoinRequestEvent -> MemberJoinRequestEventDTO(
-            eventId,
-            message,
-            fromId,
-            groupId,
-            groupName,
-            fromNick
-        )
-        is BotInvitedJoinGroupRequestEvent -> BotInvitedJoinGroupRequestEventDTO(
-            eventId,
-            "",
-            invitorId,
-            groupId,
-            groupName,
-            invitorNick
-        )
-        else -> IgnoreEventDTO
-    }
-}
-
 @Serializable
 @SerialName("BotOnlineEvent")
 data class BotOnlineEventDTO(val qq: Long) : BotEventDTO()

+ 121 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/dto/message.kt

@@ -0,0 +1,121 @@
+package net.mamoe.mirai.api.http.adapter.internal.dto
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+sealed class MessagePacketDTO : EventDTO() {
+    lateinit var messageChain: MessageChainDTO
+}
+
+typealias MessageChainDTO = List<MessageDTO>
+
+@Serializable
+@SerialName("FriendMessage")
+data class FriendMessagePacketDTO(val sender: QQDTO) : MessagePacketDTO()
+
+@Serializable
+@SerialName("GroupMessage")
+data class GroupMessagePacketDTO(val sender: MemberDTO) : MessagePacketDTO()
+
+@Serializable
+@SerialName("TempMessage")
+data class TempMessagePacketDto(val sender: MemberDTO) : MessagePacketDTO()
+
+@Serializable
+@SerialName("StrangerMessage")
+data class StrangerMessagePacketDto(val sender: QQDTO) : MessagePacketDTO()
+
+
+// Message
+@Serializable
+@SerialName("Source")
+data class MessageSourceDTO(val id: Int, val time: Int) : MessageDTO()
+
+@Serializable
+@SerialName("At")
+data class AtDTO(val target: Long, val display: String = "") : MessageDTO()
+
+@Serializable
+@SerialName("AtAll")
+data class AtAllDTO(val target: Long = 0) : MessageDTO() // target为保留字段
+
+@Serializable
+@SerialName("Face")
+data class FaceDTO(val faceId: Int = -1, val name: String = "") : MessageDTO()
+
+@Serializable
+@SerialName("Plain")
+data class PlainDTO(val text: String) : MessageDTO()
+
+internal interface ImageLikeDTO {
+    val imageId: String?
+    val url: String?
+    val path: String?
+}
+
+internal interface VoiceLikeDTO {
+    val voiceId: String?
+    val url: String?
+    val path: String?
+}
+
+@Serializable
+@SerialName("Image")
+data class ImageDTO(
+    override val imageId: String? = null,
+    override val url: String? = null,
+    override val path: String? = null
+) : MessageDTO(), ImageLikeDTO
+
+@Serializable
+@SerialName("FlashImage")
+data class FlashImageDTO(
+    override val imageId: String? = null,
+    override val url: String? = null,
+    override val path: String? = null
+) : MessageDTO(), ImageLikeDTO
+
+@Serializable
+@SerialName("Voice")
+data class VoiceDTO(
+    override val voiceId: String? = null,
+    override val url: String? = null,
+    override val path: String? = null
+) : MessageDTO(), VoiceLikeDTO
+
+@Serializable
+@SerialName("Xml")
+data class XmlDTO(val xml: String) : MessageDTO()
+
+@Serializable
+@SerialName("Json")
+data class JsonDTO(val json: String) : MessageDTO()
+
+@Serializable
+@SerialName("App")
+data class AppDTO(val content: String) : MessageDTO()
+
+@Serializable
+@SerialName("Quote")
+data class QuoteDTO(
+    val id: Int,
+    val senderId: Long,
+    val targetId: Long,
+    val groupId: Long,
+    val origin: MessageChainDTO
+) : MessageDTO()
+
+@Serializable
+@SerialName("Poke")
+data class PokeMessageDTO(
+    val name: String
+) : MessageDTO()
+
+@Serializable
+@SerialName("Unknown")
+object UnknownMessageDTO : MessageDTO()
+
+@Serializable
+sealed class MessageDTO : DTO
+

+ 111 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/dto/parameter.kt

@@ -0,0 +1,111 @@
+package net.mamoe.mirai.api.http.adapter.internal.dto
+
+import kotlinx.serialization.Serializable
+import net.mamoe.mirai.contact.Group
+import net.mamoe.mirai.contact.Member
+
+@Serializable
+internal data class SendDTO(
+    val quote: Int? = null,
+    val target: Long? = null,
+    val qq: Long? = null,
+    val group: Long? = null,
+    val messageChain: MessageChainDTO
+) : AuthedDTO()
+
+@Serializable
+internal data class SendImageDTO(
+    val target: Long? = null,
+    val qq: Long? = null,
+    val group: Long? = null,
+    val urls: List<String>
+) : AuthedDTO()
+
+@Serializable
+@Suppress("unused")
+internal class SendRetDTO(
+    val code: Int = 0,
+    val msg: String = "success",
+    val messageId: Int
+) : DTO
+
+@Serializable
+@Suppress("unused")
+internal class UploadImageRetDTO(
+    val imageId: String,
+    val url: String,
+    val path: String? = ""
+) : DTO
+
+@Serializable
+@Suppress("unused")
+internal class UploadVoiceRetDTO(
+    val voiceId: String,
+    val url: String?,
+    val path: String? = ""
+) : DTO
+
+@Serializable
+internal data class RecallDTO(
+    val target: Int
+) : AuthedDTO()
+
+
+@Serializable
+internal data class MuteDTO(
+    val target: Long,
+    val memberId: Long = 0,
+    val time: Int = 0
+) : AuthedDTO()
+
+@Serializable
+internal data class KickDTO(
+    val target: Long,
+    val memberId: Long,
+    val msg: String = ""
+) : AuthedDTO()
+
+@Serializable
+internal data class QuitDTO(
+    val target: Long
+) : AuthedDTO()
+
+@Serializable
+internal data class GroupConfigDTO(
+    val target: Long,
+    val config: GroupDetailDTO
+) : AuthedDTO()
+
+@Serializable
+internal data class GroupDetailDTO(
+    val name: String? = null,
+    val announcement: String? = null,
+    val confessTalk: Boolean? = null,
+    val allowMemberInvite: Boolean? = null,
+    val autoApprove: Boolean? = null,
+    val anonymousChat: Boolean? = null
+) : DTO {
+    constructor(group: Group) : this(
+        group.name,
+        group.settings.entranceAnnouncement,
+        false,
+        group.settings.isAllowMemberInvite,
+        group.settings.isAutoApproveEnabled,
+        group.settings.isAnonymousChatEnabled
+    )
+}
+
+@Serializable
+internal data class MemberInfoDTO(
+    val target: Long,
+    val memberId: Long,
+    val info: MemberDetailDTO
+) : AuthedDTO()
+
+@Serializable
+internal data class MemberDetailDTO(
+    val name: String? = null,
+    val specialTitle: String? = null
+) : DTO {
+    constructor(member: Member) : this(member.nameCard, member.specialTitle)
+}

+ 38 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/dto/restful.kt

@@ -0,0 +1,38 @@
+package net.mamoe.mirai.api.http.adapter.internal.dto
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ListRestfulResult(
+    val code: Int = 0,
+    val msg: String = "",
+    val data: List<DTO>
+) : DTO
+
+@Serializable
+data class IntRestfulResult(
+    val code: Int = 0,
+    val msg: String = "",
+    val data: Int
+) : DTO
+
+@Serializable
+data class EventListRestfulResult(
+    val code: Int = 0,
+    val msg: String = "",
+    val data: List<EventDTO>
+) : DTO
+
+@Serializable
+data class EventRestfulResult(
+    val code: Int = 0,
+    val msg: String = "",
+    val data: EventDTO?
+) : DTO
+
+@Serializable
+data class StringMapRestfulResult(
+    val code: Int = 0,
+    val msg: String = "",
+    val data: Map<String, String>
+) : DTO

+ 26 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/serializer/extensions.kt

@@ -0,0 +1,26 @@
+package net.mamoe.mirai.api.http.adapter.internal.serializer
+
+import net.mamoe.mirai.api.http.context.serializer.InternalSerializerHolder
+
+/**
+ * 序列化 object
+ */
+internal inline fun <reified T : Any> T.toJson(): String =
+    InternalSerializerHolder.serializer.encode(this)
+
+
+/**
+ * 序列化列表
+ */
+internal inline fun <reified T : Any> List<T>.toJson(): String =
+    InternalSerializerHolder.serializer.encode(this)
+
+
+/**
+ * 解析 object,可指定序列化器
+ *
+ * 异常时返回 null
+ */
+internal inline fun <reified T : Any> String.jsonParseOrNull(): T? = runCatching<T> {
+    InternalSerializerHolder.serializer.decode(this)
+}.getOrNull()

+ 40 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/serializer/internalSerializer.kt

@@ -0,0 +1,40 @@
+package net.mamoe.mirai.api.http.adapter.internal.serializer
+
+import net.mamoe.mirai.api.http.adapter.http.HttpAdapter
+import net.mamoe.mirai.api.http.adapter.ws.WebsocketAdapter
+import kotlin.reflect.KClass
+
+
+/**
+ * 内部序列化接口,处理 [HttpAdapter], [WebsocketAdapter] 等内部实现 Adapter 的数据序列化
+ * <P>
+ * 提供外部接口为了在使用这些内部实现的 Adapter 时,可以复用 Adapter 的交互逻辑,但序列化进行解耦
+ *
+ * {@see [JsonSerializer]}
+ */
+interface InternalSerializer {
+
+    /**
+     * 序列化方法
+     */
+    fun <T : Any> encode(dto: T, clazz: KClass<T>): String
+
+    /**
+     * 序列化列表
+     */
+    fun <T : Any> encode(list: List<T>, clazz: KClass<T>): String
+
+    /**
+     * 反序列化方法
+     */
+    fun <T : Any> decode(content: String, clazz: KClass<T>): T
+}
+
+/**
+ * 以下为解决多态情况下无法处理泛型的问题,利用扩展函数带入泛型上下文
+ */
+inline fun <reified T : Any> InternalSerializer.encode(dto: T) = encode(dto, T::class)
+
+inline fun <reified T : Any> InternalSerializer.encode(collection: List<T>) = encode(collection, T::class)
+
+inline fun <reified T : Any> InternalSerializer.decode(content: String) = decode(content, T::class)

+ 61 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/serializer/json.kt

@@ -0,0 +1,61 @@
+package net.mamoe.mirai.api.http.adapter.internal.serializer
+
+import kotlinx.serialization.*
+import kotlinx.serialization.builtins.ListSerializer
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.modules.SerializersModule
+import kotlinx.serialization.modules.SerializersModuleBuilder
+import net.mamoe.mirai.api.http.adapter.common.StateCode
+import net.mamoe.mirai.api.http.adapter.internal.dto.*
+import kotlin.reflect.KClass
+
+@OptIn(InternalSerializationApi::class)
+internal class JsonSerializer : InternalSerializer {
+
+    /**
+     * Json解析规则,需要注册支持的多态的类
+     */
+    private val json by lazy {
+        Json {
+            encodeDefaults = true
+            isLenient = true
+            ignoreUnknownKeys = true
+
+            @Suppress("UNCHECKED_CAST")
+            serializersModule = SerializersModule {
+                polymorphicSealedClass(EventDTO::class, MessagePacketDTO::class)
+                polymorphicSealedClass(EventDTO::class, BotEventDTO::class)
+//                polymorphicSealedClass(StateCode::class, StateCode::class)
+            }
+        }
+    }
+
+    /**
+     * 从 sealed class 里注册到多态序列化
+     */
+    @InternalSerializationApi
+    @Suppress("UNCHECKED_CAST")
+    private fun <B : Any, S : B> SerializersModuleBuilder.polymorphicSealedClass(
+        baseClass: KClass<B>,
+        sealedClass: KClass<S>
+    ) {
+        sealedClass.sealedSubclasses.forEach {
+            val c = it as KClass<S>
+            polymorphic(baseClass, c, c.serializer())
+        }
+    }
+
+    override fun <T : Any> encode(dto: T, clazz: KClass<T>): String = when (dto) {
+        is StateCode -> json.encodeToString(StateCode.serializer(), dto)
+        else -> json.encodeToString(clazz.serializer(), dto)
+    }
+
+    override fun <T : Any> encode(list: List<T>, clazz: KClass<T>): String {
+        return json.encodeToString(ListSerializer(clazz.serializer()), list)
+    }
+
+    override fun <T : Any> decode(content: String, clazz: KClass<T>): T {
+        return json.decodeFromString(clazz.serializer(), content)
+    }
+
+}

+ 110 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/webhook/ReportService.kt

@@ -0,0 +1,110 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.api.http.adapter.webhook
+//
+//import kotlinx.coroutines.launch
+//import net.mamoe.mirai.api.http.setting.Setting
+//import net.mamoe.mirai.api.http.adapter.internal.dto.IgnoreEventDTO
+//import net.mamoe.mirai.api.http.data.common.toDTO
+//import net.mamoe.mirai.api.http.service.MiraiApiHttpService
+//import net.mamoe.mirai.api.http.util.HttpClient
+//import net.mamoe.mirai.api.http.util.toJson
+//import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
+//import net.mamoe.mirai.event.GlobalEventChannel
+//import net.mamoe.mirai.event.Listener
+//import net.mamoe.mirai.event.events.*
+//import net.mamoe.mirai.utils.error
+//
+///**
+// * 上报服务
+// */
+//class ReportService(
+//    /**
+//     * 插件对象
+//     */
+//    override val console: JvmPlugin
+//) : MiraiApiHttpService {
+//
+//    /**
+//     * 心跳配置
+//     */
+//    private val reportConfig get() = Setting.report
+//
+//    /**
+//     * 事件监听器
+//     */
+//    private var subscription: Listener<BotEvent>? = null
+//
+//    override fun onLoad() {
+//    }
+//
+//    override fun onEnable() {
+//        subscription = GlobalEventChannel.subscribeAlways {
+//            this.takeIf { reportConfig.enable }
+//                ?.apply {
+//                    this.takeIf { reportConfig.eventMessage.report }
+//                        ?.takeIf { event -> event !is MessageEvent }
+//                        ?.toDTO()
+//                        ?.takeIf { dto -> dto != IgnoreEventDTO }
+//                        ?.apply {
+//                            reportAllDestinations(this.toJson(), bot.id)
+//                        }
+//
+//                    this.takeIf { reportConfig.groupMessage.report }
+//                        ?.takeIf { event -> event is GroupMessageEvent }
+//                        ?.apply {
+//                            reportAllDestinations(this.toDTO().toJson(), bot.id)
+//                        }
+//
+////                    this.takeIf { reportConfig.tempMessage.report }
+////                        ?.takeIf { event -> event is TempMessageEvent }
+////                        ?.apply {
+////                            reportAllDestinations(this.toDTO().toJson(), bot.id)
+////                        }
+//
+//                    this.takeIf { reportConfig.friendMessage.report }
+//                        ?.takeIf { event -> event is FriendMessageEvent }
+//                        ?.apply {
+//                            reportAllDestinations(this.toDTO().toJson(), bot.id)
+//                        }
+//                }
+//        }
+//
+//        console.logger.info("上报模块启用状态: ${reportConfig.enable}")
+//    }
+//
+//    override fun onDisable() {
+//        subscription?.complete()
+//
+//        console.logger.info("上报模块已禁用")
+//    }
+//
+//    /**
+//     * 上报到所有目标地址
+//     */
+//    private fun reportAllDestinations(json: String, botId: Long) {
+//        console.launch {
+//            reportConfig.destinations.forEach {
+//                report(it, json, botId)
+//            }
+//        }
+//    }
+//
+//    /**
+//     * 上报到指定目标地址
+//     */
+//    private suspend fun report(destination: String, json: String, botId: Long) {
+//        try {
+//            HttpClient.post(destination, json, reportConfig.extraHeaders, botId)
+//        } catch (e: Exception) {
+//            console.logger.error { "上报${destination}失败: ${e.message}" }
+//        }
+//    }
+//}

+ 20 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/ws/WebsocketAdapter.kt

@@ -0,0 +1,20 @@
+package net.mamoe.mirai.api.http.adapter.ws
+
+import io.ktor.application.*
+import net.mamoe.mirai.api.http.adapter.MahKtorAdapter
+import net.mamoe.mirai.api.http.adapter.MahKtorAdapterInitBuilder
+import net.mamoe.mirai.api.http.adapter.ws.router.websocketRouteModule
+import net.mamoe.mirai.event.events.BotEvent
+
+class WebsocketAdapter : MahKtorAdapter("ws") {
+
+    override fun MahKtorAdapterInitBuilder.initKtorAdapter() {
+        host = "localhost"
+        port = 8080
+        module(Application::websocketRouteModule)
+    }
+
+    override suspend fun onReceiveBotEvent(event: BotEvent) {
+
+    }
+}

+ 86 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/ws/router/base.kt

@@ -0,0 +1,86 @@
+package net.mamoe.mirai.api.http.adapter.ws.router
+
+import io.ktor.application.*
+import io.ktor.http.cio.websocket.*
+import io.ktor.routing.*
+import io.ktor.util.pipeline.*
+import io.ktor.websocket.*
+import net.mamoe.mirai.api.http.adapter.common.StateCode
+import net.mamoe.mirai.api.http.adapter.internal.serializer.toJson
+import net.mamoe.mirai.api.http.context.MahContextHolder
+import net.mamoe.mirai.api.http.context.session.AuthedSession
+import net.mamoe.mirai.api.http.context.session.TempSession
+
+fun Application.websocketRouteModule() {
+    install(WebSockets)
+
+    router()
+}
+
+private fun Application.router() = routing {
+
+    /**
+     * 广播通知消息
+     */
+    miraiWebsocket("/message") { session ->
+//        val listener = session.bot.eventChannel.subscribeMessages {
+//            content { bot === session.bot }.invoke {
+//                this.toDTO().takeIf { dto -> dto != IgnoreEventDTO }?.apply {
+//                    outgoing.send(Frame.Text(this.toJson()))
+//                }
+//            }
+//        }
+        for (frame in incoming) {
+            outgoing.send(frame)
+        }
+    }
+
+    /**
+     * 广播通知事件
+     */
+    miraiWebsocket("/event") { session ->
+        for (frame in incoming) {
+            outgoing.send(frame)
+        }
+    }
+
+    /**
+     * 广播通知所有信息(消息,事件)
+     */
+    miraiWebsocket("/all") { session ->
+        for (frame in incoming) {
+            outgoing.send(frame)
+        }
+    }
+}
+
+@ContextDsl
+private inline fun Route.miraiWebsocket(
+    path: String,
+    crossinline body: suspend DefaultWebSocketServerSession.(AuthedSession) -> Unit
+) {
+    webSocket(path) {
+        val sessionKey = call.parameters["sessionKey"]
+        if (sessionKey == null) {
+            outgoing.send(Frame.Text(StateCode.IllegalAccess("参数格式错误").toJson()))
+            close(CloseReason(CloseReason.Codes.NORMAL, "参数格式错误"))
+            return@webSocket
+        }
+
+        val session = MahContextHolder[sessionKey]
+
+        if (session == null) {
+            outgoing.send(Frame.Text(StateCode.IllegalSession.toJson()))
+            close(CloseReason(CloseReason.Codes.NORMAL, "Session失效或不存在"))
+            return@webSocket
+        }
+        if (session is TempSession) {
+            outgoing.send(Frame.Text(StateCode.NotVerifySession.toJson()))
+            close(CloseReason(4, "Session未认证"))
+            return@webSocket
+        }
+
+
+        body(session as AuthedSession)
+    }
+}

+ 0 - 29
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/cache/Cache.kt

@@ -1,29 +0,0 @@
-package net.mamoe.mirai.api.http.cache
-
-/**
- * 缓存接口
- * @author ryoii
- */
-interface Cache<K, V, C> {
-
-    fun push(k: K, v: V, c: C?)
-
-    fun pushCache(k: K, c: C)
-
-    operator fun get(k: K): C?
-
-    fun createContext(): CacheContext<K, V, C>
-}
-
-/**
- * 缓存上下文, 需要代理缓存接口
- * @author ryoii
- */
-interface CacheContext<K, V, C> : Cache<K, V, C> {
-
-    fun next(): V?
-
-    fun next(n: Int): List<V>
-
-    fun remain(): Int
-}

+ 0 - 73
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/cache/FixedCache.kt

@@ -1,73 +0,0 @@
-package net.mamoe.mirai.api.http.cache
-
-import net.mamoe.mirai.api.http.util.whenFalseOrNull
-
-/**
- * 固定大小缓存池
- *
- * 由于固定大小,当读取较慢时,当前 context 可能会丢失整个缓存的数据
- * 避免缓存丢失,需要根据写入和读取的速度,配置合理的容量
- */
-class FixedCache<K, V, C>(private val cap: Int) : Cache<K, V, C> {
-
-    @Volatile
-    private var cur = 0
-    private val index = IndexLinkedHashMap<K, C>(cap)
-    private var slot: Array<Node<K, V>> = Array(cap) { Node(null, null) }
-
-    override fun push(k: K, v: V, c: C?) = synchronized(this) {
-        c?.let { pushCache(k, it) }
-        with(slot[cur]) {
-            key = k
-            value = v
-        }
-        cur = shift(cur)
-    }
-
-
-    override fun pushCache(k: K, c: C) {
-        index[k] = c
-    }
-
-    override operator fun get(k: K): C? = index[k]
-
-    internal fun next(context: FixedCacheContext<K, V, C>): V? = synchronized(context) {
-        return@synchronized whenFalseOrNull(context.offset == cur) {
-            internalNext(context)
-        }
-    }
-
-    internal fun next(n: Int, context: FixedCacheContext<K, V, C>): List<V> = synchronized(context) {
-        return MutableList(n.coerceAtMost(context.remain())) { internalNext(context) }
-    }
-
-    private fun internalNext(context: FixedCacheContext<K, V, C>): V {
-        val v = slot[context.offset].value
-        context.offset = shift(context.offset)
-        return v!!
-    }
-
-    override fun createContext() = FixedCacheContext(this, cur)
-
-    private fun shift(pos: Int): Int = if (pos < cap - 1) pos + 1 else 0
-
-    internal fun remain(offset: Int) = if (offset <= cur) cur - offset else cap - offset + cur
-
-    private data class Node<K, V>(var key: K?, var value: V?)
-}
-
-class FixedCacheContext<K, V, C> internal constructor(
-    private val cache: FixedCache<K, V, C>,
-    @Volatile var offset: Int
-) : CacheContext<K, V, C>, Cache<K, V, C> by cache {
-
-    override fun get(k: K): C = cache[k] ?: throw NoSuchElementException()
-
-    override fun next(): V? = cache.next(this)
-
-    override fun createContext() = this
-
-    override fun next(n: Int): List<V> = cache.next(n, this)
-
-    override fun remain(): Int = cache.remain(offset)
-}

+ 0 - 8
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/cache/IndexLinkedHashMap.kt

@@ -1,8 +0,0 @@
-package net.mamoe.mirai.api.http.cache
-
-class IndexLinkedHashMap<K, V>(private val cap: Int) : LinkedHashMap<K, V>() {
-
-    override fun removeEldestEntry(eldest: MutableMap.MutableEntry<K, V>?): Boolean {
-        return size > cap
-    }
-}

+ 0 - 89
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/cache/LinkedCache.kt

@@ -1,89 +0,0 @@
-package net.mamoe.mirai.api.http.cache
-
-/**
- * 无界链表缓存池
- *
- * 存储的节点是无界的,但是缓存是有大小限制的
- * 当存储的节点缓存失效时,依靠 context 维持引用,当节点被消费后,依靠 GC 进行回收
- */
-class LinkedCache<K, V, C>(private val cap: Int) : Cache<K, V, C> {
-
-    private val mod = 100000007;
-
-    @Volatile
-    private var head: Node<K, V>
-
-    @Volatile
-    private var tail: Node<K, V>
-
-    private val index = IndexLinkedHashMap<K, C>(cap)
-
-    init {
-        val dummy = Node<K, V>(0, null, null, null)
-        head = dummy
-        tail = dummy
-    }
-
-    override fun push(k: K, v: V, c: C?): Unit = synchronized(this) {
-        if (c != null) {
-            pushCache(k, c)
-        }
-        offer(k, v)
-        if (remain(head) == cap) {
-            head.next?.let { head = it }
-        }
-    }
-
-    override fun pushCache(k: K, c: C) {
-        index[k] = c
-    }
-
-    override fun get(k: K): C? = index[k]
-
-    internal fun next(context: LinkedCacheContext<K, V, C>): V? = synchronized(context) {
-        return@synchronized context.pos.next?.let {
-            context.pos = it
-            it.value
-        }
-    }
-
-    internal fun next(n: Int, context: LinkedCacheContext<K, V, C>): List<V> = synchronized(context) {
-        val list = mutableListOf<V>()
-        repeat(n) {
-            val next = next(context) ?: return@synchronized list
-            list.add(next)
-        }
-        return@synchronized list
-    }
-
-    override fun createContext() = LinkedCacheContext(this, tail)
-
-    private fun offer(k: K, v: V): Node<K, V> = synchronized(this) {
-        val newNode = Node((tail.no + 1) % mod, k, v, null)
-        tail.next = newNode
-        tail = newNode
-        newNode
-    }
-
-    internal fun remain(node: Node<K, V>): Int = if (node.no <= tail.no) {
-        tail.no - node.no
-    } else {
-        mod - node.no + tail.no
-    }
-
-    internal data class Node<K, V>(val no: Int, val key: K?, val value: V?, var next: Node<K, V>?)
-}
-
-class LinkedCacheContext<K, V, C> internal constructor(
-    private val cache: LinkedCache<K, V, C>,
-    @Volatile internal var pos: LinkedCache.Node<K, V>
-) : CacheContext<K, V, C>, Cache<K, V, C> by cache {
-
-    override fun get(k: K): C = cache[k] ?: throw NoSuchElementException()
-
-    override fun next(): V? = cache.next(this)
-
-    override fun next(n: Int): List<V> = cache.next(n, this)
-
-    override fun remain(): Int = cache.remain(pos)
-}

+ 0 - 20
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/command/RegisterCommand.kt

@@ -1,20 +0,0 @@
-package net.mamoe.mirai.api.http.command
-
-import net.mamoe.mirai.api.http.HttpApiPluginBase
-import net.mamoe.mirai.console.command.CommandSender
-import net.mamoe.mirai.console.command.RawCommand
-import net.mamoe.mirai.message.data.MessageChain
-
-//internal class RegisterCommand(
-//    description: String,
-//    override val usage: String,
-//    vararg names: String,
-//) : RawCommand(
-//    HttpApiPluginBase,
-//    names,
-//    description = description
-//) {
-//    override suspend fun CommandSender.onCommand(args: MessageChain) {
-//        // do nothing
-//    }
-//}

+ 0 - 104
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/config/Setting.kt

@@ -1,104 +0,0 @@
-package net.mamoe.mirai.api.http.config
-
-import kotlinx.serialization.Serializable
-import net.mamoe.mirai.api.http.HttpApiPluginBase
-import net.mamoe.mirai.api.http.generateSessionKey
-import net.mamoe.mirai.console.data.PluginConfig
-import net.mamoe.mirai.console.data.ReadOnlyPluginData
-import net.mamoe.mirai.console.data.value
-
-typealias Destination = String
-typealias Destinations = List<Destination>
-
-/**
- * Mirai Api Http 的配置文件类,它应该是单例,并且在 [HttpApiPluginBase.onEnable] 时被初始化
- */
-object Setting : ReadOnlyPluginData("setting"), PluginConfig {
-    /**
-     * 上报子消息配置
-     *
-     * @property report 是否上报
-     */
-    @Serializable
-    data class Reportable(val report: Boolean)
-
-    /**
-     * 上报服务配置
-     *
-     * @property enable 是否开启上报
-     * @property groupMessage 群消息子配置
-     * @property friendMessage 好友消息子配置
-     * @property tempMessage 临时消息子配置
-     * @property eventMessage 事件消息子配置
-     * @property destinations 上报地址(多个),必选
-     * @property extraHeaders 上报时的额外头信息
-     */
-    @Serializable
-    data class Report(
-        val enable: Boolean = false,
-        val groupMessage: Reportable = Reportable(true),
-        val friendMessage: Reportable = Reportable(true),
-        val tempMessage: Reportable = Reportable(true),
-        val eventMessage: Reportable = Reportable(true),
-        val destinations: Destinations = emptyList(),
-        val extraHeaders: Map<String, String> = emptyMap()
-    )
-
-    /**
-     * 心跳服务配置
-     *
-     * @property enable 是否启动心跳服务
-     * @property delay 心跳启动延迟
-     * @property period 心跳周期
-     * @property destinations 心跳 PING 的地址列表,必选
-     * @property extraBody 心跳额外请求体
-     * @property extraHeaders 心跳额外请求头
-     */
-    @Serializable
-    data class HeartBeat(
-        val enable: Boolean = false,
-        val delay: Long = 1000,
-        val period: Long = 15000,
-        val destinations: Destinations = emptyList(),
-        val extraBody: Map<String, String> = emptyMap(),
-        val extraHeaders: Map<String, String> = emptyMap(),
-    )
-
-    val cors: List<String> by value(listOf("*"))
-
-    /**
-     * mirai api http 所使用的地址,默认为 0.0.0.0
-     */
-    val host: String by value("0.0.0.0")
-
-    /**
-     * mirai api http 所使用的端口,默认为 8080
-     */
-    val port: Int by value(8080)
-
-    /**
-     * 认证密钥,默认为随机
-     */
-    val authKey: String by value("INITKEY" + generateSessionKey())
-
-    /**
-     * FIXME: 什么的缓存区
-     * 缓存区大小,默认为 4096
-     */
-    val cacheSize: Int by value(4096)
-
-    /**
-     * 是否启用 websocket 服务
-     */
-    val enableWebsocket: Boolean by value(false)
-
-    /**
-     * 上报服务配置
-     */
-    val report: Report by value(Report())
-
-    /**
-     * 心跳服务配置
-     */
-    val heartbeat: HeartBeat by value(HeartBeat())
-}

+ 83 - 6
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/context/MahContext.kt

@@ -9,18 +9,95 @@
 
 package net.mamoe.mirai.api.http.context
 
-import net.mamoe.mirai.api.http.context.session.SessionManager
+import net.mamoe.mirai.Bot
 import net.mamoe.mirai.api.http.adapter.MahAdapter
+import net.mamoe.mirai.api.http.adapter.common.NoSuchBotException
+import net.mamoe.mirai.api.http.context.cache.MessageSourceCache
+import net.mamoe.mirai.api.http.context.session.AuthedSession
+import net.mamoe.mirai.api.http.context.session.ISession
+import net.mamoe.mirai.api.http.context.session.manager.SessionManager
+import net.mamoe.mirai.api.http.setting.MainSetting
+import kotlin.coroutines.EmptyCoroutineContext
 
 /**
  * mah 上下文,一般情况只有一个示例
  */
-class MahContext(
-    val adapter: MahAdapter,
-    val SessionManager: SessionManager
-) {
+open class MahContext internal constructor() {
 
     companion object {
+        const val SINGLE_SESSION_KEY = "SINGLE_SESSION"
+    }
+
+    /**
+     * adapter 列表
+     */
+    val adapters: MutableList<MahAdapter> = mutableListOf()
+
+    /**
+     * 全局 session 管理
+     */
+    lateinit var sessionManager: SessionManager
+
+    /**
+     * 全局消息缓存
+     */
+    lateinit var cacheMap: MutableMap<Long, MessageSourceCache>
+
+    /**
+     * 本地模式, 调试使用. 不引用 Console, 从内部启动 adapter 进行调试
+     *
+     * 因此, 需要保证 adapter 的实现不能与 console 耦合
+     */
+    var localMode = false
+
+    /**
+     * 认证模式, 创建连接是否需要开启认证
+     *
+     * 具体是否启用依赖于 adapter 的实现, Context 中止给出用户的配置
+     */
+    var enableVerify = true
+
+    /**
+     * 单实例模式,只使用一个 bot,无需绑定 session 区分
+     */
+    var singleMode = false
+
+    /**
+     * 添加一个 adapter
+     */
+    operator fun plus(adapter: MahAdapter) = adapters.add(adapter)
+}
 
+
+fun interface MahContextBuilder {
+    operator fun MahContext.invoke()
+}
+
+object MahContextHolder {
+    lateinit var mahContext: MahContext
+
+    operator fun get(sessionKey: String): ISession? {
+        if (mahContext.singleMode) {
+            val session = MahContextHolder[MahContext.SINGLE_SESSION_KEY]
+            if (session == null) {
+                val bot = Bot.instances.firstOrNull() ?: throw NoSuchBotException
+                val singleAuthedSession = AuthedSession(bot, MahContext.SINGLE_SESSION_KEY, EmptyCoroutineContext)
+                sessionManager[MahContext.SINGLE_SESSION_KEY] = singleAuthedSession
+            }
+            return session
+        }
+
+        return sessionManager[sessionKey]
     }
-}
+
+    fun newCache(qq: Long): MessageSourceCache {
+        var cache = mahContext.cacheMap[qq]
+        if (cache == null) {
+            cache = MessageSourceCache(MainSetting.cacheSize)
+            mahContext.cacheMap[qq] = cache
+        }
+        return cache
+    }
+
+    val sessionManager get() = mahContext.sessionManager
+}

+ 15 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/context/cache/MessageSourceCache.kt

@@ -0,0 +1,15 @@
+package net.mamoe.mirai.api.http.context.cache
+
+import net.mamoe.mirai.message.data.OnlineMessageSource
+
+class MessageSourceCache(private val cacheSize: Int) : LinkedHashMap<Int, OnlineMessageSource>() {
+
+    override fun removeEldestEntry(eldest: MutableMap.MutableEntry<Int, OnlineMessageSource>?) = size > cacheSize
+
+    fun offer(source: OnlineMessageSource) {
+        put(source.ids.firstOrNull() ?: 0, source)
+    }
+
+    override operator fun get(key: Int): OnlineMessageSource = super.get(key)
+        ?: throw NoSuchElementException()
+}

+ 11 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/context/serializer/InternalSerializerHolder.kt

@@ -0,0 +1,11 @@
+package net.mamoe.mirai.api.http.context.serializer
+
+import net.mamoe.mirai.api.http.adapter.internal.serializer.InternalSerializer
+import net.mamoe.mirai.api.http.adapter.internal.serializer.JsonSerializer
+
+internal object InternalSerializerHolder {
+
+    internal val serializer: InternalSerializer by lazy {
+        JsonSerializer()
+    }
+}

+ 51 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/context/session/manager/api.kt

@@ -0,0 +1,51 @@
+package net.mamoe.mirai.api.http.context.session.manager
+
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.api.http.context.session.*
+
+/**
+ * Session管理
+ * 默认提供了{@link DefaultSessionManager}
+ */
+interface SessionManager {
+
+    /**
+     * 全局认证 key
+     */
+    val verifyKey: String
+
+    /**
+     * 创建临时 session
+     */
+    fun createTempSession(): TempSession
+
+    /**
+     * 将临时 session 转为已认证(绑定) session
+     */
+    fun authSession(bot: Bot, tempSessionKey: String): IAuthedSession
+
+    /**
+     * 将临时 session 转为已认证(绑定) session
+     */
+    fun authSession(bot: Bot, tempSession: TempSession): IAuthedSession
+
+    /**
+     * 临时 Session 转为自定义 AuthedSession
+     */
+    fun authSession(tempSessionKey: String, authedSession: IAuthedSession): IAuthedSession
+
+    /**
+     * 临时 Session 转为自定义 AuthedSession
+     */
+    fun authSession(tempSession: TempSession, authedSession: IAuthedSession): IAuthedSession
+
+    operator fun get(key: String): ISession?
+
+    operator fun set(key: String, session: ISession)
+
+    fun closeSession(key: String)
+
+    fun closeSession(session: ISession)
+
+    fun close()
+}

+ 18 - 13
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/context/session/manager/default.kt

@@ -11,14 +11,11 @@ package net.mamoe.mirai.api.http.context.session.manager
 
 import kotlinx.coroutines.*
 import net.mamoe.mirai.Bot
-import net.mamoe.mirai.api.http.context.session.AuthedSession
-import net.mamoe.mirai.api.http.context.session.Session
-import net.mamoe.mirai.api.http.context.session.SessionManager
-import net.mamoe.mirai.api.http.context.session.TempSession
+import net.mamoe.mirai.api.http.context.session.*
 import kotlin.coroutines.EmptyCoroutineContext
 
-class DefaultSessionManager(val authKey: String) : SessionManager {
-    private val sessionMap: MutableMap<String, Session> = mutableMapOf()
+class DefaultSessionManager(override val verifyKey: String) : SessionManager {
+    private val sessionMap: MutableMap<String, ISession> = mutableMapOf()
 
     override fun createTempSession(): TempSession =
         TempSession(generateSessionKey(), EmptyCoroutineContext).also { newTempSession ->
@@ -27,29 +24,37 @@ class DefaultSessionManager(val authKey: String) : SessionManager {
             newTempSession.launch {
                 delay(180000)
                 sessionMap[newTempSession.key]?.run {
-                    if (this is TempSession)
+                    if (this is TempSession) {
                         closeSession(newTempSession.key)
+                    }
                 }
             }
         }
 
     override fun authSession(bot: Bot, tempSessionKey: String) =
-        AuthedSession(bot, tempSessionKey, EmptyCoroutineContext).also { session ->
-            closeSession(tempSessionKey)
-            set(tempSessionKey, session)
-        }
+        authSession(tempSessionKey, AuthedSession(bot, tempSessionKey, EmptyCoroutineContext))
 
     override fun authSession(bot: Bot, tempSession: TempSession) = authSession(bot, tempSession.key)
 
+    override fun authSession(tempSession: TempSession, authedSession: IAuthedSession): IAuthedSession =
+        authSession(tempSession.key, authedSession)
+
+    override fun authSession(tempSessionKey: String, authedSession: IAuthedSession): IAuthedSession {
+        closeSession(tempSessionKey)
+        set(tempSessionKey, authedSession)
+        return authedSession
+    }
+
     override operator fun get(key: String) = sessionMap[key]
 
-    override fun set(key: String, session: Session) = sessionMap.set(key, session)
+    override operator fun set(key: String, session: ISession) = sessionMap.set(key, session)
 
     override fun closeSession(key: String) {
         sessionMap[key]?.close()
+        sessionMap.remove(key)
     }
 
-    override fun closeSession(session: Session) {
+    override fun closeSession(session: ISession) {
         closeSession(session.key)
     }
 

+ 9 - 11
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/context/session/manager/util.kt

@@ -1,21 +1,19 @@
 package net.mamoe.mirai.api.http.context.session.manager
 
-import net.mamoe.mirai.api.http.context.session.SessionManager
-
 tailrec fun SessionManager.generateSessionKey(): String {
-    fun generateRandomSessionKey(): String {
-        val all = "QWERTYUIOPASDFGHJKLZXCVBNM1234567890qwertyuiopasdfghjklzxcvbnm"
-        return buildString(capacity = 8) {
-            repeat(8) {
-                append(all.random())
-            }
-        }
-    }
-
     val key = generateRandomSessionKey()
     this[key]?.apply {
         return key
     }
 
     return generateSessionKey()
+}
+
+fun generateRandomSessionKey(): String {
+    val all = "QWERTYUIOPASDFGHJKLZXCVBNM1234567890qwertyuiopasdfghjklzxcvbnm"
+    return buildString(capacity = 8) {
+        repeat(8) {
+            append(all.random())
+        }
+    }
 }

+ 23 - 32
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/context/session/session.kt

@@ -1,46 +1,37 @@
 package net.mamoe.mirai.api.http.context.session
 
-import kotlinx.coroutines.*
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
 import net.mamoe.mirai.Bot
+import net.mamoe.mirai.api.http.context.MahContextHolder
+import net.mamoe.mirai.api.http.context.cache.MessageSourceCache
 import kotlin.coroutines.CoroutineContext
 
-/**
- * Session管理
- * 默认提供了{@link DefaultSessionManager}
- */
-interface SessionManager {
+open class TempSession internal constructor(initKey: String, coroutineContext: CoroutineContext) :
+    Session(coroutineContext, initKey)
 
-    fun createTempSession(): TempSession
-
-    fun authSession(bot: Bot, tempSessionKey: String): AuthedSession
-
-    fun authSession(bot: Bot, tempSession: TempSession): AuthedSession
-
-    operator fun get(key: String): Session?
-
-    operator fun set(key: String, session: Session)
-
-    fun closeSession(key: String)
-
-    fun closeSession(session: Session)
-
-    fun close()
+class AuthedSession internal constructor(override val bot: Bot, originKey: String, coroutineContext: CoroutineContext) :
+    Session(coroutineContext, originKey), IAuthedSession {
+    override val sourceCache: MessageSourceCache = MahContextHolder.newCache(bot.id)
 }
 
-open class TempSession internal constructor(initKey: String, coroutineContext: CoroutineContext)
-    : Session(coroutineContext, initKey)
-
-open class AuthedSession internal constructor(val bot: Bot, originKey: String, coroutineContext: CoroutineContext)
-    : Session(coroutineContext, originKey)
-
-abstract class Session internal constructor(
-    coroutineContext: CoroutineContext,
-    val key: String,
-) : CoroutineScope {
+abstract class Session internal constructor(coroutineContext: CoroutineContext, override val key: String) : ISession {
     private val supervisorJob = SupervisorJob(coroutineContext[Job])
     final override val coroutineContext: CoroutineContext = supervisorJob + coroutineContext
 
-    internal open fun close() {
+    override fun close() {
         supervisorJob.complete()
     }
 }
+
+interface ISession : CoroutineScope {
+    val key: String
+
+    fun close()
+}
+
+interface IAuthedSession : ISession {
+    val bot: Bot
+    val sourceCache: MessageSourceCache
+}

+ 0 - 35
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/Config.kt

@@ -1,35 +0,0 @@
-/*
- * Copyright 2020 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/master/LICENSE
- */
-
-package net.mamoe.mirai.api.http.data
-
-import net.mamoe.mirai.api.http.context.session.manager.AuthedSession
-
-class Config(
-    private val session: AuthedSession,
-    cacheSize: Int,
-    enableWebsocket: Boolean
-) {
-
-    var cacheSize = cacheSize
-        set(value) {
-            session.cacheQueue.cacheSize = value
-            field = value
-        }
-
-    var enableWebsocket = enableWebsocket
-        set(value) {
-            if (value) {
-                session.enableWebSocket()
-            } else {
-                session.disableWebSocket()
-            }
-            field = value
-        }
-}

+ 0 - 257
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/common/MessageDTO.kt

@@ -1,257 +0,0 @@
-/*
- * Copyright 2020 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/master/LICENSE
- */
-
-package net.mamoe.mirai.api.http.data.common
-
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-import net.mamoe.mirai.api.http.HttpApiPluginBase
-import net.mamoe.mirai.api.http.util.FaceMap
-import net.mamoe.mirai.api.http.util.PokeMap
-import net.mamoe.mirai.api.http.util.toHexArray
-import net.mamoe.mirai.contact.Contact
-import net.mamoe.mirai.contact.Group
-import net.mamoe.mirai.event.events.FriendMessageEvent
-import net.mamoe.mirai.event.events.GroupMessageEvent
-import net.mamoe.mirai.event.events.MessageEvent
-import net.mamoe.mirai.event.events.TempMessageEvent
-import net.mamoe.mirai.message.*
-import net.mamoe.mirai.message.data.*
-import net.mamoe.mirai.message.data.Image.Key.queryUrl
-import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
-import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
-import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsVoice
-import net.mamoe.mirai.utils.MiraiExperimentalApi
-import net.mamoe.mirai.utils.MiraiInternalApi
-import java.net.URL
-
-/*
-*   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("TempMessage")
-data class TempMessagePacketDto(val sender: MemberDTO) : MessagePacketDTO()
-
-@Serializable
-@SerialName("StrangerMessage")
-data class StrangerMessagePacketDto(val sender: QQDTO) : MessagePacketDTO()
-
-
-// Message
-@Serializable
-@SerialName("Source")
-data class MessageSourceDTO(val id: Int, val time: Int) : MessageDTO()
-
-@Serializable
-@SerialName("At")
-data class AtDTO(val target: Long, val display: String = "") : MessageDTO()
-
-@Serializable
-@SerialName("AtAll")
-data class AtAllDTO(val target: Long = 0) : MessageDTO() // target为保留字段
-
-@Serializable
-@SerialName("Face")
-data class FaceDTO(val faceId: Int = -1, val name: String = "") : MessageDTO()
-
-@Serializable
-@SerialName("Plain")
-data class PlainDTO(val text: String) : MessageDTO()
-
-@Serializable
-@SerialName("Image")
-data class ImageDTO(
-    val imageId: String? = null,
-    val url: String? = null,
-    val path: String? = null
-) : MessageDTO()
-
-@Serializable
-@SerialName("FlashImage")
-data class FlashImageDTO(
-    val imageId: String? = null,
-    val url: String? = null,
-    val path: String? = null
-) : MessageDTO()
-
-@Serializable
-@SerialName("Voice")
-data class VoiceDTO(
-    val voiceId: String? = null,
-    val url: String? = null,
-    val path: String? = null
-) : MessageDTO()
-
-@Serializable
-@SerialName("Xml")
-data class XmlDTO(val xml: String) : MessageDTO()
-
-@Serializable
-@SerialName("Json")
-data class JsonDTO(val json: String) : MessageDTO()
-
-@Serializable
-@SerialName("App")
-data class AppDTO(val content: String) : MessageDTO()
-
-@Serializable
-@SerialName("Quote")
-data class QuoteDTO(
-    val id: Int,
-    val senderId: Long,
-    val targetId: Long,
-    val groupId: Long,
-    val origin: MessageChainDTO
-) : MessageDTO()
-
-@Serializable
-@SerialName("Poke")
-data class PokeMessageDTO(
-    val name: String
-) : MessageDTO()
-
-@Serializable
-@SerialName("Unknown")
-object UnknownMessageDTO : MessageDTO()
-
-/*
-*   Abstract Class
-* */
-@Serializable
-sealed class MessagePacketDTO : EventDTO() {
-    lateinit var messageChain: MessageChainDTO
-}
-
-typealias MessageChainDTO = List<MessageDTO>
-
-@Serializable
-sealed class MessageDTO : DTO
-
-
-/*
-    Extend function
- */
-suspend fun MessageEvent.toDTO() = when (this) {
-    is FriendMessageEvent -> FriendMessagePacketDTO(QQDTO(sender))
-    is GroupMessageEvent -> GroupMessagePacketDTO(MemberDTO(sender))
-    is TempMessageEvent -> TempMessagePacketDto(MemberDTO(sender))
-    else -> IgnoreEventDTO
-}.apply {
-    if (this is MessagePacketDTO) {
-        // 将MessagePacket中的所有Message转为DTO对象,并添加到messageChain
-        messageChain = message.toMessageChainDTO { it != UnknownMessageDTO }
-        // else: `this` is bot event
-    }
-}
-
-suspend inline fun MessageChain.toMessageChainDTO(filter: (MessageDTO) -> Boolean): MessageChainDTO =
-    // `foreachContent`会忽略`MessageSource`,手动添加
-    mutableListOf<MessageDTO>().apply {
-        // `MessageSource` 在 `QuoteReplay` 中可能不存在
-        this@toMessageChainDTO[MessageSource]?.let { this.add(it.toDTO()) }
-        // `QuoteReply`会被`foreachContent`过滤,手动添加
-        this@toMessageChainDTO[QuoteReply]?.let { this.add(it.toDTO()) }
-        [email protected] { content ->
-            (content as? MessageContent)?.toDTO()?.takeIf { filter(it) }?.let(::add)
-        }
-    }
-
-
-suspend fun MessageChainDTO.toMessageChain(contact: Contact) =
-    buildMessageChain { [email protected] { it.toMessage(contact)?.let(::add) } }
-
-
-suspend fun Message.toDTO() = when (this) {
-    is MessageSource -> MessageSourceDTO(ids.firstOrNull() ?: 0, time)
-    is At -> AtDTO(target, "")
-    is AtAll -> AtAllDTO(0L)
-    is Face -> FaceDTO(id, FaceMap[id])
-    is PlainText -> PlainDTO(content)
-    is Image -> ImageDTO(imageId, queryUrl())
-    is FlashImage -> FlashImageDTO(image.imageId, image.queryUrl())
-    is Voice -> VoiceDTO(fileName, url)
-    is ServiceMessage -> XmlDTO(content)
-    is LightApp -> AppDTO(content)
-    is QuoteReply -> QuoteDTO(source.ids.firstOrNull() ?: 0, source.fromId, source.targetId,
-        groupId = when {
-            source is OfflineMessageSource && (source as OfflineMessageSource).kind == MessageSourceKind.GROUP ||
-                    source is OnlineMessageSource && (source as OnlineMessageSource).subject is Group -> source.targetId
-            else -> 0L
-        },
-        // 避免套娃
-        origin = source.originalMessage.toMessageChainDTO { it != UnknownMessageDTO && it !is QuoteDTO })
-    is PokeMessage -> PokeMessageDTO(PokeMap[pokeType])
-    else -> UnknownMessageDTO
-}
-
-@OptIn(MiraiInternalApi::class, MiraiExperimentalApi::class)
-suspend fun MessageDTO.toMessage(contact: Contact) = when (this) {
-    is AtDTO -> (contact as Group).getOrFail(target).at()
-    is AtAllDTO -> AtAll
-    is FaceDTO -> when {
-        faceId >= 0 -> Face(faceId)
-        name.isNotEmpty() -> Face(FaceMap[name])
-        else -> Face(255)
-    }
-    is PlainDTO -> PlainText(text)
-    is ImageDTO -> when {
-        !imageId.isNullOrBlank() -> Image(imageId)
-        !url.isNullOrBlank() -> withContext(Dispatchers.IO) { URL(url).openStream().uploadAsImage(contact) }
-        !path.isNullOrBlank() -> with(HttpApiPluginBase.image(path)) {
-            if (exists()) {
-                uploadAsImage(contact)
-            } else throw NoSuchFileException(this)
-        }
-        else -> null
-    }
-    is FlashImageDTO -> when {
-        !imageId.isNullOrBlank() -> Image(imageId)
-        !url.isNullOrBlank() -> withContext(Dispatchers.IO) { URL(url).openStream().uploadAsImage(contact) }
-        !path.isNullOrBlank() -> with(HttpApiPluginBase.image(path)) {
-            if (exists()) {
-                uploadAsImage(contact)
-            } else throw NoSuchFileException(this)
-        }
-        else -> null
-    }?.flash()
-    is VoiceDTO -> when {
-        contact !is Group -> null
-        !voiceId.isNullOrBlank() -> Voice(voiceId, voiceId.substringBefore(".").toHexArray(), 0, 0, "")
-        !url.isNullOrBlank() -> withContext(Dispatchers.IO) { URL(url).openStream().toExternalResource().uploadAsVoice(contact) }
-        !path.isNullOrBlank() -> with(HttpApiPluginBase.voice(path)) {
-            if (exists()) {
-                inputStream().toExternalResource().uploadAsVoice(contact)
-            } else throw NoSuchFileException(this)
-        }
-        else -> null
-    }
-    is XmlDTO -> SimpleServiceMessage(60, xml)
-    is JsonDTO -> SimpleServiceMessage(1, json)
-    is AppDTO -> LightApp(content)
-    is PokeMessageDTO -> PokeMap[name]
-    // ignore
-    is QuoteDTO,
-    is MessageSourceDTO,
-    is UnknownMessageDTO
-    -> null
-}
-
-

+ 0 - 53
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/common/RestfulResult.kt

@@ -1,53 +0,0 @@
-/*
- * Copyright 2020 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/master/LICENSE
- */
-
-package net.mamoe.mirai.api.http.data.common
-
-import kotlinx.serialization.Serializable
-
-@Serializable
-data class RestfulResult(
-    val code: Int = 0,
-    val errorMessage: String = ""
-) : DTO
-
-@Serializable
-data class BooleanRestfulResult(
-    val code: Int = 0,
-    val errorMessage: String = "",
-    val data: Boolean
-) : DTO
-
-@Serializable
-data class IntRestfulResult(
-    val code: Int = 0,
-    val errorMessage: String = "",
-    val data: Int
-) : DTO
-
-@Serializable
-data class EventListRestfulResult(
-    val code: Int = 0,
-    val errorMessage: String = "",
-    val data: List<EventDTO>
-) : DTO
-
-@Serializable
-data class EventRestfulResult(
-    val code: Int = 0,
-    val errorMessage: String = "",
-    val data: EventDTO?
-) : DTO
-
-@Serializable
-data class StringMapRestfulResult(
-    val code: Int = 0,
-    val errorMessage: String = "",
-    val data: Map<String, String>
-) : DTO

+ 0 - 25
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/queue/CacheQueue.kt

@@ -1,25 +0,0 @@
-/*
- * Copyright 2020 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/master/LICENSE
- */
-
-package net.mamoe.mirai.api.http.queue
-
-import net.mamoe.mirai.message.data.OnlineMessageSource
-
-class CacheQueue : LinkedHashMap<Int, OnlineMessageSource>() {
-
-    var cacheSize = 4096
-
-    override fun get(key: Int): OnlineMessageSource = super.get(key) ?: throw NoSuchElementException()
-
-    override fun removeEldestEntry(eldest: MutableMap.MutableEntry<Int, OnlineMessageSource>?): Boolean = size > cacheSize
-
-    fun add(source: OnlineMessageSource) {
-        put(source.ids.firstOrNull() ?: 0, source)
-    }
-}

+ 0 - 81
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/queue/MessageQueue.kt

@@ -1,81 +0,0 @@
-/*
- * Copyright 2020 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/master/LICENSE
- */
-
-package net.mamoe.mirai.api.http.queue
-
-import net.mamoe.mirai.api.http.data.common.EventDTO
-import net.mamoe.mirai.api.http.data.common.IgnoreEventDTO
-import net.mamoe.mirai.api.http.data.common.toDTO
-import net.mamoe.mirai.event.events.BotEvent
-import java.util.concurrent.ConcurrentLinkedDeque
-
-class MessageQueue : ConcurrentLinkedDeque<BotEvent>() {
-
-    suspend fun fetch(size: Int): List<EventDTO> {
-        var count = size
-
-        val ret = ArrayList<EventDTO>(count)
-        while (!this.isEmpty() && count > 0) {
-            val event = pop()
-
-            event.toDTO().also {
-                if (it != IgnoreEventDTO) {
-                    ret.add(it)
-                    count--
-                }
-            }
-        }
-        return ret
-    }
-
-    suspend fun fetchLatest(size: Int = 10): List<EventDTO> {
-        var count = size
-
-        val ret = ArrayList<EventDTO>(count)
-        while (!this.isEmpty() && count > 0) {
-            val event = removeLast()
-
-            event.toDTO().also {
-                if (it != IgnoreEventDTO) {
-                    ret.add(it)
-                    count--
-                }
-            }
-        }
-        return ret
-    }
-
-    suspend fun peek(size: Int): List<EventDTO> {
-        var count = size
-        val ret = ArrayList<EventDTO>(count)
-
-        val iterator: Iterator<BotEvent> = iterator();
-
-        while(iterator.hasNext() && count > 0) {
-            ret.add(iterator.next().toDTO())
-            count--
-        }
-
-        return ret
-    }
-
-    suspend fun peekLatest(size: Int): List<EventDTO> {
-        var count = size
-        val ret = ArrayList<EventDTO>(count)
-
-        val iterator: Iterator<BotEvent> = reversed().iterator();
-
-        while(iterator.hasNext() && count > 0) {
-            ret.add(iterator.next().toDTO())
-            count--
-        }
-
-        return ret
-    }
-}

+ 0 - 80
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/AuthRouteModule.kt

@@ -1,80 +0,0 @@
-/*
- * Copyright 2020 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/master/LICENSE
- */
-
-package net.mamoe.mirai.api.http.route
-
-import io.ktor.application.Application
-import io.ktor.application.call
-import io.ktor.routing.routing
-import kotlinx.serialization.Serializable
-import net.mamoe.mirai.Bot
-import net.mamoe.mirai.api.http.context.session.manager.AuthedSession
-import net.mamoe.mirai.api.http.context.session.SessionManager
-import net.mamoe.mirai.api.http.data.NoSuchBotException
-import net.mamoe.mirai.api.http.data.StateCode
-import net.mamoe.mirai.api.http.data.common.AuthDTO
-import net.mamoe.mirai.api.http.data.common.DTO
-import net.mamoe.mirai.api.http.data.common.VerifyDTO
-
-/**
- * 授权路由
- */
-fun Application.authModule() {
-    routing {
-
-        /**
-         * 获取授权
-         */
-        miraiAuth<AuthDTO>("/auth") {
-            if (it.authKey != SessionManager.authKey) {
-                call.respondStateCode(StateCode.AuthKeyFail)
-            } else {
-                call.respondDTO(AuthRetDTO(0, SessionManager.createTempSession().key))
-            }
-        }
-
-        /**
-         * 验证并分配session
-         */
-        miraiVerify<BindDTO>("/verify", verifiedSessionKey = false) {
-            val bot = getBotOrThrow(it.qq)
-            if (SessionManager[it.sessionKey] !is AuthedSession) {
-                SessionManager.createAuthedSession(bot, it.sessionKey)
-            }
-            call.respondStateCode(StateCode.Success)
-        }
-
-        /**
-         * 释放session
-         */
-        miraiVerify<BindDTO>("/release") {
-            val bot = getBotOrThrow(it.qq)
-            val session = SessionManager[it.sessionKey] as AuthedSession
-            if (bot.id == session.bot.id) {
-                SessionManager.closeSession(it.sessionKey)
-                call.respondStateCode(StateCode.Success)
-            } else {
-                throw NoSuchElementException()
-            }
-        }
-
-    }
-}
-
-@Serializable
-private data class AuthRetDTO(val code: Int, val session: String) : DTO
-
-@Serializable
-private data class BindDTO(override val sessionKey: String, val qq: Long) : VerifyDTO()
-
-internal fun getBotOrThrow(qq: Long) = try {
-    Bot.getInstance(qq)
-} catch (e: NoSuchElementException) {
-    throw NoSuchBotException
-}

+ 0 - 197
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/CommandRouteModule.kt

@@ -1,197 +0,0 @@
-/*
- * Copyright 2020 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/master/LICENSE
- */
-
-package net.mamoe.mirai.api.http.route
-
-import io.ktor.application.*
-import io.ktor.http.*
-import io.ktor.http.cio.websocket.*
-import io.ktor.response.*
-import io.ktor.routing.*
-import io.ktor.websocket.*
-import kotlinx.serialization.Serializable
-import net.mamoe.mirai.Bot
-import net.mamoe.mirai.api.http.HttpApiPluginBase
-import net.mamoe.mirai.api.http.context.session.SessionManager
-import net.mamoe.mirai.api.http.data.IllegalParamException
-import net.mamoe.mirai.api.http.data.StateCode
-import net.mamoe.mirai.api.http.data.common.DTO
-import net.mamoe.mirai.api.http.util.toJson
-import net.mamoe.mirai.console.command.CommandExecuteResult
-import net.mamoe.mirai.console.command.CommandManager
-import net.mamoe.mirai.console.command.CommandSender
-import net.mamoe.mirai.console.permission.AbstractPermitteeId
-import net.mamoe.mirai.console.permission.PermitteeId
-import net.mamoe.mirai.console.util.ConsoleExperimentalApi
-import net.mamoe.mirai.contact.Contact
-import net.mamoe.mirai.contact.User
-import net.mamoe.mirai.message.MessageReceipt
-import net.mamoe.mirai.message.data.Message
-import net.mamoe.mirai.message.data.PlainText
-import kotlin.coroutines.CoroutineContext
-import kotlin.coroutines.EmptyCoroutineContext
-
-/**
- * 命令行路由
- */
-@OptIn(ConsoleExperimentalApi::class)
-fun Application.commandModule() {
-
-    routing {
-        /**
-         * 注册命令
-         */
-        miraiAuth<PostCommandDTO>("/command/register") {
-            if (it.authKey != SessionManager.authKey) {
-                call.respondStateCode(StateCode.AuthKeyFail)
-            } else {
-                val names = ArrayList<String>(1 + it.alias.size).apply {
-                    add(it.name)
-                    addAll(it.alias)
-                }
-
-//                RegisterCommand(it.description, it.usage, *names.toTypedArray()).register(true)
-                call.respondStateCode(StateCode(-1, "未支持操作"))
-            }
-        }
-
-        /**
-         * 执行命令
-         */
-        miraiAuth<PostCommandDTO>("/command/send") {
-            if (it.authKey != SessionManager.authKey) {
-                call.respondStateCode(StateCode.AuthKeyFail)
-            } else {
-                val sender = HttpCommandSender(call)
-
-                CommandManager.run {
-                    when (val result = executeCommand(sender, PlainText("${it.name} ${it.args.joinToString(" ")}"))) {
-                        is CommandExecuteResult.Success -> if (!sender.consume) call.respondText("")
-                        else -> call.respondStateCode(StateCode.NoElement)
-                    }
-                }
-            }
-        }
-
-        /**
-         * 获取Manager
-         */
-        route("/managers", HttpMethod.Get) {
-            intercept {
-                val qq = call.parameters["qq"] ?: throw IllegalParamException("参数格式错误")
-                val managers = listOf<Long>()
-                call.respondJson(managers.toJson())
-            }
-        }
-
-        /**
-         * 广播命令
-         */
-        webSocket("/command") {
-            // 校验Auth key
-            val authKey = call.parameters["authKey"]
-            if (authKey == null) {
-                outgoing.send(Frame.Text(StateCode(400, "参数格式错误").toJson(StateCode.serializer())))
-                close(CloseReason(CloseReason.Codes.NORMAL, "参数格式错误"))
-                return@webSocket
-            }
-            if (authKey != SessionManager.authKey) {
-                outgoing.send(Frame.Text(StateCode.AuthKeyFail.toJson(StateCode.serializer())))
-                close(CloseReason(CloseReason.Codes.NORMAL, "Auth Key错误"))
-                return@webSocket
-            }
-
-            // 订阅onCommand事件
-            val subscriber = HttpApiPluginBase.subscribeCommand { name, friend, group, args ->
-                outgoing.send(Frame.Text(CommandDTO(name, friend, group, args).toJson()))
-            }
-
-            try {
-                // 阻塞websocket
-                for (frame in incoming) {
-                    /* do nothing */
-                    HttpApiPluginBase.logger.info("command websocket send $frame")
-                }
-            } finally {
-                HttpApiPluginBase.unSubscribeCommand(subscriber)
-            }
-        }
-    }
-}
-
-// TODO: 将command输出返回给请求
-class HttpCommandSender(
-    private val call: ApplicationCall,
-    override val coroutineContext: CoroutineContext = EmptyCoroutineContext
-) : CommandSender {
-    override val bot: Bot? = null
-    override val name: String = "Mirai Http Api"
-    override val permitteeId: PermitteeId
-        get() = object : PermitteeId {
-            override val directParents: Array<out PermitteeId>
-                get() = arrayOf(AbstractPermitteeId.Console)
-
-            override fun asString(): String = "http-api"
-        }
-
-
-    override val subject: Contact? = null
-    override val user: User? = null
-
-    var consume = false
-
-    override suspend fun sendMessage(message: String): MessageReceipt<Contact>? {
-//        appendMessage(message)
-        if (!consume) {
-            call.respondText(message)
-            consume = true
-        }
-
-        return null
-    }
-
-    override suspend fun sendMessage(message: Message): MessageReceipt<Contact>? {
-//        appendMessage(messageChain.toString())
-        if (!consume) {
-            call.respondText(message.toString())
-            consume = true
-        }
-
-        return null
-    }
-
-    /*override suspend fun catchExecutionException(e: Throwable) {
-        // Nothing
-    }*/
-
-
-//    override suspend fun flushMessage() {
-//        if (builder.isNotEmpty()) {
-//            call.respondText(builder.toString().removeSuffix("\n"))
-//        }
-//    }
-}
-
-@Serializable
-data class CommandDTO(
-    val name: String,
-    val friend: Long,
-    val group: Long,
-    val args: List<String>,
-) : DTO
-
-@Serializable
-private data class PostCommandDTO(
-    val authKey: String,
-    val name: String,
-    val alias: List<String> = emptyList(),
-    val description: String = "",
-    val usage: String = "",
-    val args: List<String> = emptyList(),
-) : DTO

+ 0 - 79
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/ConfRouteModule.kt

@@ -1,79 +0,0 @@
-/*
- * Copyright 2020 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/master/LICENSE
- */
-
-package net.mamoe.mirai.api.http.route
-
-import io.ktor.application.*
-import io.ktor.routing.*
-import kotlinx.serialization.Serializable
-import net.mamoe.mirai.api.http.context.session.manager.AuthedSession
-import net.mamoe.mirai.api.http.HttpApiPluginBase
-import net.mamoe.mirai.api.http.data.StateCode
-import net.mamoe.mirai.api.http.data.common.StringMapRestfulResult
-import net.mamoe.mirai.api.http.data.common.VerifyDTO
-import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription
-import kotlin.reflect.full.memberProperties
-
-private val mahVersion by lazy {
-    val desc = HttpApiPluginBase.description
-    JvmPluginDescription::class.memberProperties.first { it.name == "version" }
-        .get(desc).toString()
-}
-
-/**
- * 配置路由
- */
-fun Application.configRouteModule() {
-
-    routing {
-
-        /**
-         * 获取API-HTTP插件信息
-         */
-        get("/about") {
-            call.respondDTO(
-                StringMapRestfulResult(
-                    data = mapOf(
-                        "version" to mahVersion
-                    )
-                )
-            )
-        }
-
-        /**
-         * 获取API-HTTP配置
-         */
-        miraiGet("config") {
-            call.respondDTO(ConfigDTO(it))
-        }
-
-        /**
-         * 修改API-HTTP配置
-         */
-        miraiVerify<ConfigDTO>("config") {
-            val sessionConfig = it.session.config
-            it.cacheSize?.apply { sessionConfig.cacheSize = this }
-            it.enableWebsocket?.apply { sessionConfig.enableWebsocket = this }
-            call.respondStateCode(StateCode.Success)
-        }
-    }
-}
-
-@Serializable
-data class ConfigDTO(
-    override val sessionKey: String,
-    val cacheSize: Int? = null,
-    val enableWebsocket: Boolean? = null
-) : VerifyDTO() {
-    constructor(session: AuthedSession) : this(
-        sessionKey = session.key,
-        cacheSize = session.config.cacheSize,
-        enableWebsocket = session.config.enableWebsocket
-    )
-}

+ 0 - 96
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/EventRouteModule.kt

@@ -1,96 +0,0 @@
-/*
- * Copyright 2020 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/master/LICENSE
- */
-
-package net.mamoe.mirai.api.http.route
-
-import io.ktor.application.*
-import io.ktor.routing.routing
-import kotlinx.serialization.Serializable
-import net.mamoe.mirai.LowLevelApi
-import net.mamoe.mirai.Mirai
-import net.mamoe.mirai.api.http.data.StateCode
-import net.mamoe.mirai.api.http.data.common.VerifyDTO
-
-/**
- * 事件响应路由
- */
-
-@OptIn(LowLevelApi::class)
-fun Application.eventRouteModule() {
-
-    routing {
-
-        miraiVerify<EventRespDTO>("/resp/newFriendRequestEvent") {
-            Mirai.solveNewFriendRequestEvent(
-                it.session.bot,
-                eventId = it.eventId,
-                fromId = it.fromId,
-                fromNick = "",
-                accept = it.operate == 0,
-                blackList = it.operate == 2
-            )
-//            when(it.operate) {
-//                0 -> event.accept() // accept
-//                1 -> event.reject(blackList = false) // reject
-//                2 -> event.reject(blackList = true) // black list
-//                else -> {
-//                    call.respondDTO(StateCode.NoOperateSupport)
-//                    return@miraiVerify
-//                }
-//            }
-            call.respondStateCode(StateCode.Success)
-        }
-
-        miraiVerify<EventRespDTO>("/resp/memberJoinRequestEvent") {
-            Mirai.solveMemberJoinRequestEvent(
-                it.session.bot,
-                eventId = it.eventId,
-                fromId = it.fromId,
-                fromNick = "",
-                groupId = it.groupId,
-                accept = if (it.operate == 0) true else if (it.operate % 2 == 0) null else false,
-                blackList = it.operate == 3 || it.operate == 4
-            )
-//            when(it.operate) {
-//                0 -> event.accept() // accept
-//                1 -> event.reject(blackList = false) // reject
-//                2 -> event.ignore(blackList = false) //ignore
-//                3 -> event.reject(blackList = true) // reject and black list
-//                4 -> event.ignore(blackList = true) // ignore and black list
-//                else -> {
-//                    call.respondDTO(StateCode.NoOperateSupport)
-//                    return@miraiVerify
-//                }
-//            }
-            call.respondStateCode(StateCode.Success)
-        }
-
-        miraiVerify<EventRespDTO>("/resp/botInvitedJoinGroupRequestEvent") {
-            Mirai.solveBotInvitedJoinGroupRequestEvent(
-                it.session.bot,
-                eventId = it.eventId,
-                invitorId = it.fromId,
-                groupId = it.groupId,
-                accept = it.operate == 0
-            )
-            call.respondStateCode(StateCode.Success)
-        }
-
-    }
-}
-
-@Serializable
-private data class EventRespDTO(
-    override val sessionKey: String,
-    val eventId: Long,
-    val fromId: Long,
-    val groupId: Long,
-    val operate: Int,
-    val message: String
-) : VerifyDTO()

+ 0 - 186
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/GroupManageRouteModule.kt

@@ -1,186 +0,0 @@
-/*
- * Copyright 2020 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/master/LICENSE
- */
-
-package net.mamoe.mirai.api.http.route
-
-import io.ktor.application.*
-import io.ktor.routing.*
-import kotlinx.serialization.Serializable
-import net.mamoe.mirai.api.http.data.StateCode
-import net.mamoe.mirai.api.http.data.common.DTO
-import net.mamoe.mirai.api.http.data.common.VerifyDTO
-import net.mamoe.mirai.contact.Group
-import net.mamoe.mirai.contact.Member
-
-/**
- * 群管理路由
- */
-fun Application.groupManageModule() {
-    routing {
-
-        /**
-         * 禁言所有人(需要相关权限)
-         */
-        miraiVerify<MuteDTO>("/muteAll") {
-            it.session.bot.getGroupOrFail(it.target).settings.isMuteAll = true
-            call.respondStateCode(StateCode.Success)
-        }
-
-        /**
-         * 取消禁言所有人(需要相关权限)
-         */
-        miraiVerify<MuteDTO>("/unmuteAll") {
-            it.session.bot.getGroupOrFail(it.target).settings.isMuteAll = false
-            call.respondStateCode(StateCode.Success)
-        }
-
-        /**
-         * 禁言指定群成员(需要相关权限)
-         */
-        miraiVerify<MuteDTO>("/mute") {
-            it.session.bot.getGroupOrFail(it.target).getOrFail(it.memberId).mute(it.time)
-            call.respondStateCode(StateCode.Success)
-        }
-
-        /**
-         * 取消禁言指定群成员(需要相关权限)
-         */
-        miraiVerify<MuteDTO>("/unmute") {
-            it.session.bot.getGroupOrFail(it.target).getOrFail(it.memberId).unmute()
-            call.respondStateCode(StateCode.Success)
-        }
-
-        /**
-         * 移出群聊(需要相关权限)
-         */
-        miraiVerify<KickDTO>("/kick") {
-            it.session.bot.getGroupOrFail(it.target).getOrFail(it.memberId).kick(it.msg)
-            call.respondStateCode(StateCode.Success)
-        }
-
-        /**
-         * Bot退出群聊(Bot不能为群主)
-         */
-        miraiVerify<QuitDTO>("/quit") {
-            val success = it.session.bot.getGroupOrFail(it.target).quit()
-            call.respondStateCode(
-                if (success) StateCode.Success
-                else StateCode.PermissionDenied
-            )
-        }
-
-        /**
-         * 获取群设置(需要相关权限)
-         */
-        miraiGet("/groupConfig") {
-            val group = it.bot.getGroupOrFail(paramOrNull("target"))
-            call.respondDTO(GroupDetailDTO(group))
-        }
-
-        /**
-         * 修改群设置(需要相关权限)
-         */
-        miraiVerify<GroupConfigDTO>("/groupConfig") { dto ->
-            val group = dto.session.bot.getGroupOrFail(dto.target)
-            with(dto.config) {
-                name?.let { group.name = it }
-                announcement?.let { group.settings.entranceAnnouncement = it }
-                // confessTalk?.let { group.settings.isConfessTalkEnabled = it }
-                allowMemberInvite?.let { group.settings.isAllowMemberInvite = it }
-                // TODO: 待core接口实现设置可改
-//                autoApprove?.let { group.autoApprove = it }
-//                anonymousChat?.let { group.anonymousChat = it }
-            }
-            call.respondStateCode(StateCode.Success)
-        }
-
-        /**
-         * 群员信息管理(需要相关权限)
-         */
-        miraiGet("/memberInfo") {
-            val member = it.bot.getGroupOrFail(paramOrNull("target")).getOrFail(paramOrNull("memberId"))
-            call.respondDTO(MemberDetailDTO(member))
-        }
-
-        miraiVerify<MemberInfoDTO>("/memberInfo") { dto ->
-            val member = dto.session.bot.getGroupOrFail(dto.target).getOrFail(dto.memberId)
-            with(dto.info) {
-                name?.let { member.nameCard = it }
-                specialTitle?.let { member.specialTitle = it }
-            }
-            call.respondStateCode(StateCode.Success)
-        }
-
-    }
-}
-
-
-@Serializable
-private data class MuteDTO(
-    override val sessionKey: String,
-    val target: Long,
-    val memberId: Long = 0,
-    val time: Int = 0
-) : VerifyDTO()
-
-@Serializable
-private data class KickDTO(
-    override val sessionKey: String,
-    val target: Long,
-    val memberId: Long,
-    val msg: String = ""
-) : VerifyDTO()
-
-@Serializable
-private data class QuitDTO(
-    override val sessionKey: String,
-    val target: Long
-) : VerifyDTO()
-
-@Serializable
-private data class GroupConfigDTO(
-    override val sessionKey: String,
-    val target: Long,
-    val config: GroupDetailDTO
-) : VerifyDTO()
-
-@Serializable
-private data class GroupDetailDTO(
-    val name: String? = null,
-    val announcement: String? = null,
-    val confessTalk: Boolean? = null,
-    val allowMemberInvite: Boolean? = null,
-    val autoApprove: Boolean? = null,
-    val anonymousChat: Boolean? = null
-) : DTO {
-    constructor(group: Group) : this(
-        group.name,
-        group.settings.entranceAnnouncement,
-        false,
-        group.settings.isAllowMemberInvite,
-        group.settings.isAutoApproveEnabled,
-        group.settings.isAnonymousChatEnabled
-    )
-}
-
-@Serializable
-private data class MemberInfoDTO(
-    override val sessionKey: String,
-    val target: Long,
-    val memberId: Long,
-    val info: MemberDetailDTO
-) : VerifyDTO()
-
-@Serializable
-private data class MemberDetailDTO(
-    val name: String? = null,
-    val specialTitle: String? = null
-) : DTO {
-    constructor(member: Member) : this(member.nameCard, member.specialTitle)
-}

+ 0 - 79
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/InfoRouteModule.kt

@@ -1,79 +0,0 @@
-/*
- * Copyright 2020 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/master/LICENSE
- */
-
-package net.mamoe.mirai.api.http.route
-
-import io.ktor.application.*
-import io.ktor.routing.*
-import net.mamoe.mirai.api.http.data.common.GroupDTO
-import net.mamoe.mirai.api.http.data.common.MemberDTO
-import net.mamoe.mirai.api.http.data.common.QQDTO
-import net.mamoe.mirai.api.http.util.toJson
-
-/**
- * 基本信息路由
- */
-fun Application.infoModule() {
-    routing {
-
-        /**
-         * 查询好友列表
-         */
-        miraiGet("/friendList") {
-            val ls = it.bot.friends.toList().map { qq -> QQDTO(qq) }
-            call.respondJson(ls.toJson())
-        }
-
-        /**
-         * 查询QQ群列表
-         */
-        miraiGet("/groupList") {
-            val ls = it.bot.groups.toList().map { group -> GroupDTO(group) }
-            call.respondJson(ls.toJson())
-        }
-
-        /**
-         * 查询QQ群成员列表
-         */
-        miraiGet("/memberList") {
-            val ls = it.bot.getGroupOrFail(paramOrNull("target")).members.toList().map { member -> MemberDTO(member) }
-            call.respondJson(ls.toJson())
-        }
-
-//        /**
-//         * 查询机器人个人信息
-//         */
-//        miraiGet("/botProfile") {
-//            // TODO: 等待queryProfile()支持
-//            val profile = it.bot.selfQQ
-//            call.respondJson(profile.toJson())
-//        }
-//
-//        /**
-//         * 查询好友个人信息
-//         */
-//        miraiGet("/friendProfile") {
-//            // TODO: 等待queryProfile()支持
-//            val profile = it.bot.getFriend(paramOrNull("friendId"))
-//            call.respondJson(profile.toJson())
-//        }
-//
-//        /**
-//         * 查询QQ群成员个人信息
-//         */
-//        miraiGet("/memberProfile") {
-//            // TODO: 等待queryProfile()支持
-//            val profile = it.bot
-//                .getGroup(paramOrNull("groupId"))
-//                .get(paramOrNull("memberId"))
-//                .queryProfile()
-//            call.respondJson(profile.toJson())
-//        }
-    }
-}

+ 0 - 359
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/MessageRouteModule.kt

@@ -1,359 +0,0 @@
-/*
- * Copyright 2020 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/master/LICENSE
- */
-
-package net.mamoe.mirai.api.http.route
-
-import io.ktor.application.*
-import io.ktor.http.content.*
-import io.ktor.routing.*
-import kotlinx.serialization.Serializable
-import net.mamoe.mirai.api.http.HttpApiPluginBase
-import net.mamoe.mirai.api.http.data.IllegalAccessException
-import net.mamoe.mirai.api.http.data.IllegalParamException
-import net.mamoe.mirai.api.http.data.StateCode
-import net.mamoe.mirai.api.http.data.common.*
-import net.mamoe.mirai.api.http.context.session.manager.generateSessionKey
-import net.mamoe.mirai.api.http.util.toJson
-import net.mamoe.mirai.contact.Contact
-import net.mamoe.mirai.message.MessageReceipt
-import net.mamoe.mirai.message.data.*
-import net.mamoe.mirai.message.data.Image.Key.queryUrl
-import net.mamoe.mirai.message.data.MessageSource.Key.quote
-import net.mamoe.mirai.message.data.MessageSource.Key.recall
-import net.mamoe.mirai.message.data.OnlineMessageSource.Incoming
-import net.mamoe.mirai.message.data.OnlineMessageSource.Outgoing
-import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
-import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
-import java.net.URL
-
-/**
- * 消息路由
- */
-fun Application.messageModule() {
-    routing {
-
-        /**
-         * 获取MessageQueue剩余消息数量
-         */
-        miraiGet("/countMessage") {
-            val count: Int = it.messageQueue.size
-
-            call.respondDTO(IntRestfulResult(data = count))
-        }
-
-        /**
-         * 获取指定条数最老的消息并从MessageQueue删除获取的消息
-         */
-        miraiGet("/fetchMessage") {
-            val count: Int = paramOrNull("count")
-            val list = it.messageQueue.fetch(count)
-
-            call.respondDTO(EventListRestfulResult(data = list))
-        }
-
-        /**
-         * 获取指定条数最新的消息并从MessageQueue删除获取的消息
-         */
-        miraiGet("/fetchLatestMessage") {
-            val count: Int = paramOrNull("count")
-            val list = it.messageQueue.fetchLatest(count)
-
-            call.respondDTO(EventListRestfulResult(data = list))
-        }
-
-        /**
-         * 获取指定条数最老的消息,和/fetchMessage不一样,这个方法不会删除消息
-         */
-        miraiGet("/peakMessage") {
-            val count: Int = paramOrNull("count")
-            val list = it.messageQueue.peek(count)
-
-            call.respondDTO(EventListRestfulResult(data = list))
-        }
-
-        /**
-         * 获取指定条数最新的消息,和/fetchLatestMessage不一样,这个方法不会删除消息
-         */
-        miraiGet("/peekLatestMessage") {
-            val count: Int = paramOrNull("count")
-            val list = it.messageQueue.peekLatest(count)
-
-            call.respondDTO(EventListRestfulResult(data = list))
-        }
-
-        /**
-         * 获取指定ID消息(从CacheQueue获取)
-         */
-        miraiGet("/messageFromId") {
-            val id: Int = paramOrNull("id")
-            it.cacheQueue[id].apply {
-
-                val dto = when (this) {
-                    is Outgoing.ToGroup -> GroupMessagePacketDTO(MemberDTO(target.botAsMember))
-                    is Outgoing.ToFriend -> FriendMessagePacketDTO(QQDTO(sender.asFriend))
-                    is Outgoing.ToTemp -> TempMessagePacketDto(MemberDTO(target))
-                    is Outgoing.ToStranger -> StrangerMessagePacketDto(QQDTO(target))
-
-                    is Incoming.FromGroup -> GroupMessagePacketDTO(MemberDTO(sender))
-                    is Incoming.FromFriend -> FriendMessagePacketDTO(QQDTO(sender))
-                    is Incoming.FromTemp -> TempMessagePacketDto(MemberDTO(sender))
-                    is Incoming.FromStranger -> StrangerMessagePacketDto(QQDTO(sender))
-                }
-
-                dto.messageChain = messageChainOf(this, originalMessage)
-                    .toMessageChainDTO { d -> d != UnknownMessageDTO }
-                call.respondDTO(
-                    EventRestfulResult(
-                        data = dto
-                    )
-                )
-            }
-        }
-
-        /**
-         * 发送消息
-         */
-        suspend fun <C : Contact> sendMessage(
-            quote: QuoteReply?,
-            messageChain: MessageChain,
-            target: C
-        ): MessageReceipt<Contact> {
-            val send = if (quote == null) {
-                messageChain
-            } else {
-                ((quote + messageChain) as Iterable<Message>).toMessageChain()
-            }
-            return target.sendMessage(send)
-        }
-
-        /**
-         * 发送消息给好友
-         */
-        miraiVerify<SendDTO>("/sendFriendMessage") {
-            val quote = it.quote?.let { q ->
-                it.session.cacheQueue[q].run {
-                    this.quote()
-                }
-            }
-
-            val bot = it.session.bot
-
-            fun findQQ(qq: Long): Contact = bot.getFriend(qq)
-                    ?: bot.getStranger(qq)
-                    ?: throw NoSuchElementException("friend $qq not found")
-
-            val qq = when {
-                it.target != null -> findQQ(it.target)
-                it.qq != null -> findQQ(it.qq)
-                else -> throw NoSuchElementException()
-            }
-
-            val receipt = sendMessage(quote, it.messageChain.toMessageChain(qq), qq)
-            it.session.cacheQueue.add(receipt.source)
-
-            call.respondDTO(SendRetDTO(messageId = receipt.source.ids.firstOrNull() ?: 0))
-        }
-
-        /**
-         * 发送消息到QQ群
-         */
-        miraiVerify<SendDTO>("/sendGroupMessage") {
-            val quote = it.quote?.let { q ->
-                it.session.cacheQueue[q].run {
-                    this.quote()
-                }
-            }
-
-            val bot = it.session.bot
-            val group = when {
-                it.target != null -> bot.getGroupOrFail(it.target)
-                it.group != null -> bot.getGroupOrFail(it.group)
-                else -> throw NoSuchElementException()
-            }
-
-            val receipt = sendMessage(quote, it.messageChain.toMessageChain(group), group)
-            it.session.cacheQueue.add(receipt.source)
-
-            call.respondDTO(SendRetDTO(messageId = receipt.source.ids.firstOrNull() ?: 0))
-        }
-
-        /**
-         * 发送消息给临时会话
-         */
-        miraiVerify<SendDTO>("/sendTempMessage") {
-            val quote = it.quote?.let { q ->
-                it.session.cacheQueue[q].run {
-                    this.quote()
-                }
-            }
-
-            val bot = it.session.bot
-            val member = when {
-                it.qq != null && it.group != null -> bot.getGroupOrFail(it.group).getOrFail(it.qq)
-                else -> throw NoSuchElementException()
-            }
-
-            val receipt = sendMessage(quote, it.messageChain.toMessageChain(member), member)
-            it.session.cacheQueue.add(receipt.source)
-
-            call.respondDTO(SendRetDTO(messageId = receipt.source.ids.firstOrNull() ?: 0))
-        }
-
-        /**
-         * 发送图片消息
-         */
-        miraiVerify<SendImageDTO>("sendImageMessage") {
-            val bot = it.session.bot
-            val contact = when {
-                it.target != null -> bot.getFriend(it.target) ?: bot.getGroupOrFail(it.target)
-                it.qq != null && it.group != null -> bot.getGroupOrFail(it.group).getOrFail(it.qq)
-                it.qq != null -> bot.getFriendOrFail(it.qq)
-                it.group != null -> bot.getGroupOrFail(it.group)
-                else -> throw IllegalParamException("target、qq、group不可全为null")
-            }
-            val ls = it.urls.map { url -> URL(url).openStream().uploadAsImage(contact) }
-            val receipt = contact.sendMessage(buildMessageChain { addAll(ls) })
-
-            it.session.cacheQueue.add(receipt.source)
-            call.respondJson(ls.map { image -> image.imageId }.toJson())
-        }
-
-        // TODO: 重构
-        miraiMultiPart("uploadImage") { session, parts ->
-
-            var path: String?
-
-            val type = parts.value("type")
-            parts.file("img")?.apply {
-
-                val image = streamProvider().use {
-                    // originalFileName assert not null
-                    val newFile = HttpApiPluginBase.saveImageAsync(
-                        originalFileName ?: generateSessionKey(), it.readBytes()
-                    )
-
-                    when (type) {
-                        "group" -> session.bot.groups.firstOrNull()?.uploadImage(newFile.await().toExternalResource())
-                        "friend",
-                        "temp"
-                        -> session.bot.friends.firstOrNull()?.uploadImage(newFile.await().toExternalResource())
-                        else -> null
-                    }.apply {
-                        // 使用apply不影响when返回
-                        path = newFile.await().absolutePath
-                    }
-                }
-
-                image?.apply {
-                    call.respondDTO(
-                        UploadImageRetDTO(
-                            imageId,
-                            queryUrl(),
-                            path
-                        )
-                    )
-                } ?: throw IllegalAccessException("图片上传错误")
-
-            } ?: throw IllegalAccessException("未知错误")
-        }
-
-        miraiMultiPart("uploadVoice") { session, parts ->
-
-            var path: String?
-
-            val type = parts.value("type")
-            parts.file("voice")?.apply {
-
-                val voice = streamProvider().use {
-                    // originalFileName assert not null
-                    val newFile = HttpApiPluginBase.saveVoiceAsync(
-                        originalFileName ?: generateSessionKey(), it.readBytes()
-                    )
-
-                    when (type) {
-                        "group" -> session.bot.groups.firstOrNull()?.uploadVoice(newFile.await().toExternalResource())
-                        else -> null
-                    }.apply {
-                        // 使用apply不影响when返回
-                        path = newFile.await().absolutePath
-
-                    }
-                }
-
-                voice?.apply {
-                    call.respondDTO(
-                        UploadVoiceRetDTO(
-                            fileName,
-                            url,
-                            path
-                        )
-                    )
-                } ?: throw IllegalAccessException("语音上传错误")
-
-            } ?: throw IllegalAccessException("未知错误")
-        }
-
-        /**
-         * 撤回消息
-         */
-        miraiVerify<RecallDTO>("recall") {
-            it.session.cacheQueue[it.target].recall()
-            call.respondStateCode(StateCode.Success)
-        }
-    }
-}
-
-@Serializable
-private data class SendDTO(
-    override val sessionKey: String,
-    val quote: Int? = null,
-    val target: Long? = null,
-    val qq: Long? = null,
-    val group: Long? = null,
-    val messageChain: MessageChainDTO
-) : VerifyDTO()
-
-@Serializable
-private data class SendImageDTO(
-    override val sessionKey: String,
-    val target: Long? = null,
-    val qq: Long? = null,
-    val group: Long? = null,
-    val urls: List<String>
-) : VerifyDTO()
-
-@Serializable
-@Suppress("unused")
-private class SendRetDTO(
-    val code: Int = 0,
-    val msg: String = "success",
-    val messageId: Int
-) : DTO
-
-@Serializable
-@Suppress("unused")
-private class UploadImageRetDTO(
-    val imageId: String,
-    val url: String,
-    val path: String?
-) : DTO
-
-@Serializable
-@Suppress("unused")
-private class UploadVoiceRetDTO(
-    val voiceId: String,
-    val url: String?,
-    val path: String?
-) : DTO
-
-@Serializable
-private data class RecallDTO(
-    override val sessionKey: String,
-    val target: Int
-) : VerifyDTO()

+ 0 - 134
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/WebSocketRouteModule.kt

@@ -1,134 +0,0 @@
-/*
- * Copyright 2020 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/master/LICENSE
- */
-
-package net.mamoe.mirai.api.http.route
-
-import io.ktor.application.*
-import io.ktor.http.cio.websocket.*
-import io.ktor.routing.*
-import io.ktor.util.pipeline.*
-import io.ktor.websocket.*
-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.data.StateCode
-import net.mamoe.mirai.api.http.data.common.IgnoreEventDTO
-import net.mamoe.mirai.api.http.data.common.toDTO
-import net.mamoe.mirai.api.http.util.toJson
-import net.mamoe.mirai.event.events.BotEvent
-import net.mamoe.mirai.event.events.MessageEvent
-import net.mamoe.mirai.event.subscribeAlways
-import net.mamoe.mirai.event.subscribeMessages
-
-/**
- * 广播路由
- */
-fun Application.websocketRouteModule() {
-    routing {
-
-        /**
-         * 广播通知消息
-         */
-        miraiWebsocket("/message") { session ->
-            val listener = session.bot.eventChannel.subscribeMessages {
-                content { bot === session.bot }.invoke {
-                    this.toDTO().takeIf { dto -> dto != IgnoreEventDTO }?.apply {
-                        outgoing.send(Frame.Text(this.toJson()))
-                    }
-                }
-            }
-
-            try {
-                for (frame in incoming) {
-                    outgoing.send(frame)
-                }
-            } finally {
-                listener.complete()
-            }
-        }
-
-        /**
-         * 广播通知事件
-         */
-        miraiWebsocket("/event") { session ->
-            val listener = session.bot.eventChannel.subscribeAlways<BotEvent> {
-                if (it.bot === session.bot && this !is MessageEvent) {
-                    this.toDTO().takeIf { dto -> dto != IgnoreEventDTO }?.apply {
-                        outgoing.send(Frame.Text(this.toJson()))
-                    }
-                }
-            }
-
-            try {
-                for (frame in incoming) {
-                    outgoing.send(frame)
-                }
-            } finally {
-                listener.complete()
-            }
-        }
-
-        /**
-         * 广播通知所有信息(消息,事件)
-         */
-        miraiWebsocket("/all") { session ->
-            val listener = session.bot.eventChannel.subscribeAlways<BotEvent> {
-                if (it.bot === session.bot) {
-                    this.toDTO().takeIf { dto -> dto != IgnoreEventDTO }?.apply {
-                        outgoing.send(Frame.Text(this.toJson()))
-                    }
-                }
-            }
-
-            try {
-                for (frame in incoming) {
-                    outgoing.send(frame)
-                }
-            } finally {
-                listener.complete()
-            }
-        }
-    }
-}
-
-
-@ContextDsl
-private inline fun Route.miraiWebsocket(
-    path: String,
-    crossinline body: suspend DefaultWebSocketServerSession.(AuthedSession) -> Unit
-) {
-    webSocket(path) {
-        val sessionKey = call.parameters["sessionKey"]
-        if (sessionKey == null) {
-            outgoing.send(Frame.Text(StateCode(400, "参数格式错误").toJson(StateCode.serializer())))
-            close(CloseReason(CloseReason.Codes.NORMAL, "参数格式错误"))
-            return@webSocket
-        }
-        if (!SessionManager.containSession(sessionKey)) {
-            outgoing.send(Frame.Text(StateCode.IllegalSession.toJson(StateCode.serializer())))
-            close(CloseReason(CloseReason.Codes.NORMAL, "Session失效或不存在"))
-            return@webSocket
-        }
-        if (SessionManager[sessionKey] is TempSession) {
-            outgoing.send(Frame.Text(StateCode.NotVerifySession.toJson(StateCode.serializer())))
-            close(CloseReason(4, "Session未认证"))
-            return@webSocket
-        }
-
-
-        val session = SessionManager[sessionKey] as AuthedSession
-        if (!session.config.enableWebsocket) {
-            outgoing.send(Frame.Text(StateCode.PermissionDenied.toJson(StateCode.serializer())))
-            close(CloseReason(10, "无操作权限"))
-            return@webSocket
-        }
-
-        body(session)
-    }
-}

+ 0 - 38
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/service/MiraiApiHttpService.kt

@@ -1,38 +0,0 @@
-/*
- * Copyright 2020 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/master/LICENSE
- */
-
-package net.mamoe.mirai.api.http.service
-
-import net.mamoe.mirai.console.plugin.Plugin
-
-/**
- * MiraiApiHttp抽象服务
- */
-interface MiraiApiHttpService {
-
-    /**
-     * Mirai Console
-     */
-    val console: Plugin
-
-    /**
-     * 对应MiraiConsole生命周期
-     */
-    fun onLoad();
-
-    /**
-     * 对应MiraiConsole生命周期
-     */
-    fun onEnable();
-
-    /**
-     * 对应MiraiConsole生命周期
-     */
-    fun onDisable();
-}

+ 0 - 41
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/service/MiraiApiHttpServices.kt

@@ -1,41 +0,0 @@
-/*
- * Copyright 2020 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/master/LICENSE
- */
-
-package net.mamoe.mirai.api.http.service
-
-import net.mamoe.mirai.api.http.service.heartbeat.HeartBeatService
-import net.mamoe.mirai.api.http.service.report.ReportService
-import net.mamoe.mirai.console.plugin.Plugin
-import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
-
-
-class MiraiApiHttpServices(override val console: JvmPlugin) : MiraiApiHttpService {
-    private val services: List<MiraiApiHttpService> = listOf(
-        HeartBeatService(console),
-        ReportService(console)
-    )
-
-    override fun onLoad() {
-        services.forEach {
-            it.onLoad()
-        }
-    }
-
-    override fun onEnable() {
-        services.forEach {
-            it.onEnable()
-        }
-    }
-
-    override fun onDisable() {
-        services.forEach {
-            it.onDisable()
-        }
-    }
-}

+ 0 - 74
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/service/heartbeat/HeartBeatService.kt

@@ -1,74 +0,0 @@
-/*
- * Copyright 2020 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/master/LICENSE
- */
-
-package net.mamoe.mirai.api.http.service.heartbeat
-
-import kotlinx.coroutines.launch
-import net.mamoe.mirai.api.http.config.Setting
-import net.mamoe.mirai.api.http.service.MiraiApiHttpService
-import net.mamoe.mirai.api.http.util.HttpClient
-import net.mamoe.mirai.console.plugin.Plugin
-import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
-import java.util.*
-import kotlin.concurrent.timerTask
-
-/**
- * 心跳服务
- */
-class HeartBeatService(override val console: JvmPlugin) : MiraiApiHttpService {
-
-    val config get() = Setting.heartbeat
-
-    /**
-     * 心跳计时器
-     */
-    private var timer: Timer = Timer("HeartBeat", false)
-
-    override fun onLoad() {
-    }
-
-    override fun onEnable() {
-        timer.schedule(timerTask {
-            if (config.enable) {
-                console.launch {
-                    pingAllDestinations()
-                }
-            }
-        }, config.delay, config.period)
-
-        console.logger.info("心跳模块启用状态: ${config.enable}")
-    }
-
-    override fun onDisable() {
-        timer.cancel()
-        timer.purge()
-
-        console.logger.info("心跳模块已禁用")
-    }
-
-    /**
-     * 发送心跳到所有目标地址
-     */
-    private suspend fun pingAllDestinations() {
-        config.destinations.forEach {
-            ping(it)
-        }
-    }
-
-    /**
-     * 发送心跳到指定地址
-     */
-    private suspend fun ping(destination: String) {
-        try {
-            HttpClient.post(destination, config.extraBody, config.extraHeaders)
-        } catch (e: Exception) {
-            console.logger.error("发送${destination}心跳失败: ${e.message}")
-        }
-    }
-}

+ 0 - 111
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/service/report/ReportService.kt

@@ -1,111 +0,0 @@
-/*
- * Copyright 2020 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/master/LICENSE
- */
-
-package net.mamoe.mirai.api.http.service.report
-
-import kotlinx.coroutines.launch
-import net.mamoe.mirai.api.http.config.Setting
-import net.mamoe.mirai.api.http.data.common.IgnoreEventDTO
-import net.mamoe.mirai.api.http.data.common.toDTO
-import net.mamoe.mirai.api.http.service.MiraiApiHttpService
-import net.mamoe.mirai.api.http.util.HttpClient
-import net.mamoe.mirai.api.http.util.toJson
-import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
-import net.mamoe.mirai.event.GlobalEventChannel
-import net.mamoe.mirai.event.Listener
-import net.mamoe.mirai.event.events.*
-import net.mamoe.mirai.event.subscribeAlways
-import net.mamoe.mirai.utils.error
-
-/**
- * 上报服务
- */
-class ReportService(
-    /**
-     * 插件对象
-     */
-    override val console: JvmPlugin
-) : MiraiApiHttpService {
-
-    /**
-     * 心跳配置
-     */
-    private val reportConfig get() = Setting.report
-
-    /**
-     * 事件监听器
-     */
-    private var subscription: Listener<BotEvent>? = null
-
-    override fun onLoad() {
-    }
-
-    override fun onEnable() {
-        subscription = GlobalEventChannel.subscribeAlways {
-            this.takeIf { reportConfig.enable }
-                ?.apply {
-                    this.takeIf { reportConfig.eventMessage.report }
-                        ?.takeIf { event -> event !is MessageEvent }
-                        ?.toDTO()
-                        ?.takeIf { dto -> dto != IgnoreEventDTO }
-                        ?.apply {
-                            reportAllDestinations(this.toJson(), bot.id)
-                        }
-
-                    this.takeIf { reportConfig.groupMessage.report }
-                        ?.takeIf { event -> event is GroupMessageEvent }
-                        ?.apply {
-                            reportAllDestinations(this.toDTO().toJson(), bot.id)
-                        }
-
-                    this.takeIf { reportConfig.tempMessage.report }
-                        ?.takeIf { event -> event is TempMessageEvent }
-                        ?.apply {
-                            reportAllDestinations(this.toDTO().toJson(), bot.id)
-                        }
-
-                    this.takeIf { reportConfig.friendMessage.report }
-                        ?.takeIf { event -> event is FriendMessageEvent }
-                        ?.apply {
-                            reportAllDestinations(this.toDTO().toJson(), bot.id)
-                        }
-                }
-        }
-
-        console.logger.info("上报模块启用状态: ${reportConfig.enable}")
-    }
-
-    override fun onDisable() {
-        subscription?.complete()
-
-        console.logger.info("上报模块已禁用")
-    }
-
-    /**
-     * 上报到所有目标地址
-     */
-    private fun reportAllDestinations(json: String, botId: Long) {
-        console.launch {
-            reportConfig.destinations.forEach {
-                report(it, json, botId)
-            }
-        }
-    }
-
-    /**
-     * 上报到指定目标地址
-     */
-    private suspend fun report(destination: String, json: String, botId: Long) {
-        try {
-            HttpClient.post(destination, json, reportConfig.extraHeaders, botId)
-        } catch (e: Exception) {
-            console.logger.error { "上报${destination}失败: ${e.message}" }
-        }
-    }
-}

+ 123 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/setting/MainSetting.kt

@@ -0,0 +1,123 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.api.http.setting
+
+import net.mamoe.mirai.api.http.HttpApiPluginBase
+import net.mamoe.mirai.api.http.context.session.manager.generateRandomSessionKey
+import net.mamoe.mirai.console.data.PluginConfig
+import net.mamoe.mirai.console.data.ReadOnlyPluginData
+import net.mamoe.mirai.console.data.value
+
+typealias Destination = String
+typealias Destinations = List<Destination>
+
+/**
+ * Mirai Api Http 的配置文件类,它应该是单例,并且在 [HttpApiPluginBase.onEnable] 时被初始化
+ */
+object MainSetting : ReadOnlyPluginData("setting"), PluginConfig {
+//    /**
+//     * 上报子消息配置
+//     *
+//     * @property report 是否上报
+//     */
+//    @Serializable
+//    data class Reportable(val report: Boolean)
+//
+//    /**
+//     * 上报服务配置
+//     *
+//     * @property enable 是否开启上报
+//     * @property groupMessage 群消息子配置
+//     * @property friendMessage 好友消息子配置
+//     * @property tempMessage 临时消息子配置
+//     * @property eventMessage 事件消息子配置
+//     * @property destinations 上报地址(多个),必选
+//     * @property extraHeaders 上报时的额外头信息
+//     */
+//    @Serializable
+//    data class Report(
+//        val enable: Boolean = false,
+//        val groupMessage: Reportable = Reportable(true),
+//        val friendMessage: Reportable = Reportable(true),
+//        val tempMessage: Reportable = Reportable(true),
+//        val eventMessage: Reportable = Reportable(true),
+//        val destinations: Destinations = emptyList(),
+//        val extraHeaders: Map<String, String> = emptyMap()
+//    )
+//
+//    /**
+//     * 心跳服务配置
+//     *
+//     * @property enable 是否启动心跳服务
+//     * @property delay 心跳启动延迟
+//     * @property period 心跳周期
+//     * @property destinations 心跳 PING 的地址列表,必选
+//     * @property extraBody 心跳额外请求体
+//     * @property extraHeaders 心跳额外请求头
+//     */
+//    @Serializable
+//    data class HeartBeat(
+//        val enable: Boolean = false,
+//        val delay: Long = 1000,
+//        val period: Long = 15000,
+//        val destinations: Destinations = emptyList(),
+//        val extraBody: Map<String, String> = emptyMap(),
+//        val extraHeaders: Map<String, String> = emptyMap(),
+//    )
+
+//    val cors: List<String> by value(listOf("*"))
+
+    val modules: List<String> by value(listOf("http"))
+
+    /**
+     * mirai api http 所使用的地址,默认为 0.0.0.0
+     */
+    val host: String by value("0.0.0.0")
+
+    /**
+     * mirai api http 所使用的端口,默认为 8080
+     */
+    val port: Int by value(8080)
+
+    /**
+     * 认证模式, 创建连接是否需要开启认证
+     */
+    val enableVerify: Boolean by value(true)
+
+    /**
+     * 认证密钥,默认为随机
+     */
+    val verifyKey: String by value("INITKEY" + generateRandomSessionKey())
+
+    /**
+     * 单实例模式,只使用一个 bot,无需绑定 session 区分
+     */
+    val singleMode: Boolean by value(false)
+
+    /**
+     * 消息记录缓存区大小,默认为 4096
+     */
+    val cacheSize: Int by value(4096)
+//
+//    /**
+//     * 是否启用 websocket 服务
+//     */
+//    val enableWebsocket: Boolean by value(false)
+//
+//    /**
+//     * 上报服务配置
+//     */
+//    val report: Report by value(Report())
+//
+//    /**
+//     * 心跳服务配置
+//     */
+//    val heartbeat: HeartBeat by value(HeartBeat())
+}

+ 0 - 70
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/util/Json.kt

@@ -1,70 +0,0 @@
-/*
- * Copyright 2020 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/master/LICENSE
- */
-
-package net.mamoe.mirai.api.http.util
-
-import kotlinx.serialization.*
-import kotlinx.serialization.json.Json
-import kotlinx.serialization.modules.SerializersModule
-import net.mamoe.mirai.api.http.data.common.*
-import kotlin.reflect.KClass
-
-// 解析失败时直接返回null,由路由判断响应400状态
-inline fun <reified T : Any> String.jsonParseOrNull(
-    serializer: DeserializationStrategy<T>? = null
-): T? = try {
-    if (serializer == null) MiraiJson.json.decodeFromString(this) else Json.decodeFromString(this)
-} catch (e: Exception) {
-    null
-}
-
-
-inline fun <reified T : Any> T.toJson(
-    serializer: SerializationStrategy<T>? = null
-): String = if (serializer == null) MiraiJson.json.encodeToString(this)
-else MiraiJson.json.encodeToString(serializer, this)
-
-
-// 序列化列表时,stringify需要使用的泛型是T,而非List<T>
-// 因为使用的stringify的stringify(objs: List<T>)重载
-
-inline fun <reified T : Any> List<T>.toJson(
-    serializer: SerializationStrategy<List<T>>? = null
-): String = if (serializer == null) MiraiJson.json.encodeToString(this)
-else MiraiJson.json.encodeToString(serializer, this)
-
-
-/**
- * Json解析规则,需要注册支持的多态的类
- */
-object MiraiJson {
-
-    @OptIn(InternalSerializationApi::class)
-    val json = Json {
-
-        encodeDefaults = true
-        isLenient = true
-        ignoreUnknownKeys = true
-
-        @Suppress("UNCHECKED_CAST")
-        serializersModule = SerializersModule {
-
-
-            polymorphic(EventDTO::class, GroupMessagePacketDTO::class, GroupMessagePacketDTO.serializer())
-            polymorphic(EventDTO::class, FriendMessagePacketDTO::class, FriendMessagePacketDTO.serializer())
-            polymorphic(EventDTO::class, TempMessagePacketDto::class, TempMessagePacketDto.serializer())
-
-            BotEventDTO::class.sealedSubclasses.forEach {
-                val clazz = it as KClass<BotEventDTO>
-                polymorphic(EventDTO::class, clazz, clazz.serializer())
-
-            }
-        }
-    }
-}

+ 11 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/util/bot.kt

@@ -0,0 +1,11 @@
+package net.mamoe.mirai.api.http.util
+
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.api.http.adapter.common.NoSuchBotException
+
+
+internal fun getBotOrThrow(qq: Long) = try {
+    Bot.getInstance(qq)
+} catch (e: NoSuchElementException) {
+    throw NoSuchBotException
+}

+ 41 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/util/extends.kt

@@ -0,0 +1,41 @@
+package net.mamoe.mirai.api.http.util
+
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
+
+@OptIn(ExperimentalContracts::class)
+inline fun <T> whenTrueOrNull(condition: Boolean, blk: () -> T): T? {
+    contract {
+        callsInPlace(blk, InvocationKind.AT_MOST_ONCE)
+    }
+
+    return if (condition) blk() else null
+}
+
+@OptIn(ExperimentalContracts::class)
+inline fun <T> whenTrueOrThrow(condition: Boolean, blk: () -> T): T {
+    contract {
+        callsInPlace(blk, InvocationKind.AT_MOST_ONCE)
+    }
+
+    return if (condition) blk() else throw IllegalStateException("condition is not true")
+}
+
+@OptIn(ExperimentalContracts::class)
+inline fun <T> whenFalseOrNull(condition: Boolean, blk: () -> T): T? {
+    contract {
+        callsInPlace(blk, InvocationKind.AT_MOST_ONCE)
+    }
+
+    return if (!condition) blk() else null
+}
+
+@OptIn(ExperimentalContracts::class)
+inline fun <T> whenFalseOrThrow(condition: Boolean, blk: () -> T): T {
+    contract {
+        callsInPlace(blk, InvocationKind.AT_MOST_ONCE)
+    }
+
+    return if (!condition) blk() else throw IllegalStateException("condition is not false")
+}

+ 2 - 2
mirai-api-http/src/test/kotlin/mirai/RunMirai.kt

@@ -5,7 +5,6 @@ import net.mamoe.mirai.api.http.HttpApiPluginBase
 import net.mamoe.mirai.console.MiraiConsole
 import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.enable
 import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.load
-import net.mamoe.mirai.console.terminal.MiraiConsoleTerminalLoader
 import net.mamoe.mirai.console.util.ConsoleExperimentalApi
 
 object RunMirai {
@@ -15,7 +14,8 @@ object RunMirai {
     @ConsoleExperimentalApi
     @JvmStatic
     fun main(args: Array<String>) {
-        MiraiConsoleTerminalLoader.startAsDaemon()
+        // TODO: fix me
+//        MiraiConsoleTerminalLoader.startAsDaemon()
 
         HttpApiPluginBase.load()
         HttpApiPluginBase.enable()

+ 44 - 0
mirai-api-http/src/test/kotlin/net/mamoe/mirai/api/http/adapter/launch/HttpAdapterLaunch.kt

@@ -0,0 +1,44 @@
+package net.mamoe.mirai.api.http.adapter.launch
+
+import kotlinx.coroutines.runBlocking
+import net.mamoe.mirai.BotFactory
+import net.mamoe.mirai.api.http.MahPluginImpl
+import net.mamoe.mirai.api.http.adapter.MahAdapterFactory
+import net.mamoe.mirai.api.http.context.session.manager.DefaultSessionManager
+import net.mamoe.mirai.api.http.setting.MainSetting
+import net.mamoe.mirai.utils.BotConfiguration
+import org.junit.Test
+
+class HttpAdapterLaunch : LaunchTester() {
+
+    @Test
+    fun launch() {
+        if (!enable) return
+
+        runBlocking {
+            with(MainSetting) {
+
+                // 创建上下文启动 mah 插件
+                MahPluginImpl.start {
+                    sessionManager = DefaultSessionManager(verifyKey)
+                    enableVerify = false
+                    singleMode = true
+                    localMode = true
+
+                    MahAdapterFactory.build("http")?.let(::plus)
+                    MahAdapterFactory.build("ws")?.let(::plus)
+                }
+            }
+
+            val bot = BotFactory.newBot(qq, password) {
+                fileBasedDeviceInfo("../device.json")
+
+                protocol = BotConfiguration.MiraiProtocol.ANDROID_WATCH
+            }
+
+            bot.login()
+            bot.join()
+            Thread.sleep(90000000)
+        }
+    }
+}

+ 19 - 0
mirai-api-http/src/test/kotlin/net/mamoe/mirai/api/http/adapter/launch/LaunchTester.kt

@@ -0,0 +1,19 @@
+package net.mamoe.mirai.api.http.adapter.launch
+
+import java.io.File
+import java.util.*
+
+abstract class LaunchTester {
+
+    private val properties: Properties by lazy {
+        Properties().apply {
+            File("../launcher.properties").inputStream().use { load(it) }
+        }
+    }
+
+    protected val enable: Boolean get() = properties.getProperty("enable").toBoolean()
+
+    protected val qq: Long get() = properties.getProperty("qq").toLong()
+
+    protected val password: String get() = properties.getProperty("password")
+}

+ 56 - 0
mirai-api-http/src/test/kotlin/net/mamoe/mirai/api/http/adapter/serialization/SerializationTest.kt

@@ -0,0 +1,56 @@
+package net.mamoe.mirai.api.http.adapter.serialization
+
+import net.mamoe.mirai.api.http.adapter.common.StateCode
+import net.mamoe.mirai.api.http.adapter.internal.dto.*
+import net.mamoe.mirai.api.http.adapter.internal.serializer.jsonParseOrNull
+import net.mamoe.mirai.api.http.adapter.internal.serializer.toJson
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class SerializationTest {
+
+    /**
+     * 测试消息链序列化情况
+     */
+    @Test
+    fun testMessageChain() {
+        val chain = groupMessageDTO()
+        val json = chain.toJson()
+        assertEquals(chain, json.jsonParseOrNull(), "messageChain 序列化异常")
+    }
+
+    /**
+     * 测试 message 和 event 多态序列化
+     */
+    @Test
+    fun testPolymorphic() {
+        val expected = """
+            [{"type":"GroupMessage","messageChain":[{"type":"At","target":0,"display":"at name"},{"type":"Plain","text":"test plain text content"}],"sender":{"id":0,"memberName":"","permission":"OWNER","group":{"id":0,"name":"","permission":"OWNER"}}},{"type":"FriendMessage","messageChain":[{"type":"At","target":0,"display":"at name"},{"type":"Plain","text":"test plain text content"}],"sender":{"id":0,"nickname":"","remark":""}}]
+        """.trimIndent()
+        val ls: List<EventDTO> = listOf(groupMessageDTO(), friendMessageDTO())
+        val json = ls.toJson()
+        // 实际运行时,只有多个 package 的队列进行序列化,而不会有反序列化出现
+        // 测试不考虑多 messagePacket 的反序列化
+        assertEquals(expected, json, "消息序列化异常")
+    }
+
+    /**
+     * 内置状态码序列化测试
+     */
+    @Test
+    fun testBuildInStateCode() {
+        val expected = """{"code":0,"msg":"success"}"""
+        val json = StateCode.Success.toJson()
+        assertEquals(expected, json, "State code 序列化异常")
+    }
+
+    /**
+     * 自定义状态码序列化测试
+     */
+    @Test
+    fun testCustomStateCode() {
+        val expected = """{"code":400,"msg":"test access error"}"""
+        val json = StateCode.IllegalAccess("test access error").toJson()
+        assertEquals(expected, json, "State code: IllegalAccess 序列化异常")
+    }
+}

+ 24 - 0
mirai-api-http/src/test/kotlin/net/mamoe/mirai/api/http/adapter/serialization/factory.kt

@@ -0,0 +1,24 @@
+package net.mamoe.mirai.api.http.adapter.serialization
+
+import net.mamoe.mirai.api.http.adapter.internal.dto.*
+import net.mamoe.mirai.contact.MemberPermission
+
+fun groupMessageDTO(id: Long = 0, name: String = ""): GroupMessagePacketDTO {
+    return GroupMessagePacketDTO(
+        sender = MemberDTO(
+            id, name, MemberPermission.OWNER,
+            group = GroupDTO(id, name, MemberPermission.OWNER)
+        )
+    ).apply { messageChain = messageChainDTO() }
+}
+
+fun friendMessageDTO(id: Long = 0, name: String = ""): FriendMessagePacketDTO {
+    return FriendMessagePacketDTO(sender = QQDTO(id, name, name))
+        .apply { messageChain = messageChainDTO() }
+}
+
+fun messageChainDTO() = listOf(atMessageDTO(), textMessageDTO())
+
+fun atMessageDTO(target: Long = 0, display: String = "at name"): AtDTO = AtDTO(target, display)
+fun textMessageDTO(): PlainDTO = PlainDTO("test plain text content")
+

+ 0 - 15
settings.gradle

@@ -1,15 +0,0 @@
-pluginManagement {
-    repositories {
-        mavenLocal()
-        jcenter()
-        google()
-        mavenCentral()
-        maven { url "https://plugins.gradle.org/m2/" }
-    }
-}
-
-rootProject.name = 'mirai-api-http'
-
-include(':mirai-api-http')
-
-enableFeaturePreview('GRADLE_METADATA')

+ 15 - 0
settings.gradle.kts

@@ -0,0 +1,15 @@
+pluginManagement {
+    repositories {
+        mavenLocal()
+        jcenter()
+        google()
+        mavenCentral()
+        maven(url = "https://plugins.gradle.org/m2/")
+    }
+}
+
+rootProject.name = "mirai-api-http"
+
+include(":mirai-api-http")
+
+enableFeaturePreview("GRADLE_METADATA")