Merge pull request #138 from mzdluo123/master

群公告的lowLevelApi
This commit is contained in:
Him188 2020-03-13 22:49:30 -05:00 committed by GitHub
commit 7faa9112a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 260 additions and 9 deletions

View File

@ -99,7 +99,6 @@ Demos: [mirai-demos](https://github.com/mamoe/mirai-demos)
[<img width="60px" height="60px" src="https://avatars0.githubusercontent.com/u/25280943?s=60&v=4" />](https://github.com/HoshinoTented)
[<img width="60px" height="60px" src="https://avatars3.githubusercontent.com/u/40517459?s=60&v=4" />](https://github.com/Cyenoch)
## 鸣谢
特别感谢 [JetBrains](https://www.jetbrains.com/?from=mirai) 为开源项目提供免费的 [IntelliJ IDEA](https://www.jetbrains.com/idea/?from=mirai) 等 IDE 的授权

View File

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

View File

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

View File

@ -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<String> {
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<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.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<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${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<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${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<HttpResponse>(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")

View File

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

View File

@ -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
/**
* 让机器人退出这个群. 机器人必须为非群主才能退出. 否则将会失败
*/

View File

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

View File

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

View File

@ -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 的群成员是否存在
*/