From 4a2b510a703e9ac23d5bfab2efeee66e0c3040ca Mon Sep 17 00:00:00 2001 From: Him188 Date: Mon, 5 Jul 2021 14:28:10 +0800 Subject: [PATCH] Remove announcements' low-level API and bundle them into `AnnouncementProtocol`, improve code style and maintainability --- .../commonMain/kotlin/LowLevelApiAccessor.kt | 63 +--- .../contact/announcement/AnnouncementImage.kt | 8 +- .../announcement/AnnouncementParameters.kt | 10 +- .../AnnouncementParametersBuilder.kt | 10 +- .../contact/announcement/Announcements.kt | 2 + .../announcement/OnlineAnnouncement.kt | 2 + mirai-core/src/commonMain/kotlin/MiraiImpl.kt | 135 +-------- .../kotlin/contact/AnnouncementsImpl.kt | 257 ---------------- .../commonMain/kotlin/contact/GroupImpl.kt | 1 + .../contact/announcement/AnnouncementsImpl.kt | 284 ++++++++++++++++++ .../announcement}/GroupAnnouncement.kt | 70 ++--- .../src/commonMain/kotlin/network/keys.kt | 11 + .../src/commonMain/kotlin/utils/io/output.kt | 15 + 13 files changed, 364 insertions(+), 504 deletions(-) delete mode 100644 mirai-core/src/commonMain/kotlin/contact/AnnouncementsImpl.kt create mode 100644 mirai-core/src/commonMain/kotlin/contact/announcement/AnnouncementsImpl.kt rename {mirai-core-api/src/commonMain/kotlin/data => mirai-core/src/commonMain/kotlin/contact/announcement}/GroupAnnouncement.kt (55%) diff --git a/mirai-core-api/src/commonMain/kotlin/LowLevelApiAccessor.kt b/mirai-core-api/src/commonMain/kotlin/LowLevelApiAccessor.kt index f36eecedf..aee96bace 100644 --- a/mirai-core-api/src/commonMain/kotlin/LowLevelApiAccessor.kt +++ b/mirai-core-api/src/commonMain/kotlin/LowLevelApiAccessor.kt @@ -16,7 +16,10 @@ import net.mamoe.kjbb.JvmBlockingBridge import net.mamoe.mirai.contact.* import net.mamoe.mirai.data.* import net.mamoe.mirai.message.data.Voice -import net.mamoe.mirai.utils.* +import net.mamoe.mirai.utils.MiraiExperimentalApi +import net.mamoe.mirai.utils.MiraiInternalApi +import net.mamoe.mirai.utils.NotStableForInheritance +import net.mamoe.mirai.utils.WeakRef import kotlin.annotation.AnnotationTarget.* /** @@ -129,64 +132,6 @@ public interface LowLevelApiAccessor { ownerId: Long ): Sequence - /** - * 获取群公告列表 - * @param page 页码 - */ - @Suppress("DEPRECATION") - @Deprecated("Will be removed in the future. Use Announcement instead.", level = DeprecationLevel.WARNING) - @LowLevelApi - @MiraiExperimentalApi - public suspend fun getRawGroupAnnouncements( - bot: Bot, - groupId: Long, - page: Int = 1, - amount: Int = 10 - ): GroupAnnouncementList - - /** - * 发送群公告 - * - * @return 公告的fid - */ - @Suppress("DEPRECATION") - @Deprecated("Will be removed in the future. Use Announcement instead.", level = DeprecationLevel.WARNING) - @LowLevelApi - @MiraiExperimentalApi - public suspend fun sendGroupAnnouncement( - bot: Bot, - groupId: Long, - announcement: GroupAnnouncement - ): String - - /** - * 删除群公告 - * @param fid [GroupAnnouncement.fid] - */ - @Suppress("DEPRECATION") - @Deprecated("Will be removed in the future. Use Announcement instead.", level = DeprecationLevel.WARNING) - @LowLevelApi - @MiraiExperimentalApi - public suspend fun deleteGroupAnnouncement( - bot: Bot, - groupId: Long, - fid: String - ): Boolean - - /** - * 获取一条群公告 - * @param fid [GroupAnnouncement.fid] - */ - @Suppress("DEPRECATION") - @Deprecated("Will be removed in the future. Use Announcement instead.", level = DeprecationLevel.WARNING) - @LowLevelApi - @MiraiExperimentalApi - public suspend fun getGroupAnnouncement( - bot: Bot, - groupId: Long, - fid: String - ): GroupAnnouncement? - /** * 获取群活跃信息 diff --git a/mirai-core-api/src/commonMain/kotlin/contact/announcement/AnnouncementImage.kt b/mirai-core-api/src/commonMain/kotlin/contact/announcement/AnnouncementImage.kt index 710e25f1b..96560988f 100644 --- a/mirai-core-api/src/commonMain/kotlin/contact/announcement/AnnouncementImage.kt +++ b/mirai-core-api/src/commonMain/kotlin/contact/announcement/AnnouncementImage.kt @@ -21,10 +21,12 @@ import net.mamoe.mirai.utils.MiraiInternalApi @SerialName(AnnouncementImage.SERIAL_NAME) @Serializable public class AnnouncementImage @MiraiInternalApi public constructor( - @SerialName("h") public val height: String, - @SerialName("w") public val width: String, - @SerialName("id") public val id: String + public val height: String, + public val width: String, + public val id: String ) { + // For stability, do not make it `data class`. + public companion object { public const val SERIAL_NAME: String = "AnnouncementImage" } diff --git a/mirai-core-api/src/commonMain/kotlin/contact/announcement/AnnouncementParameters.kt b/mirai-core-api/src/commonMain/kotlin/contact/announcement/AnnouncementParameters.kt index 8b9d62552..c85460c39 100644 --- a/mirai-core-api/src/commonMain/kotlin/contact/announcement/AnnouncementParameters.kt +++ b/mirai-core-api/src/commonMain/kotlin/contact/announcement/AnnouncementParameters.kt @@ -11,8 +11,6 @@ package net.mamoe.mirai.contact.announcement import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient -import net.mamoe.mirai.utils.MiraiExperimentalApi /** * 群公告的附加参数. @@ -25,12 +23,10 @@ import net.mamoe.mirai.utils.MiraiExperimentalApi @Serializable public class AnnouncementParameters internal constructor( /** - * 群公告的图片,目前仅支持发送图片,不支持获得图片 - * @suppress 此 API 不稳定, 可能在任意时间改动 + * 群公告的图片,目前仅支持发送图片,不支持获得图片. + * @see AnnouncementImage */ - @Transient // do not serialize unstable properties - @MiraiExperimentalApi - public val image: ByteArray? = null, + public val image: AnnouncementImage? = null, /** * 是否发送给新成员 diff --git a/mirai-core-api/src/commonMain/kotlin/contact/announcement/AnnouncementParametersBuilder.kt b/mirai-core-api/src/commonMain/kotlin/contact/announcement/AnnouncementParametersBuilder.kt index 1c529bfc3..815dae43b 100644 --- a/mirai-core-api/src/commonMain/kotlin/contact/announcement/AnnouncementParametersBuilder.kt +++ b/mirai-core-api/src/commonMain/kotlin/contact/announcement/AnnouncementParametersBuilder.kt @@ -11,7 +11,6 @@ package net.mamoe.mirai.contact.announcement -import net.mamoe.mirai.utils.MiraiExperimentalApi import kotlin.contracts.InvocationKind import kotlin.contracts.contract @@ -52,10 +51,8 @@ public class AnnouncementParametersBuilder @JvmOverloads constructor( ) { /** * @see AnnouncementParameters.image - * @suppress 此 API 不稳定, 可能在任意时间改动 */ - @MiraiExperimentalApi - public var image: ByteArray? = prototype.image + public var image: AnnouncementImage? = prototype.image /** * @see AnnouncementParameters.sendToNewMember @@ -83,10 +80,9 @@ public class AnnouncementParametersBuilder @JvmOverloads constructor( public var needConfirm: Boolean = prototype.needConfirm /** - * @suppress 此 API 不稳定, 可能在任意时间改动 + * @see AnnouncementParameters.image */ - @MiraiExperimentalApi - public fun image(image: ByteArray?): AnnouncementParametersBuilder { + public fun image(image: AnnouncementImage): AnnouncementParametersBuilder { this.image = image return this } diff --git a/mirai-core-api/src/commonMain/kotlin/contact/announcement/Announcements.kt b/mirai-core-api/src/commonMain/kotlin/contact/announcement/Announcements.kt index 468458b41..8c093ad9e 100644 --- a/mirai-core-api/src/commonMain/kotlin/contact/announcement/Announcements.kt +++ b/mirai-core-api/src/commonMain/kotlin/contact/announcement/Announcements.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.toList import net.mamoe.kjbb.JvmBlockingBridge import net.mamoe.mirai.contact.PermissionDeniedException import net.mamoe.mirai.utils.ExternalResource +import net.mamoe.mirai.utils.NotStableForInheritance import java.util.stream.Stream @@ -25,6 +26,7 @@ import java.util.stream.Stream * * @since 2.7 */ +@NotStableForInheritance public interface Announcements { /** * 创建一个能获取该群内所有群公告列表的 [Flow]. 在 [Flow] 被使用时才会分页下载 [OnlineAnnouncement]. diff --git a/mirai-core-api/src/commonMain/kotlin/contact/announcement/OnlineAnnouncement.kt b/mirai-core-api/src/commonMain/kotlin/contact/announcement/OnlineAnnouncement.kt index d62cf0397..2c5c741ce 100644 --- a/mirai-core-api/src/commonMain/kotlin/contact/announcement/OnlineAnnouncement.kt +++ b/mirai-core-api/src/commonMain/kotlin/contact/announcement/OnlineAnnouncement.kt @@ -17,6 +17,7 @@ import net.mamoe.mirai.Bot import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.NormalMember import net.mamoe.mirai.contact.PermissionDeniedException +import net.mamoe.mirai.utils.NotStableForInheritance import java.time.Instant @@ -27,6 +28,7 @@ import java.time.Instant * * @since 2.7 */ +@NotStableForInheritance public interface OnlineAnnouncement : Announcement { /** * 公告所属群 diff --git a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt index 6d39eac7d..4bbc95e8a 100644 --- a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt +++ b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt @@ -18,7 +18,10 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.currentCoroutineContext import kotlinx.io.core.discardExact import kotlinx.io.core.readBytes -import kotlinx.serialization.json.* +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long import net.mamoe.mirai.* import net.mamoe.mirai.contact.* import net.mamoe.mirai.data.* @@ -557,139 +560,11 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor { check(response is PbMessageSvc.PbMsgWithDraw.Response.Success) { "Failed to recall message #${source.ids.contentToString()}: $response" } } - @Suppress("DEPRECATION", "OverridingDeprecatedMember") - @LowLevelApi - @MiraiExperimentalApi - override suspend fun getRawGroupAnnouncements( - bot: Bot, - groupId: Long, - page: Int, - amount: Int - ): GroupAnnouncementList = bot.asQQAndroidBot().run { - val rep = bot.asQQAndroidBot().network.run { - Mirai.Http.post { - url("https://web.qun.qq.com/cgi-bin/announce/list_announce") - body = MultiPartFormDataContent(formData { - append("qid", groupId) - append("bkn", bot.bkn) - append("ft", 23) //好像是一个用来识别应用的参数 - append("s", if (page == 1) 0 else -(page * amount + 1)) // 第一页这里的参数应该是-1 - append("n", amount) - append("ni", if (page == 1) 1 else 0) - append("format", "json") - }) - headers { - append( - "cookie", - "uin=o${id}; skey=${client.wLoginSigInfo.sKey.data.encodeToString()};" - ) - } - } - } -// bot.network.logger.error(rep) - return json.decodeFromString(GroupAnnouncementList.serializer(), rep) - } - private val json = Json { - ignoreUnknownKeys = true isLenient = true + ignoreUnknownKeys = true } - @Suppress("DEPRECATION", "OverridingDeprecatedMember") - @LowLevelApi - @MiraiExperimentalApi - override suspend fun sendGroupAnnouncement(bot: Bot, groupId: Long, announcement: GroupAnnouncement): String = - bot.asQQAndroidBot().run { - val rep = Mirai.Http.post { - url("https://web.qun.qq.com/cgi-bin/announce/add_qun_notice") - body = MultiPartFormDataContent(formData { - append("qid", groupId) - append("bkn", bkn) - append("text", announcement.msg.text) - append("pinned", announcement.pinned) - append( - "settings", - json.encodeToString( - GroupAnnouncementSettings.serializer(), - announcement.settings ?: GroupAnnouncementSettings() - ) - ) - append("format", "json") - }) - headers { - append( - "cookie", - "uin=o${id};" + - " skey=${client.wLoginSigInfo.sKey.data.encodeToString()};" + - " p_uin=o${id};" + - " p_skey=${client.wLoginSigInfo.psKeyMap["qun.qq.com"]?.data?.encodeToString()}; " - ) - } - } - val jsonObj = json.parseToJsonElement(rep) - return jsonObj.jsonObject["new_fid"]?.jsonPrimitive?.content - ?: throw throw IllegalStateException("Send Announcement fail group:$groupId msg:${jsonObj.jsonObject["em"]} content:${announcement.msg.text}") - } - - @Suppress("DEPRECATION", "OverridingDeprecatedMember") - @LowLevelApi - @MiraiExperimentalApi - override suspend fun deleteGroupAnnouncement(bot: Bot, groupId: Long, fid: String): Boolean = - bot.asQQAndroidBot().run { - val data = Mirai.Http.post { - url("https://web.qun.qq.com/cgi-bin/announce/del_feed") - body = MultiPartFormDataContent(formData { - append("qid", groupId) - append("bkn", bkn) - append("fid", fid) - append("format", "json") - }) - headers { - append( - "cookie", - "uin=o${id};" + - " skey=${client.wLoginSigInfo.sKey.data.encodeToString()};" + - " p_uin=o${id};" + - " p_skey=${client.wLoginSigInfo.psKeyMap["qun.qq.com"]?.data?.encodeToString()}; " - ) - } - } - val jsonObj = json.parseToJsonElement(data) - if (jsonObj.jsonObject["ec"]?.jsonPrimitive?.int ?: 1 != 0) { - throw throw IllegalStateException("delete Announcement fail group:$groupId msg:${jsonObj.jsonObject["em"]} fid:$fid") - } - - return jsonObj.jsonObject["ec"]?.jsonPrimitive?.int == 0 - } - - @Suppress("DEPRECATION", "OverridingDeprecatedMember") - @LowLevelApi - @MiraiExperimentalApi - override suspend fun getGroupAnnouncement(bot: Bot, groupId: Long, fid: String): GroupAnnouncement? = - bot.asQQAndroidBot().run { - val rep = network.run { - Mirai.Http.post { - url("https://web.qun.qq.com/cgi-bin/announce/get_feed") - body = MultiPartFormDataContent(formData { - append("qid", groupId) - append("bkn", bkn) - append("fid", fid) - append("format", "json") - }) - headers { - append( - "cookie", - "uin=o${id}; skey=${client.wLoginSigInfo.sKey.data.encodeToString()}; p_uin=o${id};" - ) - } - } - } - -// bot.network.logger.error(rep) - return json.decodeFromString(GroupAnnouncement.serializer(), rep) - - } - @LowLevelApi @MiraiExperimentalApi override suspend fun getRawGroupActiveData(bot: Bot, groupId: Long, page: Int): GroupActiveData = diff --git a/mirai-core/src/commonMain/kotlin/contact/AnnouncementsImpl.kt b/mirai-core/src/commonMain/kotlin/contact/AnnouncementsImpl.kt deleted file mode 100644 index b4fbeb960..000000000 --- a/mirai-core/src/commonMain/kotlin/contact/AnnouncementsImpl.kt +++ /dev/null @@ -1,257 +0,0 @@ -/* - * Copyright 2019-2021 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/dev/LICENSE - */ - -@file:Suppress("DEPRECATION") - -package net.mamoe.mirai.internal.contact - -import io.ktor.client.request.* -import io.ktor.client.request.forms.* -import io.ktor.http.* -import io.ktor.utils.io.core.* -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.int -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import net.mamoe.mirai.Bot -import net.mamoe.mirai.Mirai -import net.mamoe.mirai.contact.Group -import net.mamoe.mirai.contact.MemberPermission -import net.mamoe.mirai.contact.announcement.* -import net.mamoe.mirai.contact.checkBotPermission -import net.mamoe.mirai.data.GroupAnnouncement -import net.mamoe.mirai.data.GroupAnnouncementList -import net.mamoe.mirai.data.GroupAnnouncementMsg -import net.mamoe.mirai.data.GroupAnnouncementSettings -import net.mamoe.mirai.internal.asQQAndroidBot -import net.mamoe.mirai.utils.* -import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource -import java.util.stream.Stream -import kotlin.io.use - -internal class AnnouncementsImpl( - private val group: GroupImpl, -) : Announcements { - inline val bot get() = group.bot - - override suspend fun asFlow(): Flow { - return flow { - var i = 1 - while (true) { - val result = Mirai.getRawGroupAnnouncements(bot, group.id, i++) - checkResult(result, i) - - if (result.inst.isNullOrEmpty() && result.feeds.isNullOrEmpty()) break - - result.inst?.let { emitAll(it.asFlow()) } - result.feeds?.let { emitAll(it.asFlow()) } - } - }.map { it.toAnnouncement(group) } - } - - override suspend fun asStream(): Stream { - return stream { - var i = 1 - while (true) { - val result = runBlocking { Mirai.getRawGroupAnnouncements(bot, group.id, i++) } - checkResult(result, i) - - if (result.inst.isNullOrEmpty() && result.feeds.isNullOrEmpty()) break - - result.inst?.let { yieldAll(it) } - result.feeds?.let { yieldAll(it) } - } - }.map { it.toAnnouncement(group) } - } - - private fun checkResult(result: GroupAnnouncementList, i: Int) { - if (result.ec != 0) { - bot.logger.warning { "Failed to get announcements for group ${group.id}, at page $i. result=$result" } - } - } - - override suspend fun delete(fid: String): Boolean { - group.checkBotPermission(MemberPermission.ADMINISTRATOR) { "Only administrator have permission to delete group announcement" } - return Mirai.deleteGroupAnnouncement(bot, group.id, fid) - } - - override suspend fun get(fid: String): OnlineAnnouncement? { - return Mirai.getGroupAnnouncement(bot, group.id, fid)?.toAnnouncement(group) - } - - override suspend fun publish(announcement: Announcement): OnlineAnnouncement = announcement.run { - val bot = group.bot - group.checkBotPermission(MemberPermission.ADMINISTRATOR) { "Only administrator have permission to send group announcement" } - val image = parameters.image - val fid = image?.toExternalResource()?.use { - val imageUp = AnnouncementProtocol.uploadGroupAnnouncementImage(bot, group.id, it) - AnnouncementProtocol.sendGroupAnnouncementWithImage(bot, group.id, imageUp, toGroupAnnouncement(bot.id)) - } ?: Mirai.sendGroupAnnouncement(bot, group.id, toGroupAnnouncement(bot.id)) - - return OnlineAnnouncementImpl( - group = group, - senderId = bot.id, - sender = group.botAsMember, - title = title, - body = body, - parameters = parameters, - fid = fid, - isAllRead = false, - readMemberNumber = 0, - publishTime = currentTimeSeconds() - ) - } - - override suspend fun uploadImage(resource: ExternalResource): AnnouncementImage { - return AnnouncementProtocol.uploadGroupAnnouncementImage(bot, group.id, resource) - } -} - -@Suppress("DEPRECATION") -internal object AnnouncementProtocol { - private val json = Json { - ignoreUnknownKeys = true - isLenient = true - } - - suspend fun uploadGroupAnnouncementImage( - bot: Bot, - groupId: Long, - resource: ExternalResource - ): AnnouncementImage = bot.asQQAndroidBot().run { - //https://youtrack.jetbrains.com/issue/KTOR-455 - val rep = Mirai.Http.post { - url("https://web.qun.qq.com/cgi-bin/announce/upload_img") - body = MultiPartFormDataContent(formData { - append("\"bkn\"", bkn) - append("\"source\"", "troopNotice") - append("m", "0") - append( - "\"pic_up\"", - headers = Headers.build { - append(HttpHeaders.ContentType, ContentType.Image.PNG) - append(HttpHeaders.ContentDisposition, "filename=\"temp_uploadFile.png\"") - } - ) { - writeFully(resource.inputStream().withUse { readBytes() }) - } - }) - headers { - append( - "cookie", - " p_uin=o${id};" + - " p_skey=${client.wLoginSigInfo.psKeyMap["qun.qq.com"]?.data?.encodeToString() ?: error("cookie parse p_skey error")}; " - ) - } - } - val jsonObj = json.parseToJsonElement(rep) - if (jsonObj.jsonObject["ec"]?.jsonPrimitive?.int != 0) { - throw IllegalStateException("Upload group announcement image fail group:$groupId msg:${jsonObj.jsonObject["em"]}") - } - val id = jsonObj.jsonObject["id"]?.jsonPrimitive?.content - ?: throw IllegalStateException("Upload group announcement image fail group:$groupId msg:${jsonObj.jsonObject["em"]}") - return json.decodeFromString(AnnouncementImage.serializer(), id) - } - - suspend fun sendGroupAnnouncementWithImage( - bot: Bot, - groupId: Long, - image: AnnouncementImage, - announcement: GroupAnnouncement - ): String = bot.asQQAndroidBot().run { - val rep = withContext(network.coroutineContext) { - Mirai.Http.post { - url("https://web.qun.qq.com/cgi-bin/announce/add_qun_notice") - body = MultiPartFormDataContent(formData { - append("qid", groupId) - append("bkn", bkn) - append("text", announcement.msg.text) - append("pinned", announcement.pinned) - append("pic", image.id) - append("imgWidth", image.width) - append("imgHeight", image.height) - append( - "settings", - json.encodeToString( - GroupAnnouncementSettings.serializer(), - announcement.settings ?: GroupAnnouncementSettings() - ) - ) - append("format", "json") - }) - headers { - append( - "cookie", - " p_uin=o${id};" + - " p_skey=${ - client.wLoginSigInfo.psKeyMap["qun.qq.com"]?.data?.encodeToString() ?: error( - "parse error" - ) - }; " - ) - } - - } - } - val jsonObj = json.parseToJsonElement(rep) - return jsonObj.jsonObject["new_fid"]?.jsonPrimitive?.content - ?: throw throw IllegalStateException("Send Announcement with image fail group:$groupId msg:${jsonObj.jsonObject["em"]} content:${announcement.msg.text}") - } - - -} - -@Suppress("DEPRECATION") -internal fun Announcement.toGroupAnnouncement(senderId: Long): GroupAnnouncement { - return GroupAnnouncement( - sender = senderId, - msg = GroupAnnouncementMsg( - title = title, - text = body - ), - type = if (parameters.sendToNewMember) 20 else 6, - settings = GroupAnnouncementSettings( - isShowEditCard = if (parameters.isShowEditCard) 1 else 0, - tipWindowType = if (parameters.isTip) 0 else 1, - confirmRequired = if (parameters.needConfirm) 1 else 0, - ), - pinned = if (parameters.isPinned) 1 else 0, - ) -} - -@Suppress("DEPRECATION") -private fun GroupAnnouncement.toAnnouncement(group: Group): OnlineAnnouncementImpl { - val fid = this.fid - val settings = this.settings - - check(fid != null) { "GroupAnnouncement don't have id" } - check(settings != null) { "GroupAnnouncement don't have setting" } - - return OnlineAnnouncementImpl( - group = group, - senderId = sender, - sender = group[sender], - title = msg.title ?: "", - body = msg.text, - parameters = buildAnnouncementParameters { - isPinned = pinned == 1 - sendToNewMember = type == 20 - isTip = settings.tipWindowType == 0 - needConfirm = settings.confirmRequired == 1 - isShowEditCard = settings.isShowEditCard == 1 - }, - fid = fid, - isAllRead = isAllConfirm != 0, - readMemberNumber = readNum, - publishTime = time - ) -} diff --git a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt index 5a5b18328..1500744bf 100644 --- a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt @@ -21,6 +21,7 @@ import net.mamoe.mirai.data.MemberInfo import net.mamoe.mirai.event.broadcast import net.mamoe.mirai.event.events.* import net.mamoe.mirai.internal.QQAndroidBot +import net.mamoe.mirai.internal.contact.announcement.AnnouncementsImpl import net.mamoe.mirai.internal.contact.info.MemberInfoImpl import net.mamoe.mirai.internal.message.OfflineGroupImage import net.mamoe.mirai.internal.network.context.BdhSession diff --git a/mirai-core/src/commonMain/kotlin/contact/announcement/AnnouncementsImpl.kt b/mirai-core/src/commonMain/kotlin/contact/announcement/AnnouncementsImpl.kt new file mode 100644 index 000000000..94b6654fa --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/contact/announcement/AnnouncementsImpl.kt @@ -0,0 +1,284 @@ +/* + * Copyright 2019-2021 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/dev/LICENSE + */ + +@file:Suppress("DEPRECATION") + +package net.mamoe.mirai.internal.contact.announcement + +import io.ktor.client.request.* +import io.ktor.client.request.forms.* +import io.ktor.http.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import net.mamoe.mirai.Bot +import net.mamoe.mirai.Mirai +import net.mamoe.mirai.contact.Group +import net.mamoe.mirai.contact.MemberPermission +import net.mamoe.mirai.contact.announcement.* +import net.mamoe.mirai.contact.checkBotPermission +import net.mamoe.mirai.internal.QQAndroidBot +import net.mamoe.mirai.internal.asQQAndroidBot +import net.mamoe.mirai.internal.contact.GroupImpl +import net.mamoe.mirai.internal.contact.OnlineAnnouncementImpl +import net.mamoe.mirai.internal.contact.announcement.AnnouncementProtocol.deleteGroupAnnouncement +import net.mamoe.mirai.internal.contact.announcement.AnnouncementProtocol.getGroupAnnouncement +import net.mamoe.mirai.internal.contact.announcement.AnnouncementProtocol.toAnnouncement +import net.mamoe.mirai.internal.contact.announcement.AnnouncementProtocol.toGroupAnnouncement +import net.mamoe.mirai.internal.network.psKey +import net.mamoe.mirai.internal.network.sKey +import net.mamoe.mirai.internal.utils.io.writeResource +import net.mamoe.mirai.utils.* +import net.mamoe.mirai.utils.Either.Companion.rightOrNull +import java.util.stream.Stream + +internal class AnnouncementsImpl( + private val group: GroupImpl, +) : Announcements { + inline val bot get() = group.bot + + override suspend fun asFlow(): Flow { + return flow { + var i = 1 + while (true) { + val result = AnnouncementProtocol.getRawGroupAnnouncements(bot, group.id, i++).rightOrNull ?: break + + if (result.inst.isNullOrEmpty() && result.feeds.isNullOrEmpty()) break + + result.inst?.let { emitAll(it.asFlow()) } + result.feeds?.let { emitAll(it.asFlow()) } + } + }.map { it.toAnnouncement(group) } + } + + override suspend fun asStream(): Stream { + return stream { + var i = 1 + while (true) { + val result = runBlocking { + AnnouncementProtocol.getRawGroupAnnouncements(bot, group.id, i++) + }.rightOrNull ?: break + + if (result.inst.isNullOrEmpty() && result.feeds.isNullOrEmpty()) break + + result.inst?.let { yieldAll(it) } + result.feeds?.let { yieldAll(it) } + } + }.map { it.toAnnouncement(group) } + } + + override suspend fun delete(fid: String): Boolean { + group.checkBotPermission(MemberPermission.ADMINISTRATOR) { "Only administrator have permission to delete group announcement" } + return bot.deleteGroupAnnouncement(group.id, fid) + } + + override suspend fun get(fid: String): OnlineAnnouncement { + return bot.getGroupAnnouncement(group.id, fid).toAnnouncement(group) + } + + override suspend fun publish(announcement: Announcement): OnlineAnnouncement = announcement.run { + val bot = group.bot + group.checkBotPermission(MemberPermission.ADMINISTRATOR) { "Only administrator have permission to send group announcement" } + val image = parameters.image + val fid = AnnouncementProtocol.sendGroupAnnouncement(bot, group.id, toGroupAnnouncement(bot.id), image) + + return OnlineAnnouncementImpl( + group = group, + senderId = bot.id, + sender = group.botAsMember, + title = title, + body = body, + parameters = parameters, + fid = fid, + isAllRead = false, + readMemberNumber = 0, + publishTime = currentTimeSeconds() + ) + } + + override suspend fun uploadImage(resource: ExternalResource): AnnouncementImage { + return AnnouncementProtocol.uploadGroupAnnouncementImage(bot, resource) + } +} + +internal object AnnouncementProtocol { + @Serializable + data class UploadImageResp( + @SerialName("ec") override val errorCode: Int = 0, + @SerialName("em") override val errorMessage: String? = null, + @SerialName("id") val id: String, + ) : CheckableResponseA(), JsonStruct + + suspend fun uploadGroupAnnouncementImage( + bot: Bot, + resource: ExternalResource + ): AnnouncementImage = bot.asQQAndroidBot().run { + val resp = Mirai.Http.post { + url("https://web.qun.qq.com/cgi-bin/announce/upload_img") + body = MultiPartFormDataContent(formData { + append("\"bkn\"", bkn) + append("\"source\"", "troopNotice") + append("m", "0") + append( + "\"pic_up\"", + headers = Headers.build { + append(HttpHeaders.ContentType, ContentType.Image.PNG) + append(HttpHeaders.ContentDisposition, "filename=\"temp_uploadFile.png\"") + } + ) { + writeResource(resource) + } + }) + cookie("p_uin", "o${bot.id}") + cookie("p_skey", psKey("qun.qq.com")) + }.loadSafelyAs(UploadImageResp.serializer()).checked() + return resp.id.loadSafelyAs(GroupAnnouncementImage.serializer()).checked().toPublic() + } + + @Serializable + data class SendGroupAnnouncementResp( + @SerialName("ec") override val errorCode: Int = 0, + @SerialName("em") override val errorMessage: String? = null, + @SerialName("new_fid") val fid: String, + ) : CheckableResponseA(), JsonStruct + + suspend fun sendGroupAnnouncement( + bot: Bot, + groupId: Long, + announcement: GroupAnnouncement, + image: AnnouncementImage?, + ): String = bot.asQQAndroidBot().run { + return Mirai.Http.post { + url("https://web.qun.qq.com/cgi-bin/announce/add_qun_notice") + body = MultiPartFormDataContent(formData { + append("qid", groupId) + this.append("bkn", bkn) + append("text", announcement.msg.text) + append("pinned", announcement.pinned) + image?.let { + append("pic", image.id) + append("imgWidth", image.width) + append("imgHeight", image.height) + } + append( + "settings", + announcement.settings.toJsonString(GroupAnnouncementSettings.serializer()), + ) + append("format", "json") + }) + cookie("uin", "o${bot.id}") + cookie("p_uin", "o${bot.id}") + cookie("skey", sKey) + cookie("p_skey", psKey("qun.qq.com")) + }.loadSafelyAs(SendGroupAnnouncementResp.serializer()).checked().fid + } + + suspend fun getRawGroupAnnouncements( + bot: Bot, + groupId: Long, + page: Int, + amount: Int = 10 + ): Either = bot.asQQAndroidBot().run { + return bot.asQQAndroidBot().network.run { + Mirai.Http.post { + url("https://web.qun.qq.com/cgi-bin/announce/list_announce") + body = MultiPartFormDataContent(formData { + append("qid", groupId) + append("bkn", bot.bkn) + append("ft", 23) //好像是一个用来识别应用的参数 + append("s", if (page == 1) 0 else -(page * amount + 1)) // 第一页这里的参数应该是-1 + append("n", amount) + append("ni", if (page == 1) 1 else 0) + append("format", "json") + }) + cookie("uin", "o${bot.id}") + cookie("skey", sKey) + } + }.loadSafelyAs(GroupAnnouncementList.serializer()) + } + + @Serializable + data class DeleteResp( + @SerialName("ec") override val errorCode: Int = 0, + @SerialName("em") override val errorMessage: String? = null, + ) : CheckableResponseA(), JsonStruct + + suspend fun QQAndroidBot.deleteGroupAnnouncement(groupId: Long, fid: String): Boolean { + Mirai.Http.post { + url("https://web.qun.qq.com/cgi-bin/announce/del_feed") + body = feedBody(groupId, fid) + cookie("uin", "o$id") + cookie("p_uin", "o$id") + cookie("skey", sKey) + cookie("p_skey", psKey("qun.qq.com")) + }.loadSafelyAs(DeleteResp.serializer()).checked() + return true + } + + suspend fun QQAndroidBot.getGroupAnnouncement(groupId: Long, fid: String): GroupAnnouncement { + return Mirai.Http.post { + url("https://web.qun.qq.com/cgi-bin/announce/get_feed") + body = feedBody(groupId, fid) + cookie("uin", "o$id") + cookie("p_uin", "o$id") + cookie("skey", sKey) + }.loadAs(GroupAnnouncement.serializer()) + } + + private fun QQAndroidBot.feedBody( + groupId: Long, + fid: String + ) = MultiPartFormDataContent(formData { + append("qid", groupId) + append("bkn", bkn) + append("fid", fid) + append("format", "json") + }) + + fun Announcement.toGroupAnnouncement(senderId: Long): GroupAnnouncement { + return GroupAnnouncement( + sender = senderId, + msg = GroupAnnouncementMsg( + title = title, + text = body + ), + type = if (parameters.sendToNewMember) 20 else 6, + settings = GroupAnnouncementSettings( + isShowEditCard = if (parameters.isShowEditCard) 1 else 0, + tipWindowType = if (parameters.isTip) 0 else 1, + confirmRequired = if (parameters.needConfirm) 1 else 0, + ), + pinned = if (parameters.isPinned) 1 else 0, + ) + } + + fun GroupAnnouncement.toAnnouncement(group: Group): OnlineAnnouncementImpl { + val fid = this.fid ?: "" + + return OnlineAnnouncementImpl( + group = group, + senderId = sender, + sender = group[sender], + title = msg.title ?: "", + body = msg.text, + parameters = buildAnnouncementParameters { + isPinned = pinned == 1 + sendToNewMember = type == 20 + isTip = settings.tipWindowType == 0 + needConfirm = settings.confirmRequired == 1 + isShowEditCard = settings.isShowEditCard == 1 + }, + fid = fid, + isAllRead = isAllConfirm != 0, + readMemberNumber = readNum, + publishTime = time + ) + } +} diff --git a/mirai-core-api/src/commonMain/kotlin/data/GroupAnnouncement.kt b/mirai-core/src/commonMain/kotlin/contact/announcement/GroupAnnouncement.kt similarity index 55% rename from mirai-core-api/src/commonMain/kotlin/data/GroupAnnouncement.kt rename to mirai-core/src/commonMain/kotlin/contact/announcement/GroupAnnouncement.kt index c2dbec363..bb4437649 100644 --- a/mirai-core-api/src/commonMain/kotlin/data/GroupAnnouncement.kt +++ b/mirai-core/src/commonMain/kotlin/contact/announcement/GroupAnnouncement.kt @@ -9,27 +9,22 @@ @file:Suppress("DEPRECATION") -package net.mamoe.mirai.data +package net.mamoe.mirai.internal.contact.announcement import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import net.mamoe.mirai.contact.announcement.Announcement -import net.mamoe.mirai.utils.MiraiExperimentalApi +import net.mamoe.mirai.contact.announcement.AnnouncementImage +import net.mamoe.mirai.utils.CheckableResponseA +import net.mamoe.mirai.utils.JsonStruct +import net.mamoe.mirai.utils.MiraiInternalApi -/** - * 群公告的协议数据结构. 仅同于内部操作, 用户请使用 [Announcement]. - * - * @suppress 此 API 非常不稳定, 将在未来版本删除 - */ -@Deprecated("Will be removed in the future. Use Announcement instead.", level = DeprecationLevel.WARNING) -@MiraiExperimentalApi @Serializable -public data class GroupAnnouncementList( - val ec: Int, //状态码 0 是正常的 - @SerialName("em") val msg: String, //信息 +internal data class GroupAnnouncementList( + @SerialName("ec") override val errorCode: Int = 0, + @SerialName("em") override val errorMessage: String? = null, val feeds: List? = null, //群公告列表 val inst: List? = null //置顶列表? 应该是发送给新成员的 -) { +) : CheckableResponseA(), JsonStruct { /* // notes from original implementor, luo123, on 2020/3/13 @@ -41,52 +36,45 @@ public data class GroupAnnouncementList( */ } -/** - * 群公告的协议数据结构. 仅同于内部操作, 用户请使用 [AnnouncementImpl]. - * - * @suppress 此 API 非常不稳定, 将在未来版本删除 - */ -@Deprecated("Will be removed in the future. Use Announcement instead.", level = DeprecationLevel.WARNING) -@MiraiExperimentalApi @Serializable -public data class GroupAnnouncement( +internal data class GroupAnnouncement( @SerialName("u") val sender: Long = 0, //发送者id val msg: GroupAnnouncementMsg, val type: Int = 0, //20 为inst , 6 为feeds - val settings: GroupAnnouncementSettings? = null, + val settings: GroupAnnouncementSettings = GroupAnnouncementSettings.DEFAULT, @SerialName("pubt") val time: Long = 0, //发布时间 @SerialName("read_num") val readNum: Int = 0, //如果需要确认,则为确认收到的人数,反之则为已经阅读的人数 @SerialName("is_read") val isRead: Int = 0, //好像没用 @SerialName("is_all_confirm") val isAllConfirm: Int = 0, //为0 则未全部收到 val pinned: Int = 0, //1为置顶, 0为默认 val fid: String? = null, //公告的id -) +) : JsonStruct -/** - * 群公告的协议数据结构. 仅同于内部操作, 用户请使用 [AnnouncementImpl]. - * - * @suppress 此 API 非常不稳定, 将在未来版本删除 - */ -@Deprecated("Will be removed in the future. Use Announcement instead.", level = DeprecationLevel.WARNING) -@MiraiExperimentalApi @Serializable -public data class GroupAnnouncementMsg( +internal class GroupAnnouncementImage @MiraiInternalApi constructor( + @SerialName("h") val height: String, + @SerialName("w") val width: String, + @SerialName("id") val id: String +) : JsonStruct { + fun toPublic(): AnnouncementImage = AnnouncementImage(height, width, id) +} + +@Serializable +internal data class GroupAnnouncementMsg( val text: String, val text_face: String? = null, val title: String? = null ) -/** - * 群公告的协议数据结构. 仅同于内部操作, 用户请使用 [AnnouncementImpl]. - * - * @suppress 此 API 非常不稳定, 将在未来版本删除 - */ -@Deprecated("Will be removed in the future. Use Announcement instead.", level = DeprecationLevel.WARNING) -@MiraiExperimentalApi + @Serializable -public data class GroupAnnouncementSettings( +internal data class GroupAnnouncementSettings( @SerialName("is_show_edit_card") val isShowEditCard: Int = 0, //引导群成员修改该昵称 1 引导 @SerialName("remind_ts") val remindTs: Int = 0, @SerialName("tip_window_type") val tipWindowType: Int = 0, //是否用弹窗展示 1 不使用 @SerialName("confirm_required") val confirmRequired: Int = 0 // 是否需要确认收到 1 需要 -) +) : JsonStruct { + companion object { + val DEFAULT = GroupAnnouncementSettings() + } +} diff --git a/mirai-core/src/commonMain/kotlin/network/keys.kt b/mirai-core/src/commonMain/kotlin/network/keys.kt index 6b2d32837..8ca5a7808 100644 --- a/mirai-core/src/commonMain/kotlin/network/keys.kt +++ b/mirai-core/src/commonMain/kotlin/network/keys.kt @@ -14,6 +14,8 @@ import kotlinx.io.core.ByteReadPacket import kotlinx.io.core.writeFully import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import net.mamoe.mirai.internal.AbstractBot +import net.mamoe.mirai.internal.network.components.BotClientHolder import net.mamoe.mirai.utils.* @@ -137,6 +139,10 @@ internal data class WLoginSigInfo( override fun toString(): String { return "WLoginSigInfo(uin=$uin, encryptA1=${encryptA1?.toUHexString()}, noPicSig=${noPicSig?.toUHexString()}, simpleInfo=$simpleInfo, appPri=$appPri, a2ExpiryTime=$a2ExpiryTime, loginBitmap=$loginBitmap, tgt=${tgt.toUHexString()}, a2CreationTime=$a2CreationTime, tgtKey=${tgtKey.toUHexString()}, userStSig=$userStSig, userStKey=${userStKey.toUHexString()}, userStWebSig=$userStWebSig, userA5=$userA5, userA8=$userA8, lsKey=$lsKey, sKey=$sKey, userSig64=$userSig64, openId=${openId.toUHexString()}, openKey=$openKey, vKey=$vKey, accessToken=$accessToken, d2=$d2, d2Key=${d2Key.toUHexString()}, sid=$sid, aqSig=$aqSig, psKey=$psKeyMap, superKey=${superKey.toUHexString()}, payToken=${payToken.toUHexString()}, pf=${pf.toUHexString()}, pfKey=${pfKey.toUHexString()}, da2=${da2.toUHexString()}, wtSessionTicket=$wtSessionTicket, wtSessionTicketKey=${wtSessionTicketKey.toUHexString()}, deviceToken=${deviceToken.toUHexString()})" } + + fun getPsKey(name: String): String { + return psKeyMap[name]?.data?.encodeToString() ?: error("Cannot find PsKey $name") + } } internal typealias PSKeyMap = MutableMap @@ -173,6 +179,11 @@ internal open class KeyWithExpiry( } } +internal val KeyWithExpiry.str get() = data.encodeToString() +internal val AbstractBot.sKey get() = client.wLoginSigInfo.sKey.str +internal fun AbstractBot.psKey(name: String) = client.wLoginSigInfo.getPsKey(name) +internal val AbstractBot.client get() = components[BotClientHolder].client + @Serializable internal open class KeyWithCreationTime( open val data: ByteArray, diff --git a/mirai-core/src/commonMain/kotlin/utils/io/output.kt b/mirai-core/src/commonMain/kotlin/utils/io/output.kt index dee848cb3..1ab7bc0c2 100644 --- a/mirai-core/src/commonMain/kotlin/utils/io/output.kt +++ b/mirai-core/src/commonMain/kotlin/utils/io/output.kt @@ -13,9 +13,13 @@ package net.mamoe.mirai.internal.utils.io +import io.ktor.utils.io.streams.* import kotlinx.io.core.* +import kotlinx.io.streams.outputStream import net.mamoe.mirai.internal.utils.coerceAtMostOrFail import net.mamoe.mirai.internal.utils.crypto.TEA +import net.mamoe.mirai.utils.ExternalResource +import net.mamoe.mirai.utils.withUse internal fun BytePacketBuilder.writeShortLVByteArrayLimitedLength(array: ByteArray, maxLength: Int) { if (array.size <= maxLength) { @@ -29,6 +33,17 @@ internal fun BytePacketBuilder.writeShortLVByteArrayLimitedLength(array: ByteArr } } +internal fun BytePacketBuilder.writeResource( + resource: ExternalResource, + close: Boolean = false, +): Long = resource.inputStream().withUse { copyTo(outputStream()) }.also { + if (close) resource.close() +} + +internal fun io.ktor.utils.io.core.BytePacketBuilder.writeResource( + resource: ExternalResource, +): Long = resource.inputStream().withUse { copyTo(outputStream()) } + internal inline fun BytePacketBuilder.writeShortLVByteArray(byteArray: ByteArray): Int { this.writeShort(byteArray.size.toShort()) this.writeFully(byteArray)