Skip to content

Commit e11c42a

Browse files
committed
refactor: move Milky serving logic into acidify-milky
1 parent d04554e commit e11c42a

34 files changed

Lines changed: 615 additions & 457 deletions

.devin/wiki.json

Lines changed: 54 additions & 75 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
- `acidify-core` - PC & Android NTQQ 协议的核心实现 [![Maven Central](https://img.shields.io/maven-central/v/org.ntqqrev/acidify-core?label=Maven%20Central&logo=maven&color=blue)](https://central.sonatype.com/artifact/org.ntqqrev/acidify-core)
1818
- `@acidify/core` - `acidify-core` 的 Kotlin/JS 导出版本 [![npm](https://img.shields.io/npm/v/%40acidify%2Fcore)](https://www.npmjs.com/package/@acidify/core)
19+
- `acidify-core-runner` - 基于 `acidify-core` 的最小命令行运行器
20+
- `acidify-milky` - 基于 `acidify-core` 的 Milky 协议适配层,提供 HTTP 接口与类型转换
1921
- `yogurt` - 基于 `acidify-core` 的 QQ 协议端 [![GitHub Release](https://img.shields.io/github/v/release/SaltifyDev/yogurt-releases?label=GitHub%20release)](https://github.com/SaltifyDev/yogurt-releases)
2022
- `@acidify/yogurt` - Yogurt 的预编译二进制包 [![npm](https://img.shields.io/npm/v/%40acidify%2Fyogurt)](https://www.npmjs.com/package/@acidify/yogurt)
2123
- `yogurt-jvm` - Yogurt 的 JVM 平台适配 (Workaround for Ktor plugin's incompatibility issue)

acidify-milky/build.gradle.kts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
plugins {
2+
id("buildsrc.convention.kotlin-multiplatform")
3+
alias(libs.plugins.kotlin.serialization)
4+
alias(libs.plugins.kotlinx.atomicfu)
5+
}
6+
7+
group = "org.ntqqrev"
8+
version = libs.versions.milky.get()
9+
10+
kotlin {
11+
compilerOptions {
12+
freeCompilerArgs.add("-Xcontext-parameters")
13+
}
14+
15+
sourceSets {
16+
commonMain.dependencies {
17+
implementation(project(":acidify-core"))
18+
implementation(libs.ktor.server.core)
19+
implementation(libs.ktor.server.di)
20+
implementation(libs.ktor.server.content.negotiation)
21+
implementation(libs.ktor.server.sse)
22+
implementation(libs.ktor.server.websockets)
23+
implementation(libs.ktor.client.core)
24+
implementation(libs.ktor.client.content.negotiation)
25+
implementation(libs.ktor.serialization.kotlinx.json)
26+
implementation(libs.milky.types)
27+
}
28+
}
29+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package org.ntqqrev.acidify.milky
2+
3+
import org.ntqqrev.acidify.message.ImageFormat
4+
import kotlin.time.Duration
5+
6+
interface Codec {
7+
suspend fun getImageInfo(input: ByteArray): ImageInfo
8+
suspend fun audioToPcm(input: ByteArray): ByteArray
9+
suspend fun silkEncode(input: ByteArray): ByteArray
10+
suspend fun calculatePcmDuration(
11+
input: ByteArray,
12+
bitDepth: Int = 16,
13+
channelCount: Int = 1,
14+
sampleRate: Int = 24000,
15+
): Duration
16+
17+
suspend fun getVideoInfo(videoData: ByteArray): VideoInfo
18+
suspend fun getVideoFirstFrameJpg(videoData: ByteArray): ByteArray
19+
}
20+
21+
data class ImageInfo(
22+
val format: ImageFormat,
23+
val width: Int,
24+
val height: Int,
25+
)
26+
27+
data class VideoInfo(
28+
val width: Int,
29+
val height: Int,
30+
val duration: Duration,
31+
)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package org.ntqqrev.acidify.milky
2+
3+
import io.ktor.server.application.*
4+
import io.ktor.server.plugins.di.*
5+
import io.ktor.server.routing.*
6+
import kotlinx.coroutines.launch
7+
import org.ntqqrev.acidify.AbstractBot
8+
import org.ntqqrev.acidify.milky.api.apiAuth
9+
import org.ntqqrev.acidify.milky.api.apiLoginProtect
10+
import org.ntqqrev.acidify.milky.api.apiRoutes
11+
import org.ntqqrev.acidify.milky.event.eventAuth
12+
import org.ntqqrev.acidify.milky.event.eventSse
13+
import org.ntqqrev.acidify.milky.event.eventWebSocket
14+
import org.ntqqrev.acidify.milky.event.eventWebhook
15+
16+
context(ctx: MilkyContext)
17+
fun Route.configureMilky() {
18+
route("/api") {
19+
if (ctx.httpAccessToken.isNotEmpty()) {
20+
apiAuth()
21+
}
22+
apiLoginProtect()
23+
apiRoutes()
24+
}
25+
route("/event") {
26+
if (ctx.httpAccessToken.isNotEmpty()) {
27+
eventAuth()
28+
}
29+
eventWebSocket()
30+
eventSse()
31+
}
32+
with(application) {
33+
eventWebhook()
34+
monitor.subscribe(ApplicationStarted) {
35+
launch {
36+
val bot = dependencies.resolve<AbstractBot>()
37+
ctx.pipeBotEventFlow(bot.eventFlow)
38+
}
39+
}
40+
}
41+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package org.ntqqrev.acidify.milky
2+
3+
import io.ktor.server.application.*
4+
import kotlinx.coroutines.CoroutineScope
5+
import kotlinx.coroutines.channels.BufferOverflow
6+
import kotlinx.coroutines.flow.Flow
7+
import kotlinx.coroutines.flow.MutableSharedFlow
8+
import kotlinx.coroutines.flow.mapNotNull
9+
import kotlinx.coroutines.launch
10+
import org.ntqqrev.acidify.event.AcidifyEvent
11+
import org.ntqqrev.acidify.milky.transform.transformAcidifyEvent
12+
import org.ntqqrev.milky.Event
13+
14+
open class MilkyContext(
15+
val application: Application,
16+
val implName: String,
17+
val implVersion: String,
18+
val protocolOs: String,
19+
val httpAccessToken: String,
20+
val webhookEndpoints: List<WebhookEndpoint>,
21+
val reportSelfMessage: Boolean,
22+
val resolveUri: suspend (uri: String) -> ByteArray,
23+
val codec: Codec,
24+
) : CoroutineScope by application {
25+
class WebhookEndpoint(
26+
val url: String,
27+
val accessToken: String,
28+
)
29+
30+
val eventFlow = MutableSharedFlow<Event>(
31+
extraBufferCapacity = 100,
32+
onBufferOverflow = BufferOverflow.DROP_OLDEST,
33+
)
34+
35+
internal fun pipeBotEventFlow(flow: Flow<AcidifyEvent>) = launch {
36+
flow.mapNotNull { transformAcidifyEvent(it) }
37+
.collect {
38+
eventFlow.emit(it)
39+
}
40+
}
41+
}

yogurt/src/commonMain/kotlin/org/ntqqrev/yogurt/api/Auth.kt renamed to acidify-milky/src/commonMain/kotlin/org/ntqqrev/acidify/milky/api/Auth.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
package org.ntqqrev.yogurt.api
1+
package org.ntqqrev.acidify.milky.api
22

33
import io.ktor.http.*
44
import io.ktor.server.application.*
55
import io.ktor.server.response.*
66
import io.ktor.server.routing.*
7-
import org.ntqqrev.yogurt.YogurtApp.config
7+
import org.ntqqrev.acidify.milky.MilkyContext
88

9-
fun Route.configureMilkyApiAuth() = install(createRouteScopedPlugin("ApiAuth") {
9+
context(ctx: MilkyContext)
10+
fun Route.apiAuth() = install(createRouteScopedPlugin("ApiAuth") {
1011
onCall { call ->
11-
if (call.request.headers["Authorization"] != "Bearer ${config.milky.http.accessToken}") {
12+
if (call.request.headers["Authorization"] != "Bearer ${ctx.httpAccessToken}") {
1213
call.respond(HttpStatusCode.Unauthorized)
1314
return@onCall
1415
}

yogurt/src/commonMain/kotlin/org/ntqqrev/yogurt/api/HttpRoutes.kt renamed to acidify-milky/src/commonMain/kotlin/org/ntqqrev/acidify/milky/api/HttpRoutes.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package org.ntqqrev.yogurt.api
1+
package org.ntqqrev.acidify.milky.api
22

33
import io.ktor.server.plugins.*
44
import io.ktor.server.plugins.di.*
@@ -9,22 +9,24 @@ import kotlinx.serialization.json.encodeToJsonElement
99
import org.ntqqrev.acidify.AbstractBot
1010
import org.ntqqrev.acidify.exception.OidbException
1111
import org.ntqqrev.acidify.exception.ServiceException
12+
import org.ntqqrev.acidify.milky.MilkyContext
13+
import org.ntqqrev.acidify.milky.api.handler.*
1214
import org.ntqqrev.milky.ApiEndpoint
1315
import org.ntqqrev.milky.ApiGeneralResponse
1416
import org.ntqqrev.milky.milkyJsonModule
15-
import org.ntqqrev.yogurt.api.handler.*
1617
import kotlin.time.DurationUnit
1718
import kotlin.time.measureTime
1819

1920
inline fun <reified T : Any, reified R : Any> ApiEndpoint<T, R>.define(
2021
noinline handler: suspend MilkyApiContext.(T) -> R
2122
) = MilkyApiHandler(this.path, handler)
2223

24+
context(ctx: MilkyContext)
2325
private inline fun <reified T : Any, reified R : Any> Route.serve(
2426
handler: MilkyApiHandler<T, R>
2527
) = post(handler.path) {
2628
val bot = application.dependencies.resolve<AbstractBot>()
27-
val context = MilkyApiContext(bot, application)
29+
val context = MilkyApiContext(bot, ctx)
2830
val logger = bot.createLogger("HttpModule")
2931
try {
3032
val payload = call.receive<T>()
@@ -98,7 +100,8 @@ private inline fun <reified T : Any, reified R : Any> Route.serve(
98100
}
99101
}
100102

101-
fun Route.configureMilkyApiHttpRoutes() {
103+
context(ctx: MilkyContext)
104+
fun Route.apiRoutes() {
102105
serve(GetLoginInfo)
103106
serve(GetImplInfo)
104107
serve(GetUserProfile)

yogurt/src/commonMain/kotlin/org/ntqqrev/yogurt/api/LoginProtect.kt renamed to acidify-milky/src/commonMain/kotlin/org/ntqqrev/acidify/milky/api/LoginProtect.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package org.ntqqrev.yogurt.api
1+
package org.ntqqrev.acidify.milky.api
22

33
import io.ktor.server.application.*
44
import io.ktor.server.plugins.di.*
@@ -7,9 +7,9 @@ import io.ktor.server.routing.*
77
import org.ntqqrev.acidify.AbstractBot
88
import org.ntqqrev.milky.ApiGeneralResponse
99

10-
fun Route.configureMilkyApiLoginProtect() = install(createRouteScopedPlugin("ProtectNotLoggedIn") {
10+
fun Route.apiLoginProtect() = install(createRouteScopedPlugin("ProtectNotLoggedIn") {
1111
onCall { call ->
12-
val bot = this@configureMilkyApiLoginProtect.application.dependencies.resolve<AbstractBot>()
12+
val bot = this@apiLoginProtect.application.dependencies.resolve<AbstractBot>()
1313
if (!bot.isLoggedIn) {
1414
call.respond(
1515
ApiGeneralResponse(
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.ntqqrev.acidify.milky.api
2+
3+
import io.ktor.server.application.*
4+
import org.ntqqrev.acidify.AbstractBot
5+
import org.ntqqrev.acidify.milky.Codec
6+
import org.ntqqrev.acidify.milky.MilkyContext
7+
8+
class MilkyApiContext(
9+
val bot: AbstractBot,
10+
application: Application,
11+
implName: String,
12+
implVersion: String,
13+
protocolOs: String,
14+
httpAccessToken: String,
15+
webhookEndpoints: List<WebhookEndpoint>,
16+
reportSelfMessage: Boolean,
17+
resolveUri: suspend (uri: String) -> ByteArray,
18+
codec: Codec,
19+
) : MilkyContext(
20+
application,
21+
implName,
22+
implVersion,
23+
protocolOs,
24+
httpAccessToken,
25+
webhookEndpoints,
26+
reportSelfMessage,
27+
resolveUri,
28+
codec
29+
) {
30+
constructor(bot: AbstractBot, ctx: MilkyContext) : this(
31+
bot,
32+
ctx.application,
33+
ctx.implName,
34+
ctx.implVersion,
35+
ctx.protocolOs,
36+
ctx.httpAccessToken,
37+
ctx.webhookEndpoints,
38+
ctx.reportSelfMessage,
39+
ctx.resolveUri,
40+
ctx.codec,
41+
)
42+
}

0 commit comments

Comments
 (0)