diff --git a/README.md b/README.md
index cc467d3ea..766a1c09c 100644
--- a/README.md
+++ b/README.md
@@ -99,7 +99,6 @@ Demos: [mirai-demos](https://github.com/mamoe/mirai-demos)
[](https://github.com/HoshinoTented)
[](https://github.com/Cyenoch)
-
## 鸣谢
特别感谢 [JetBrains](https://www.jetbrains.com/?from=mirai) 为开源项目提供免费的 [IntelliJ IDEA](https://www.jetbrains.com/idea/?from=mirai) 等 IDE 的授权
diff --git a/mirai-core-qqandroid/build.gradle.kts b/mirai-core-qqandroid/build.gradle.kts
index 8179cd718..9793d514e 100644
--- a/mirai-core-qqandroid/build.gradle.kts
+++ b/mirai-core-qqandroid/build.gradle.kts
@@ -110,6 +110,7 @@ kotlin {
runtimeOnly(files("build/classes/kotlin/jvm/main")) // classpath is not properly set by IDE
api(kotlinx("serialization-runtime", serializationVersion))
//api(kotlinx("serialization-protobuf", serializationVersion))
+
}
}
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt
index d67a04053..cfe36b068 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt
@@ -9,9 +9,19 @@
package net.mamoe.mirai.qqandroid
+import io.ktor.client.HttpClient
+import io.ktor.client.request.*
+import io.ktor.client.request.forms.MultiPartFormDataContent
+import io.ktor.client.request.forms.formData
+import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.io.core.Closeable
+import kotlinx.serialization.MissingFieldException
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonConfiguration
+import kotlinx.serialization.json.int
+import kotlinx.serialization.json.long
import net.mamoe.mirai.LowLevelAPI
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.data.*
@@ -35,6 +45,7 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.LongConn
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.MessageSvc
import net.mamoe.mirai.qqandroid.utils.toIpV4AddressString
import net.mamoe.mirai.utils.*
+import net.mamoe.mirai.utils.io.encodeToString
import net.mamoe.mirai.utils.io.toUHexString
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
@@ -719,4 +730,6 @@ internal class GroupImpl(
if (this::class != other::class) return false
return this.id == other.id && this.bot == other.bot
}
+
+
}
\ No newline at end of file
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.kt
index b87883f51..fabc9c806 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.kt
@@ -9,18 +9,25 @@
package net.mamoe.mirai.qqandroid
+import io.ktor.client.HttpClient
+import io.ktor.client.request.forms.MultiPartFormDataContent
+import io.ktor.client.request.forms.formData
import io.ktor.client.request.get
+import io.ktor.client.request.headers
+import io.ktor.client.request.post
+import io.ktor.client.request.url
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.async
import kotlinx.coroutines.io.ByteReadChannel
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonConfiguration
+import kotlinx.serialization.json.int
import net.mamoe.mirai.BotAccount
import net.mamoe.mirai.BotImpl
import net.mamoe.mirai.LowLevelAPI
import net.mamoe.mirai.contact.*
-import net.mamoe.mirai.data.AddFriendResult
-import net.mamoe.mirai.data.FriendInfo
-import net.mamoe.mirai.data.GroupInfo
-import net.mamoe.mirai.data.MemberInfo
+import net.mamoe.mirai.data.*
import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.MessageRecallEvent
import net.mamoe.mirai.message.data.*
@@ -33,6 +40,7 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.PbMessageSvc
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement
import net.mamoe.mirai.qqandroid.network.protocol.packet.list.FriendList
import net.mamoe.mirai.utils.*
+import net.mamoe.mirai.utils.io.encodeToString
import kotlin.collections.asSequence
import kotlin.coroutines.CoroutineContext
@@ -59,6 +67,10 @@ internal abstract class QQAndroidBotBase constructor(
internal var firstLoginSucceed: Boolean = false
override val uin: Long get() = client.uin
+ companion object {
+ val json = Json(JsonConfiguration(ignoreUnknownKeys = true, encodeDefaults = true))
+ }
+
@Deprecated(
"use friends instead",
level = DeprecationLevel.ERROR,
@@ -198,7 +210,7 @@ internal abstract class QQAndroidBotBase constructor(
}
}
- @OptIn(LowLevelAPI::class)
+ @LowLevelAPI
override suspend fun _lowLevelRecallGroupMessage(groupId: Long, messageId: Long) {
network.run {
val response: PbMessageSvc.PbMsgWithDraw.Response =
@@ -209,6 +221,129 @@ internal abstract class QQAndroidBotBase constructor(
}
}
+
+ @LowLevelAPI
+ @MiraiExperimentalAPI
+ override suspend fun _lowLevelGetAnnouncements(groupId: Long, page: Int, amount: Int): GroupAnnouncementList {
+ val data = network.async {
+ HttpClient().post {
+ url("https://web.qun.qq.com/cgi-bin/announce/list_announce")
+ body = MultiPartFormDataContent(formData {
+ append("qid", groupId)
+ append("bkn", 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${selfQQ.id}; skey=${client.wLoginSigInfo.sKey.data.encodeToString()}; p_uin=o${selfQQ.id};"
+ )
+ }
+ }
+ }
+
+ val rep = data.await()
+// bot.network.logger.error(rep)
+ return json.parse(GroupAnnouncementList.serializer(), rep)
+ }
+
+ @LowLevelAPI
+ @MiraiExperimentalAPI
+ override suspend fun _lowLevelSendAnnouncement(groupId: Long, announcement: GroupAnnouncement): String {
+ val rep = network.async {
+ HttpClient().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.stringify(
+ GroupAnnouncementSettings.serializer(),
+ announcement.settings ?: GroupAnnouncementSettings()
+ )
+ )
+ append("format", "json")
+ })
+ headers {
+ append(
+ "cookie",
+ "uin=o${selfQQ.id};" +
+ " skey=${client.wLoginSigInfo.sKey.data.encodeToString()};" +
+ " p_uin=o${selfQQ.id};" +
+ " p_skey=${client.wLoginSigInfo.psKeyMap["qun.qq.com"]?.data?.encodeToString()}; "
+ )
+ }
+ }
+ }
+ val jsonObj = json.parseJson(rep.await())
+ return jsonObj.jsonObject["new_fid"]?.primitive?.content
+ ?: throw throw IllegalStateException("Send Announcement fail group:$groupId msg:${jsonObj.jsonObject["em"]} content:${announcement.msg.text}")
+ }
+
+ @LowLevelAPI
+ @MiraiExperimentalAPI
+ override suspend fun _lowLevelDeleteAnnouncement(groupId: Long, fid: String) {
+ val rep = network.async {
+ HttpClient().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${selfQQ.id};" +
+ " skey=${client.wLoginSigInfo.sKey.data.encodeToString()};" +
+ " p_uin=o${selfQQ.id};" +
+ " p_skey=${client.wLoginSigInfo.psKeyMap["qun.qq.com"]?.data?.encodeToString()}; "
+ )
+ }
+ }
+ }
+ val data = rep.await()
+ val jsonObj = json.parseJson(data)
+ if (jsonObj.jsonObject["ec"]?.int ?: 1 != 0) {
+ throw throw IllegalStateException("delete Announcement fail group:$groupId msg:${jsonObj.jsonObject["em"]} fid:$fid")
+ }
+ }
+
+ @OptIn(LowLevelAPI::class)
+ @MiraiExperimentalAPI
+ override suspend fun _lowLevelGetAnnouncement(groupId: Long, fid: String): GroupAnnouncement {
+ val data = network.async {
+ HttpClient().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${selfQQ.id}; skey=${client.wLoginSigInfo.sKey.data.encodeToString()}; p_uin=o${selfQQ.id};"
+ )
+ }
+ }
+ }
+
+ val rep = data.await()
+// bot.network.logger.error(rep)
+ return json.parse(GroupAnnouncement.serializer(), rep)
+
+ }
+
override suspend fun queryImageUrl(image: Image): String = when (image) {
is OnlineFriendImageImpl -> image.originUrl
is OnlineGroupImageImpl -> image.originUrl
@@ -224,6 +359,19 @@ internal abstract class QQAndroidBotBase constructor(
override suspend fun openChannel(image: Image): ByteReadChannel {
return MiraiPlatformUtils.Http.get(queryImageUrl(image)).content.toKotlinByteReadChannel()
}
+
+ /**
+ * 获取 获取群公告 所需的bkn参数
+ * */
+ val bkn: Int
+ get() {
+ val str = client.wLoginSigInfo.sKey.data.encodeToString()
+ var magic = 5381
+ for (i in str) {
+ magic += magic.shl(5) + i.toInt()
+ }
+ return Int.MAX_VALUE.and(magic)
+ }
}
@Suppress("DEPRECATION")
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/WtLogin.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/WtLogin.kt
index 5b7365394..ef67b961e 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/WtLogin.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/WtLogin.kt
@@ -602,6 +602,7 @@ internal class WtLogin {
userA5 = UserA5(tlvMap119.getOrEmpty(0x10b), creationTime),
userA8 = UserA8(tlvMap119.getOrEmpty(0x102), creationTime, expireTime)
)
+ //bot.network.logger.error(client.wLoginSigInfo.psKeyMap["qun.qq.com"]?.data?.encodeToString())
}
}
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Group.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Group.kt
index 15c7e97d7..c58cf228b 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Group.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Group.kt
@@ -12,7 +12,12 @@
package net.mamoe.mirai.contact
import kotlinx.coroutines.CoroutineScope
+import kotlinx.serialization.SerialInfo
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
import net.mamoe.mirai.Bot
+import net.mamoe.mirai.data.GroupAnnouncement
+import net.mamoe.mirai.data.GroupAnnouncementList
import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.event.events.MessageSendEvent.FriendMessageSendEvent
@@ -148,6 +153,8 @@ expect abstract class Group() : Contact, CoroutineScope {
*/
abstract operator fun contains(id: Long): Boolean
+
+
/**
* 让机器人退出这个群. 机器人必须为非群主才能退出. 否则将会失败
*/
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/data/GroupAnnouncement.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/data/GroupAnnouncement.kt
new file mode 100644
index 000000000..7f39ca05d
--- /dev/null
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/data/GroupAnnouncement.kt
@@ -0,0 +1,46 @@
+package net.mamoe.mirai.data
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ * 群公告数据类
+ * getGroupAnnouncementList时,如果page=1,那么你可以在inst里拿到一些置顶公告
+ *
+ * 发公告时只需要填写text,其他参数可为默认值
+ *
+ */
+@Serializable
+data class GroupAnnouncementList(
+ val ec: Int, //状态码 0 是正常的
+ @SerialName("em") val msg: String, //信息
+ val feeds: List? = null, //群公告列表
+ val inst: List? = null //置顶列表?
+)
+
+@Serializable
+data class GroupAnnouncement(
+ @SerialName("u") val sender: Long = 0,
+ val msg: GroupAnnouncementMsg,
+ val settings: GroupAnnouncementSettings? = null,
+ @SerialName("pubt") val time: Long = 0,
+ @SerialName("read_num") val readNum: Int = 0,
+ @SerialName("is_read") val isRead: Int = 0,
+ val pinned: Int = 0,
+ val fid:String? = null //公告的id
+)
+
+@Serializable
+data class GroupAnnouncementMsg(
+ val text: String,
+ val text_face: String? = null,
+ val title: String? = null
+)
+
+@Serializable
+data class GroupAnnouncementSettings(
+ @SerialName("is_show_edit_card") val isShowEditCard: Int = 0,
+ @SerialName("remind_ts") val remindTs: Int = 0,
+ @SerialName("tip_window_type") val tipWindowType: Int = 0,
+ @SerialName("confirm_required") val confirmRequired: Int = 0
+)
\ No newline at end of file
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/lowLevelApi.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/lowLevelApi.kt
index 6ea5e0857..fa3c6a95f 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/lowLevelApi.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/lowLevelApi.kt
@@ -12,9 +12,7 @@ package net.mamoe.mirai
import kotlinx.coroutines.Job
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.QQ
-import net.mamoe.mirai.data.FriendInfo
-import net.mamoe.mirai.data.GroupInfo
-import net.mamoe.mirai.data.MemberInfo
+import net.mamoe.mirai.data.*
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.MiraiInternalAPI
@@ -94,6 +92,41 @@ interface LowLevelBotAPIAccessor {
*/
@LowLevelAPI
suspend fun _lowLevelRecallGroupMessage(groupId: Long, messageId: Long)
+
+ /**
+ * 获取群公告列表
+ * @param page 页码
+ * */
+ @LowLevelAPI
+ @MiraiExperimentalAPI
+ suspend fun _lowLevelGetAnnouncements(groupId: Long, page: Int = 1, amount: Int = 10): GroupAnnouncementList
+
+ /**
+ * 发送群公告
+ *
+ * @return 公告的fid
+ * */
+ @LowLevelAPI
+ @MiraiExperimentalAPI
+ suspend fun _lowLevelSendAnnouncement(groupId: Long, announcement: GroupAnnouncement): String
+
+
+ /**
+ * 删除群公告
+ * @param fid [GroupAnnouncement.fid]
+ * */
+ @LowLevelAPI
+ @MiraiExperimentalAPI
+ suspend fun _lowLevelDeleteAnnouncement(groupId: Long, fid: String)
+
+ /**
+ * 获取一条群公告
+ * @param fid [GroupAnnouncement.fid]
+ * */
+ @LowLevelAPI
+ @MiraiExperimentalAPI
+ suspend fun _lowLevelGetAnnouncement(groupId: Long, fid: String): GroupAnnouncement
+
}
/**
diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/contact/Group.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/contact/Group.kt
index c1a6d1c21..fb9ff146f 100644
--- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/contact/Group.kt
+++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/contact/Group.kt
@@ -11,6 +11,8 @@ package net.mamoe.mirai.contact
import kotlinx.coroutines.CoroutineScope
import net.mamoe.mirai.Bot
+import net.mamoe.mirai.data.GroupAnnouncement
+import net.mamoe.mirai.data.GroupAnnouncementList
import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.event.events.MessageSendEvent.FriendMessageSendEvent
@@ -146,6 +148,7 @@ actual abstract class Group : Contact(), CoroutineScope {
*/
actual abstract fun getOrNull(id: Long): Member?
+
/**
* 检查此 id 的群成员是否存在
*/