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://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) [<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 的授权 特别感谢 [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 runtimeOnly(files("build/classes/kotlin/jvm/main")) // classpath is not properly set by IDE
api(kotlinx("serialization-runtime", serializationVersion)) api(kotlinx("serialization-runtime", serializationVersion))
//api(kotlinx("serialization-protobuf", serializationVersion)) //api(kotlinx("serialization-protobuf", serializationVersion))
} }
} }

View File

@ -9,9 +9,19 @@
package net.mamoe.mirai.qqandroid 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.launch
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.io.core.Closeable 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.LowLevelAPI
import net.mamoe.mirai.contact.* import net.mamoe.mirai.contact.*
import net.mamoe.mirai.data.* 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.network.protocol.packet.chat.receive.MessageSvc
import net.mamoe.mirai.qqandroid.utils.toIpV4AddressString import net.mamoe.mirai.qqandroid.utils.toIpV4AddressString
import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.io.encodeToString
import net.mamoe.mirai.utils.io.toUHexString import net.mamoe.mirai.utils.io.toUHexString
import kotlin.contracts.ExperimentalContracts import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract import kotlin.contracts.contract
@ -719,4 +730,6 @@ internal class GroupImpl(
if (this::class != other::class) return false if (this::class != other::class) return false
return this.id == other.id && this.bot == other.bot return this.id == other.id && this.bot == other.bot
} }
} }

View File

@ -9,18 +9,25 @@
package net.mamoe.mirai.qqandroid 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.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 io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.async
import kotlinx.coroutines.io.ByteReadChannel 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.BotAccount
import net.mamoe.mirai.BotImpl import net.mamoe.mirai.BotImpl
import net.mamoe.mirai.LowLevelAPI import net.mamoe.mirai.LowLevelAPI
import net.mamoe.mirai.contact.* import net.mamoe.mirai.contact.*
import net.mamoe.mirai.data.AddFriendResult import net.mamoe.mirai.data.*
import net.mamoe.mirai.data.FriendInfo
import net.mamoe.mirai.data.GroupInfo
import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.event.broadcast import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.MessageRecallEvent import net.mamoe.mirai.event.events.MessageRecallEvent
import net.mamoe.mirai.message.data.* 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.chat.TroopManagement
import net.mamoe.mirai.qqandroid.network.protocol.packet.list.FriendList import net.mamoe.mirai.qqandroid.network.protocol.packet.list.FriendList
import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.io.encodeToString
import kotlin.collections.asSequence import kotlin.collections.asSequence
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -59,6 +67,10 @@ internal abstract class QQAndroidBotBase constructor(
internal var firstLoginSucceed: Boolean = false internal var firstLoginSucceed: Boolean = false
override val uin: Long get() = client.uin override val uin: Long get() = client.uin
companion object {
val json = Json(JsonConfiguration(ignoreUnknownKeys = true, encodeDefaults = true))
}
@Deprecated( @Deprecated(
"use friends instead", "use friends instead",
level = DeprecationLevel.ERROR, level = DeprecationLevel.ERROR,
@ -198,7 +210,7 @@ internal abstract class QQAndroidBotBase constructor(
} }
} }
@OptIn(LowLevelAPI::class) @LowLevelAPI
override suspend fun _lowLevelRecallGroupMessage(groupId: Long, messageId: Long) { override suspend fun _lowLevelRecallGroupMessage(groupId: Long, messageId: Long) {
network.run { network.run {
val response: PbMessageSvc.PbMsgWithDraw.Response = 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) { override suspend fun queryImageUrl(image: Image): String = when (image) {
is OnlineFriendImageImpl -> image.originUrl is OnlineFriendImageImpl -> image.originUrl
is OnlineGroupImageImpl -> image.originUrl is OnlineGroupImageImpl -> image.originUrl
@ -224,6 +359,19 @@ internal abstract class QQAndroidBotBase constructor(
override suspend fun openChannel(image: Image): ByteReadChannel { override suspend fun openChannel(image: Image): ByteReadChannel {
return MiraiPlatformUtils.Http.get<HttpResponse>(queryImageUrl(image)).content.toKotlinByteReadChannel() 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") @Suppress("DEPRECATION")

View File

@ -602,6 +602,7 @@ internal class WtLogin {
userA5 = UserA5(tlvMap119.getOrEmpty(0x10b), creationTime), userA5 = UserA5(tlvMap119.getOrEmpty(0x10b), creationTime),
userA8 = UserA8(tlvMap119.getOrEmpty(0x102), creationTime, expireTime) 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 package net.mamoe.mirai.contact
import kotlinx.coroutines.CoroutineScope 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.Bot
import net.mamoe.mirai.data.GroupAnnouncement
import net.mamoe.mirai.data.GroupAnnouncementList
import net.mamoe.mirai.data.MemberInfo import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.event.events.* import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.event.events.MessageSendEvent.FriendMessageSendEvent 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 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 kotlinx.coroutines.Job
import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.QQ import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.data.FriendInfo import net.mamoe.mirai.data.*
import net.mamoe.mirai.data.GroupInfo
import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.message.data.MessageSource import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.utils.MiraiExperimentalAPI import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.MiraiInternalAPI
@ -94,6 +92,41 @@ interface LowLevelBotAPIAccessor {
*/ */
@LowLevelAPI @LowLevelAPI
suspend fun _lowLevelRecallGroupMessage(groupId: Long, messageId: Long) 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 kotlinx.coroutines.CoroutineScope
import net.mamoe.mirai.Bot 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.data.MemberInfo
import net.mamoe.mirai.event.events.* import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.event.events.MessageSendEvent.FriendMessageSendEvent 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? actual abstract fun getOrNull(id: Long): Member?
/** /**
* 检查此 id 的群成员是否存在 * 检查此 id 的群成员是否存在
*/ */