Webhook
根据官方文档的说法:
因此后续应该就只剩下 webhook 一种事件订阅方式了,也就是客户端作为被动接受事件的一方。
本章节简单介绍下在QQ机器人组件中如何使用 webhook 的方式接收事件。
禁用Websocket
首先,在配置中禁用 ws 的连接。
如果你在使用配置文件(常见于在配合spring时),添加配置项 config.disableWs=true
{
"ticket": {
"appId": "...",
"secret": "...",
"secret": "...",
},
"config": {
"disableWs":true
}
}
// 设置 disableWs 为 true
configuration.disableWs = true
处理HTTP请求
Webhook 通过接收来自QQ的事件推送请求来实现事件监听。 在组件中,暂不支持内嵌的 HTTP 服务器 (参考 #224), 因此你需要自行搭建 HTTP 服务器。
此处给出在 Spring Boot 和 Ktor server 场景下的示例。
private const val SIGNATURE_HEAD = "X-Signature-Ed25519"
private const val TIMESTAMP_HEAD = "X-Signature-Timestamp"
/**
* 处理所有qq机器人的回调请求的处理器。
*/
@RestController("/callback")
class CallbackHandler(
private val application: Application
) {
/**
* 处理 `/callback/qq/{appId}` 的事件回调请求,
* 找到对应的 bot 并向其推送事件。
*/
@PostMapping("/qq/{appId}")
fun handleEvent(
@PathVariable("appId") appId: String,
@RequestHeader(SIGNATURE_HEAD) signature: String,
@RequestHeader(TIMESTAMP_HEAD) timestamp: String,
@RequestBody payload: String,
): CompletableFuture<out ResponseEntity<Any?>> {
// 寻找指定 `appId` 的 QGBot
val targetBot = application.botManagers
.filterIsQQGuildBotManagers()
.firstNotNullOfOrNull {
it.all().firstOrNull { bot ->
bot.source.ticket.appId == appId
}
}
// 如果找不到,响应 404 异常
if (targetBot == null) {
throw ResponseStatusException(
HttpStatus.NOT_FOUND,
"app $appId not found"
)
}
// 在 servlet web 中,在异步中处理.
// 作用域、是否要用异步等根据你的项目情况调整。
val entityAsync = application.async {
val result = targetBot.emitEvent(
payload,
) {
// 配置 ed25519SignatureVerification, 即代表进行签名校验
ed25519SignatureVerification = Ed25519SignatureVerification(
signature,
timestamp
)
}
val body: Any? = when (result) {
is EmitResult.Verified -> result.verified
else -> null
}
// 响应结果。
ResponseEntity.ok(body)
}
return entityAsync.asCompletableFuture()
}
}
private const val SIGNATURE_HEAD = "X-Signature-Ed25519"
private const val TIMESTAMP_HEAD = "X-Signature-Timestamp"
/**
* 处理所有qq机器人的回调请求的处理器。
*/
@RestController("/callback")
class CallbackHandler(
private val application: Application
) {
/**
* 处理 `/callback/qq/{appId}` 的事件回调请求,
* 找到对应的 bot 并向其推送事件。
*/
@PostMapping("/qq/{appId}")
suspend fun handleEvent(
@PathVariable("appId") appId: String,
@RequestHeader(SIGNATURE_HEAD) signature: String,
@RequestHeader(TIMESTAMP_HEAD) timestamp: String,
@RequestBody payload: String,
): ResponseEntity<Any?> {
// 寻找指定 `appId` 的 QGBot
val targetBot = application.botManagers
.filterIsQQGuildBotManagers()
.firstNotNullOfOrNull {
it.all().firstOrNull { bot ->
bot.source.ticket.appId == appId
}
}
// 如果找不到,响应 404 异常
if (targetBot == null) {
throw ResponseStatusException(
HttpStatus.NOT_FOUND,
"app $appId not found"
)
}
val result = targetBot.emitEvent(
payload,
) {
// 配置 ed25519SignatureVerification, 即代表进行签名校验
ed25519SignatureVerification = Ed25519SignatureVerification(
signature,
timestamp
)
}
val body: Any? = when (result) {
is EmitResult.Verified -> result.verified
else -> null
}
// 响应结果
return ResponseEntity.ok(body)
}
}
private const val SIGNATURE_HEAD = "X-Signature-Ed25519"
private const val TIMESTAMP_HEAD = "X-Signature-Timestamp"
// 你也可以考虑直接把注册好的 bot 保存起来,而不只是保存 application
// 或者使用一些DI方案,都可以。
lateinit var simbotApplication: love.forte.simbot.application.Application
suspend fun main() {
// 启动simbot application,
// 然后启动内嵌的 HTTP 服务
// 当然,具体的启动顺序或逻辑根据你的项目需求而定。
simbotApplication = launchSimbot()
embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module)
.start(wait = true)
}
/**
* 启动 simbot application
*/
suspend fun launchSimbot(): love.forte.simbot.application.Application {
val application = launchSimpleApplication {
useQQGuild()
}
// 这里配置你的 bot、事件监听等...
// 你也可以考虑直接把注册好的 bot 保存起来,而不只是保存 application
return application
}
fun Application.module() {
configureRouting()
}
fun Application.configureRouting() {
routing {
post("/callback/qq/{appId}") {
val appId = call.parameters["appId"]
// 寻找指定 `appId` 的 QGBot
val targetBot = simbotApplication.botManagers
.filterIsQQGuildBotManagers()
.firstNotNullOfOrNull {
it.all().firstOrNull { bot ->
bot.source.ticket.appId == appId
}
}
// 如果找不到,响应 404 异常
if (targetBot == null) {
call.respond(HttpStatusCode.NotFound)
return@post
}
// 准备参数
val signature = call.request.header(SIGNATURE_HEAD)
?: run {
call.respond(
HttpStatusCode.BadRequest,
"Required header $SIGNATURE_HEAD is missing"
)
return@post
}
val timestamp = call.request.header(TIMESTAMP_HEAD)
?: run {
call.respond(
HttpStatusCode.BadRequest,
"Required header $TIMESTAMP_HEAD is missing"
)
return@post
}
val payload = call.receiveText()
val result = targetBot.emitEvent(
payload,
) {
// 配置 ed25519SignatureVerification, 即代表进行签名校验
ed25519SignatureVerification = Ed25519SignatureVerification(
signature,
timestamp
)
}
val respond: String? = when (result) {
is EmitResult.Verified ->
// 如果你安装了插件 ContentNegotiation,
// 那么也可以直接响应对象。
// 这里懒得装了,所以提前序列化成JSON字符串
Json.encodeToString(result.verified)
else -> null
}
// 响应成功结果
call.respondText(
respond ?: "{}",
ContentType.Application.Json
)
}
}
}
Opcode=13 路径验证
在首次配置回调地址时,服务端会对此地址发送 opcode=13
的校验请求。
组件默认支持处理此事件,如果 opcode=13
, emitEvent
则会返回 EmitResult.Verified
, 其中的 verified
就是 opcode=13
时服务端所需要的结果。
val respond: Any? = when (result) {
is EmitResult.Verified -> result.verified
else -> null
}
直接将 verified
响应即可。
签名校验
在接收到事件推送时,可以通过请求头中的签名信息结合bot的 secret
校验本次请求。
组件默认支持进行请求头校验,在使用 emitEvent
时提供所需的校验信息即可。
var options = new EmitEventOptions();
options.setEd25519SignatureVerification(
new Ed25519SignatureVerification(
// X-Signature-Ed25519
signature,
// X-Signature-Timestamp
timestamp
);
);
bot.emitEventXxx(payload, options);
// emitEventBlocking(...);
// emitEventAsync(...);
// emitEventReserve(...);
bot.emitEvent(payload) {
// 配置 ed25519SignatureVerification, 即代表进行签名校验
ed25519SignatureVerification = Ed25519SignatureVerification(
// X-Signature-Ed25519
signature,
// X-Signature-Timestamp
timestamp
)
}
示例
可以在仓库的 samples 模块下找到示例模块。
Last modified: 15 November 2024