Remove announcements' low-level API and bundle them into AnnouncementProtocol, improve code style and maintainability

This commit is contained in:
Him188 2021-07-05 14:28:10 +08:00
parent 251bf3d9df
commit 4a2b510a70
13 changed files with 364 additions and 504 deletions

View File

@ -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<MemberInfo>
/**
* 获取群公告列表
* @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?
/**
* 获取群活跃信息

View File

@ -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"
}

View File

@ -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,
/**
* 是否发送给新成员

View File

@ -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
}

View File

@ -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].

View File

@ -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 {
/**
* 公告所属群

View File

@ -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<String> {
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<String> {
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<String> {
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<String> {
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 =

View File

@ -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<OnlineAnnouncement> {
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<OnlineAnnouncement> {
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<String> {
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<String> {
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
)
}

View File

@ -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

View File

@ -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<OnlineAnnouncement> {
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<OnlineAnnouncement> {
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<String> {
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<String> {
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<DeserializationFailure, GroupAnnouncementList> = bot.asQQAndroidBot().run {
return bot.asQQAndroidBot().network.run {
Mirai.Http.post<String> {
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<String> {
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<String> {
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
)
}
}

View File

@ -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<GroupAnnouncement>? = null, //群公告列表
val inst: List<GroupAnnouncement>? = 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()
}
}

View File

@ -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<String, KeyWithExpiry>
@ -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,

View File

@ -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)