Skip to content

Commit

Permalink
Merge pull request #163 from simple-robot/dev/support-qq-group
Browse files Browse the repository at this point in the history
API模块中支持群聊和c2c相关事件的intents与事件监听;支持并迁移到新的 access_token 的鉴权方式。
  • Loading branch information
ForteScarlet authored Jul 11, 2024
2 parents 5c2c5cf + 495eb44 commit bc6293e
Show file tree
Hide file tree
Showing 19 changed files with 1,310 additions and 74 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/deploy-v4-website.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ env:
# Replace HI with the ID of the instance in capital letters
ARTIFACT: webHelpQG2-all.zip
# Writerside docker image version
DOCKER_VERSION: 233.14389
DOCKER_VERSION: 241.16003
# Add the variable below to upload Algolia indexes
# Replace HI with the ID of the instance in capital letters
ALGOLIA_ARTIFACT: algolia-indexes-QG.zip
Expand Down Expand Up @@ -127,7 +127,7 @@ jobs:
needs: [build, test]
runs-on: ubuntu-latest
container:
image: registry.jetbrains.team/p/writerside/builder/algolia-publisher:2.0.32-2
image: registry.jetbrains.team/p/writerside/builder/algolia-publisher:2.0.32-3

env:

Expand Down
611 changes: 598 additions & 13 deletions simbot-component-qq-guild-api/api/simbot-component-qq-guild-api.api

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -43,28 +43,40 @@ import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.jvm.JvmOverloads
import kotlin.jvm.JvmSynthetic

/**
* 格式值:"BOT_APPID", 机器人 AppID
*/
public const val X_UNION_APPID_HEADER: String = "X-Union-Appid"

/**
* 使用 [client] 向当前目标 API [QQGuildApi] 发起请求。
*
* see [鉴权方式](https://bot.q.qq.com/wiki/develop/api-v2/dev-prepare/interface-framework/api-use.html#鉴权方式)
*
* @param server 如果不为 null 则会取 [server] 中的 [Url.protocol]、[Url.hostWithPort]

Check warning on line 59 in simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/api/ApiRequests.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unresolved reference in KDoc

Cannot resolve symbol 'Url'

Check warning on line 59 in simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/api/ApiRequests.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unresolved reference in KDoc

Cannot resolve symbol 'hostWithPort'

Check warning on line 59 in simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/api/ApiRequests.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unresolved reference in KDoc

Cannot resolve symbol 'Url'

Check warning on line 59 in simbot-component-qq-guild-api/src/commonMain/kotlin/love/forte/simbot/qguild/api/ApiRequests.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unresolved reference in KDoc

Cannot resolve symbol 'protocol'
* 替换 [QQGuildApi.url] 中提供的值。
*
*/
@JvmSynthetic
@JvmOverloads // 二进制兼容
public suspend fun <R : Any> QQGuildApi<R>.request(
client: HttpClient,
token: String,
server: Url? = null
token: String? = null,
server: Url? = null,
appId: String? = null,
): HttpResponse {
val api = this

return client.request {
method = api.method

headers {
this[HttpHeaders.Authorization] = token
token?.also { this[HttpHeaders.Authorization] = it }
appId?.also { this[X_UNION_APPID_HEADER] = it }

with(api.headers) {
if (!isEmpty()) {
appendAll(api.headers)
Expand Down Expand Up @@ -127,15 +139,21 @@ public suspend fun <R : Any> QQGuildApi<R>.request(
@OptIn(ExperimentalContracts::class)
public suspend inline fun <R : Any> QQGuildApi<R>.requestText(
client: HttpClient,
token: String,
server: Url = QQGuild.URL,
token: String?,
server: Url? = QQGuild.URL,
appId: String? = null,
useResp: (HttpResponse) -> Unit = {}
): String {
contract {
callsInPlace(useResp, InvocationKind.EXACTLY_ONCE)
}

val resp = request(client, token, server)
val resp = request(
client = client,
token = token,
server = server,
appId = appId
)
useResp(resp)

val text = resp.bodyAsText()
Expand Down Expand Up @@ -175,14 +193,16 @@ public suspend inline fun <R : Any> QQGuildApi<R>.requestText(
* @see ErrInfo
*/
@JvmSynthetic
@JvmOverloads
public suspend fun <R : Any> QQGuildApi<R>.requestData(
client: HttpClient,
token: String,
server: Url = QQGuild.URL,
token: String?,
server: Url? = QQGuild.URL,
decoder: Json = QQGuild.DefaultJson,
appId: String? = null,
): R {
val resp: HttpResponse
val text = requestText(client, token, server) { resp = it }
val text = requestText(client, token, server, appId) { resp = it }

checkStatus(text, QQGuild.DefaultJson, resp.status, resp)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright (c) 2024. ForteScarlet.
*
* This file is part of simbot-component-qq-guild.
*
* simbot-component-qq-guild is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Foundation,
* either version 3 of the License, or (at your option) any later version.
*
* simbot-component-qq-guild is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with simbot-component-qq-guild.
* If not, see <https://www.gnu.org/licenses/>.
*/

package love.forte.simbot.qguild.api.app

import io.ktor.http.*
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import love.forte.simbot.qguild.ApiModel
import love.forte.simbot.qguild.api.PostQQGuildApi
import kotlin.jvm.JvmField
import kotlin.jvm.JvmStatic


/**
* [获取调用凭证](https://bot.q.qq.com/wiki/develop/api-v2/dev-prepare/interface-framework/api-use.html#获取调用凭证)
*
* 这个API似乎是一个特殊的API,它有自己的HTTP URL: `https://bots.qq.com/app/getAppAccessToken`,
* 使用时需要专门处理。
*
* @author ForteScarlet
*/
public class GetAppAccessTokenApi private constructor(
override val body: Body
) : PostQQGuildApi<AppAccessToken>() {
public companion object Factory {
public const val HTTP_URL_STRING: String = "https://bots.qq.com"

@JvmField
public val httpUrl: Url = Url(HTTP_URL_STRING)

private val PATH = arrayOf("app", "getAppAccessToken")

/**
* Create [GetAppAccessTokenApi].
*/
@JvmStatic
public fun create(
appId: String,
clientSecret: String
): GetAppAccessTokenApi = GetAppAccessTokenApi(Body(appId, clientSecret))
}


override val resultDeserializationStrategy: DeserializationStrategy<AppAccessToken>
get() = AppAccessToken.serializer()

override val path: Array<String>
get() = PATH

override fun createBody(): Any = body

override val url: Url = URLBuilder(httpUrl).apply {
appendEncodedPathSegments(components = PATH)
}.build()

@Serializable
public data class Body(
val appId: String,
val clientSecret: String
)
}

/**
* Result of [GetAppAccessTokenApi]
* @property accessToken 获取到的凭证。
* @property expiresIn 凭证有效时间,单位:秒。目前是7200秒之内的值。
*/
@ApiModel
@Serializable
public data class AppAccessToken(
@SerialName("access_token")
val accessToken: String,
@SerialName("expires_in")
val expiresIn: Int,
)
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,81 @@ public sealed class EventIntents {

}

/**
* ```
* GROUP_AND_C2C_EVENT (1 << 25)
* - C2C_MESSAGE_CREATE // 用户单聊发消息给机器人时候
* - FRIEND_ADD // 用户添加使用机器人
* - FRIEND_DEL // 用户删除机器人
* - C2C_MSG_REJECT // 用户在机器人资料卡手动关闭"主动消息"推送
* - C2C_MSG_RECEIVE // 用户在机器人资料卡手动开启"主动消息"推送开关
* - GROUP_AT_MESSAGE_CREATE // 用户在群里@机器人时收到的消息
* - GROUP_ADD_ROBOT // 机器人被添加到群聊
* - GROUP_DEL_ROBOT // 机器人被移出群聊
* - GROUP_MSG_REJECT // 群管理员主动在机器人资料页操作关闭通知
* - GROUP_MSG_RECEIVE // 群管理员主动在机器人资料页操作开启通知
* ```
*/
public data object GroupAndC2CEvent : EventIntents() {
/** C2C群聊相关事件 `intents` */
@get:JvmStatic
@get:JvmName("getIntents")
public val intents: Intents = Intents(1 shl 25)

override val intentsValue: Int
get() = intents.value

/**
* 用户单聊发消息给机器人时候
*/
public const val C2C_MESSAGE_CREATE_TYPE: String = "C2C_MESSAGE_CREATE"

/**
* 用户添加使用机器人
*/
public const val FRIEND_ADD_TYPE: String = "FRIEND_ADD"

/**
* 用户删除机器人
*/
public const val FRIEND_DEL_TYPE: String = "FRIEND_DEL"

/**
* 用户在机器人资料卡手动关闭"主动消息"推送
*/
public const val C2C_MSG_REJECT_TYPE: String = "C2C_MSG_REJECT"

/**
* 用户在机器人资料卡手动开启"主动消息"推送开关
*/
public const val C2C_MSG_RECEIVE_TYPE: String = "C2C_MSG_RECEIVE"

/**
* 用户在群里@机器人时收到的消息
*/
public const val GROUP_AT_MESSAGE_CREATE_TYPE: String = "GROUP_AT_MESSAGE_CREATE"

/**
* 机器人被添加到群聊
*/
public const val GROUP_ADD_ROBOT_TYPE: String = "GROUP_ADD_ROBOT"

/**
* 机器人被移出群聊
*/
public const val GROUP_DEL_ROBOT_TYPE: String = "GROUP_DEL_ROBOT"

/**
* 群管理员主动在机器人资料页操作关闭通知
*/
public const val GROUP_MSG_REJECT_TYPE: String = "GROUP_MSG_REJECT"

/**
* 群管理员主动在机器人资料页操作开启通知
*/
public const val GROUP_MSG_RECEIVE_TYPE: String = "GROUP_MSG_RECEIVE"
}

/**
* ```
* INTERACTION (1 << 26)
Expand Down Expand Up @@ -493,6 +568,7 @@ public val EventIntentsInstances: Array<EventIntents> = arrayOf(
DirectMessage,
OpenForumsEvent,
AudioOrLiveChannelMember,
GroupAndC2CEvent,
Interaction,
MessageAudit,
ForumsEvent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ public sealed class Signal<D>(@Serializable(Opcode.SerializerByCode::class) publ

/**
*
* @property token 创建机器人的时候分配的,格式为 `Bot {appid}.{app_token}`
* @property token 创建机器人的时候分配的,
* 格式为 `QQBot {ACCESS_TOKEN}`
* @property intents 此次连接所需要接收的事件,具体可参考 [Intents]
* @property shard 该参数是用来进行水平分片的。该参数是个拥有两个元素的数组。
* 例如:`[0,4]`,代表分为四个片,当前链接是第 0 个片,业务稍后应该继续建立 shard 为 `[1,4]`, `[2,4]`, `[3,4]` 的链接,才能完整接收事件。更多详细的内容可以参考 [Shard]。
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright (c) 2024. ForteScarlet.
*
* This file is part of simbot-component-qq-guild.
*
* simbot-component-qq-guild is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Foundation,
* either version 3 of the License, or (at your option) any later version.
*
* simbot-component-qq-guild is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with simbot-component-qq-guild.
* If not, see <https://www.gnu.org/licenses/>.
*/

package love.forte.simbot.qguild.event

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable


/**
* @property timestamp 添加时间戳
* @property openid 用户openid
*/
@Serializable
public data class C2CManagementData(
val timestamp: Int,
val openid: String,
)

/**
* 用户模块-用户管理相关事件。
* [data] 类型为 [C2CManagementData]
*/
public sealed class C2CManagementDispatch : Signal.Dispatch() {
abstract override val data: C2CManagementData
}

/**
* [用户添加机器人](https://bot.q.qq.com/wiki/develop/api-v2/server-inter/user/manage/event.html#用户添加机器人)
*
* 触发场景 用户添加机器人'好友'到消息列表
*/
@Serializable
@SerialName(EventIntents.GroupAndC2CEvent.FRIEND_ADD_TYPE)
public data class FriendAdd(
override val s: Long,
@SerialName("d")
override val data: C2CManagementData
) : C2CManagementDispatch()

/**
* [用户删除机器人](https://bot.q.qq.com/wiki/develop/api-v2/server-inter/user/manage/event.html#用户删除机器人)
*
* 触发场景 用户删除机器人'好友'
*/
@Serializable
@SerialName(EventIntents.GroupAndC2CEvent.FRIEND_DEL_TYPE)
public data class FriendDel(
override val s: Long,
@SerialName("d")
override val data: C2CManagementData
) : C2CManagementDispatch()

/**
* [拒绝机器人主动消息](https://bot.q.qq.com/wiki/develop/api-v2/server-inter/user/manage/event.html#拒绝机器人主动消息)
*
* 触发场景 用户在机器人资料卡手动关闭"主动消息"推送
*/
@Serializable
@SerialName(EventIntents.GroupAndC2CEvent.C2C_MSG_REJECT_TYPE)
public data class C2CMsgReject(
override val s: Long,
@SerialName("d")
override val data: C2CManagementData
) : C2CManagementDispatch()

/**
* [允许机器人主动消息](https://bot.q.qq.com/wiki/develop/api-v2/server-inter/user/manage/event.html#允许机器人主动消息)
*
* 触发场景 用户在机器人资料卡手动开启"主动消息"推送开关
*/
@Serializable
@SerialName(EventIntents.GroupAndC2CEvent.C2C_MSG_RECEIVE_TYPE)
public data class C2CMsgReceive(
override val s: Long,
@SerialName("d")
override val data: C2CManagementData
) : C2CManagementDispatch()
Loading

0 comments on commit bc6293e

Please sign in to comment.