| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991 |
- /*
- * 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
- */
- package net.mamoe.mirai.internal
- import io.ktor.client.*
- import io.ktor.client.engine.okhttp.*
- import io.ktor.client.features.*
- import io.ktor.client.request.*
- import io.ktor.client.request.forms.*
- import io.ktor.http.*
- import io.ktor.utils.io.core.*
- import kotlinx.coroutines.SupervisorJob
- import kotlinx.coroutines.currentCoroutineContext
- import kotlinx.io.core.discardExact
- import kotlinx.io.core.readBytes
- 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.*
- import net.mamoe.mirai.event.Event
- import net.mamoe.mirai.event.broadcast
- import net.mamoe.mirai.event.events.*
- import net.mamoe.mirai.internal.contact.*
- import net.mamoe.mirai.internal.contact.info.FriendInfoImpl
- import net.mamoe.mirai.internal.contact.info.FriendInfoImpl.Companion.impl
- import net.mamoe.mirai.internal.contact.info.MemberInfoImpl
- import net.mamoe.mirai.internal.contact.info.StrangerInfoImpl.Companion.impl
- import net.mamoe.mirai.internal.message.*
- import net.mamoe.mirai.internal.message.DeepMessageRefiner.refineDeep
- import net.mamoe.mirai.internal.network.components.EventDispatcher
- import net.mamoe.mirai.internal.network.components.EventDispatcherScopeFlag
- import net.mamoe.mirai.internal.network.highway.*
- import net.mamoe.mirai.internal.network.protocol.data.jce.SvcDevLoginInfo
- import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
- import net.mamoe.mirai.internal.network.protocol.data.proto.LongMsg
- import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm
- import net.mamoe.mirai.internal.network.protocol.data.proto.MsgTransmit
- import net.mamoe.mirai.internal.network.protocol.packet.chat.*
- import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore
- import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList
- import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc
- import net.mamoe.mirai.internal.network.protocol.packet.sendAndExpect
- import net.mamoe.mirai.internal.network.protocol.packet.summarycard.SummaryCard
- import net.mamoe.mirai.internal.network.psKey
- import net.mamoe.mirai.internal.network.sKey
- import net.mamoe.mirai.internal.utils.MiraiProtocolInternal
- import net.mamoe.mirai.internal.utils.crypto.TEA
- import net.mamoe.mirai.internal.utils.io.serialization.loadAs
- import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
- import net.mamoe.mirai.message.MessageSerializers
- import net.mamoe.mirai.message.action.Nudge
- import net.mamoe.mirai.message.data.*
- import net.mamoe.mirai.message.data.Image.Key.IMAGE_ID_REGEX
- import net.mamoe.mirai.message.data.Image.Key.IMAGE_RESOURCE_ID_REGEX_1
- import net.mamoe.mirai.message.data.Image.Key.IMAGE_RESOURCE_ID_REGEX_2
- import net.mamoe.mirai.utils.*
- import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
- import kotlin.math.absoluteValue
- import kotlin.random.Random
- internal fun getMiraiImpl() = Mirai as MiraiImpl
- @OptIn(LowLevelApi::class)
- // not object for ServiceLoader.
- internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
- companion object INSTANCE : MiraiImpl() {
- @Suppress("ObjectPropertyName", "unused", "DEPRECATION_ERROR")
- private val _init = Mirai.let {
- MessageSerializers.registerSerializer(OfflineGroupImage::class, OfflineGroupImage.serializer())
- MessageSerializers.registerSerializer(OfflineFriendImage::class, OfflineFriendImage.serializer())
- MessageSerializers.registerSerializer(OnlineFriendImageImpl::class, OnlineFriendImageImpl.serializer())
- MessageSerializers.registerSerializer(OnlineGroupImageImpl::class, OnlineGroupImageImpl.serializer())
- MessageSerializers.registerSerializer(MarketFaceImpl::class, MarketFaceImpl.serializer())
- MessageSerializers.registerSerializer(FileMessageImpl::class, FileMessageImpl.serializer())
- // MessageSource
- MessageSerializers.registerSerializer(
- OnlineMessageSourceFromGroupImpl::class,
- OnlineMessageSourceFromGroupImpl.serializer()
- )
- MessageSerializers.registerSerializer(
- OnlineMessageSourceFromFriendImpl::class,
- OnlineMessageSourceFromFriendImpl.serializer()
- )
- MessageSerializers.registerSerializer(
- OnlineMessageSourceFromTempImpl::class,
- OnlineMessageSourceFromTempImpl.serializer()
- )
- MessageSerializers.registerSerializer(
- OnlineMessageSourceFromStrangerImpl::class,
- OnlineMessageSourceFromStrangerImpl.serializer()
- )
- MessageSerializers.registerSerializer(
- OnlineMessageSourceToGroupImpl::class,
- OnlineMessageSourceToGroupImpl.serializer()
- )
- MessageSerializers.registerSerializer(
- OnlineMessageSourceToFriendImpl::class,
- OnlineMessageSourceToFriendImpl.serializer()
- )
- MessageSerializers.registerSerializer(
- OnlineMessageSourceToTempImpl::class,
- OnlineMessageSourceToTempImpl.serializer()
- )
- MessageSerializers.registerSerializer(
- OnlineMessageSourceToStrangerImpl::class,
- OnlineMessageSourceToStrangerImpl.serializer()
- )
- MessageSerializers.registerSerializer(
- OfflineMessageSourceImplData::class,
- OfflineMessageSourceImplData.serializer()
- )
- MessageSerializers.registerSerializer(
- OfflineMessageSourceImplData::class,
- OfflineMessageSourceImplData.serializer()
- )
- MessageSerializers.registerSerializer(
- UnsupportedMessageImpl::class,
- UnsupportedMessageImpl.serializer()
- )
- MessageSerializers.registerSerializer(
- OnlineAudioImpl::class,
- OnlineAudioImpl.serializer()
- )
- MessageSerializers.registerSerializer(
- OfflineAudioImpl::class,
- OfflineAudioImpl.serializer()
- )
- }
- }
- @Suppress("DEPRECATION")
- override val BotFactory: BotFactory
- get() = BotFactoryImpl
- override var FileCacheStrategy: FileCacheStrategy = net.mamoe.mirai.utils.FileCacheStrategy.PlatformDefault
- override var Http: HttpClient = HttpClient(OkHttp) {
- install(HttpTimeout) {
- this.requestTimeoutMillis = 30_0000
- this.connectTimeoutMillis = 30_0000
- this.socketTimeoutMillis = 30_0000
- }
- }
- @OptIn(LowLevelApi::class)
- override suspend fun acceptNewFriendRequest(event: NewFriendRequestEvent) {
- @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
- check(event.responded.compareAndSet(false, true)) {
- "the request $this has already been responded"
- }
- check(!event.bot.friends.contains(event.fromId)) {
- "the request $event is outdated: You had already responded it on another device."
- }
- solveNewFriendRequestEvent(
- event.bot,
- eventId = event.eventId,
- fromId = event.fromId,
- fromNick = event.fromNick,
- accept = true,
- blackList = false
- )
- event.bot.getFriend(event.fromId)?.let { friend ->
- FriendAddEvent(friend).broadcast()
- }
- }
- override suspend fun refreshKeys(bot: Bot) {
- // TODO: 2021/4/14 MiraiImpl.refreshKeysNow
- }
- override suspend fun rejectNewFriendRequest(event: NewFriendRequestEvent, blackList: Boolean) {
- @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
- check(event.responded.compareAndSet(false, true)) {
- "the request $event has already been responded"
- }
- check(!event.bot.friends.contains(event.fromId)) {
- "the request $event is outdated: You had already responded it on another device."
- }
- solveNewFriendRequestEvent(
- event.bot,
- eventId = event.eventId,
- fromId = event.fromId,
- fromNick = event.fromNick,
- accept = false,
- blackList = blackList
- )
- }
- override suspend fun acceptMemberJoinRequest(event: MemberJoinRequestEvent) {
- @Suppress("DuplicatedCode")
- checkGroupPermission(event.bot, event.groupId) { event::class.simpleName ?: "<anonymous class>" }
- @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
- check(event.responded.compareAndSet(false, true)) {
- "the request $this has already been responded"
- }
- if (event.group?.contains(event.fromId) == true) return
- solveMemberJoinRequestEvent(
- bot = event.bot,
- eventId = event.eventId,
- fromId = event.fromId,
- fromNick = event.fromNick,
- groupId = event.groupId,
- accept = true,
- blackList = false
- )
- }
- @Suppress("DuplicatedCode")
- override suspend fun rejectMemberJoinRequest(event: MemberJoinRequestEvent, blackList: Boolean, message: String) {
- checkGroupPermission(event.bot, event.groupId) { event::class.simpleName ?: "<anonymous class>" }
- @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
- check(event.responded.compareAndSet(false, true)) {
- "the request $this has already been responded"
- }
- if (event.group?.contains(event.fromId) == true) return
- solveMemberJoinRequestEvent(
- bot = event.bot,
- eventId = event.eventId,
- fromId = event.fromId,
- fromNick = event.fromNick,
- groupId = event.groupId,
- accept = false,
- blackList = blackList,
- message = message
- )
- }
- private inline fun checkGroupPermission(eventBot: Bot, groupId: Long, eventName: () -> String) {
- val group = eventBot.getGroup(groupId)
- ?: kotlin.run {
- error(
- "A ${eventName()} is outdated. Group $groupId not found for bot ${eventBot.id}. " +
- "This is because bot isn't in the group anymore"
- )
- }
- group.checkBotPermission(MemberPermission.ADMINISTRATOR)
- }
- override suspend fun getOnlineOtherClientsList(bot: Bot, mayIncludeSelf: Boolean): List<OtherClientInfo> {
- bot.asQQAndroidBot()
- val response = bot.network.run {
- StatSvc.GetDevLoginInfo(bot.client).sendAndExpect()
- }
- fun SvcDevLoginInfo.toOtherClientInfo() = OtherClientInfo(
- iAppId.toInt(),
- Platform.getByTerminalId(iTerType?.toInt() ?: 0),
- deviceName.orEmpty(),
- deviceTypeInfo.orEmpty()
- )
- return response.deviceList.map { it.toOtherClientInfo() }.let { result ->
- if (mayIncludeSelf) result else result.filterNot {
- it.appId == MiraiProtocolInternal[bot.configuration.protocol].id.toInt()
- }
- }
- }
- override suspend fun ignoreMemberJoinRequest(event: MemberJoinRequestEvent, blackList: Boolean) {
- checkGroupPermission(event.bot, event.groupId) { event::class.simpleName ?: "<anonymous class>" }
- @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
- check(event.responded.compareAndSet(false, true)) {
- "the request $this has already been responded"
- }
- solveMemberJoinRequestEvent(
- bot = event.bot,
- eventId = event.eventId,
- fromId = event.fromId,
- fromNick = event.fromNick,
- groupId = event.groupId,
- accept = null,
- blackList = blackList
- )
- }
- override suspend fun acceptInvitedJoinGroupRequest(event: BotInvitedJoinGroupRequestEvent) =
- solveInvitedJoinGroupRequest(event, accept = true)
- override suspend fun ignoreInvitedJoinGroupRequest(event: BotInvitedJoinGroupRequestEvent) =
- solveInvitedJoinGroupRequest(event, accept = false)
- override suspend fun broadcastEvent(event: Event) {
- if (currentCoroutineContext()[EventDispatcherScopeFlag] != null) {
- // called by [EventDispatcher]
- return super.broadcastEvent(event)
- }
- if (event is BotEvent) {
- val bot = event.bot
- if (bot is QQAndroidBot) {
- bot.components[EventDispatcher].broadcast(event)
- }
- } else {
- super.broadcastEvent(event)
- }
- }
- private suspend fun solveInvitedJoinGroupRequest(event: BotInvitedJoinGroupRequestEvent, accept: Boolean) {
- @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
- check(event.responded.compareAndSet(false, true)) {
- "the request $this has already been responded"
- }
- check(!event.bot.groups.contains(event.groupId)) {
- "the request $this is outdated: Bot has been already in the group."
- }
- solveBotInvitedJoinGroupRequestEvent(
- bot = event.bot,
- eventId = event.eventId,
- invitorId = event.invitorId,
- groupId = event.groupId,
- accept = accept
- )
- }
- @LowLevelApi
- override fun newFriend(bot: Bot, friendInfo: FriendInfo): FriendImpl {
- return FriendImpl(
- bot.asQQAndroidBot(),
- bot.coroutineContext,
- friendInfo.impl(),
- )
- }
- @LowLevelApi
- override fun newStranger(bot: Bot, strangerInfo: StrangerInfo): StrangerImpl {
- return StrangerImpl(
- bot.asQQAndroidBot(),
- bot.coroutineContext,
- strangerInfo.impl(),
- )
- }
- @OptIn(LowLevelApi::class)
- override suspend fun getRawGroupList(bot: Bot): Sequence<Long> {
- bot.asQQAndroidBot()
- return bot.network.run {
- FriendList.GetTroopListSimplify(bot.client).sendAndExpect(retry = 2)
- }.groups.asSequence().map { it.groupUin.shl(32) and it.groupCode }
- }
- @OptIn(LowLevelApi::class)
- override suspend fun getRawGroupMemberList(
- bot: Bot,
- groupUin: Long,
- groupCode: Long,
- ownerId: Long
- ): Sequence<MemberInfo> =
- bot.asQQAndroidBot().network.run {
- var nextUin = 0L
- var sequence = sequenceOf<MemberInfoImpl>()
- while (true) {
- val data = FriendList.GetTroopMemberList(
- client = bot.client,
- targetGroupUin = groupUin,
- targetGroupCode = groupCode,
- nextUin = nextUin
- ).sendAndExpect(retry = 3)
- sequence += data.members.asSequence().map { troopMemberInfo ->
- MemberInfoImpl(bot.client, troopMemberInfo, ownerId)
- }
- nextUin = data.nextUin
- if (nextUin == 0L) {
- break
- }
- }
- return sequence
- }
- override suspend fun recallGroupMessageRaw(
- bot: Bot,
- groupCode: Long,
- messageIds: IntArray,
- messageInternalIds: IntArray,
- ): Boolean = bot.asQQAndroidBot().run {
- val response = network.run {
- PbMessageSvc.PbMsgWithDraw.createForGroupMessage(
- client,
- groupCode,
- messageIds,
- messageInternalIds
- ).sendAndExpect()
- }
- response is PbMessageSvc.PbMsgWithDraw.Response.Success
- }
- override suspend fun recallFriendMessageRaw(
- bot: Bot,
- targetId: Long,
- messageIds: IntArray,
- messageInternalIds: IntArray,
- time: Int,
- ): Boolean = bot.asQQAndroidBot().run {
- val response = network.run {
- PbMessageSvc.PbMsgWithDraw.createForFriendMessage(
- client,
- targetId,
- messageIds,
- messageInternalIds,
- time,
- ).sendAndExpect()
- }
- response is PbMessageSvc.PbMsgWithDraw.Response.Success
- }
- override suspend fun recallGroupTempMessageRaw(
- bot: Bot,
- groupUin: Long,
- targetId: Long,
- messageIds: IntArray,
- messageInternalIds: IntArray,
- time: Int
- ): Boolean = bot.asQQAndroidBot().run {
- val response = network.run {
- PbMessageSvc.PbMsgWithDraw.createForGroupTempMessage(
- client,
- groupUin,
- targetId,
- messageIds,
- messageInternalIds,
- time,
- ).sendAndExpect()
- }
- response is PbMessageSvc.PbMsgWithDraw.Response.Success
- }
- @Suppress("RemoveExplicitTypeArguments") // false positive
- override suspend fun recallMessage(bot: Bot, source: MessageSource) = bot.asQQAndroidBot().run {
- check(source is MessageSourceInternal)
- source.ensureSequenceIdAvailable()
- @Suppress("BooleanLiteralArgument", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") // false positive
- check(!source.isRecalledOrPlanned.get() && source.isRecalledOrPlanned.compareAndSet(false, true)) {
- "$source had already been recalled."
- }
- val response: PbMessageSvc.PbMsgWithDraw.Response = when (source) {
- is OnlineMessageSourceToGroupImpl,
- is OnlineMessageSourceFromGroupImpl
- -> {
- val group = when (source) {
- is OnlineMessageSourceToGroupImpl -> source.target
- is OnlineMessageSourceFromGroupImpl -> source.group
- else -> error("stub")
- }
- if (bot.id != source.fromId) {
- group.checkBotPermission(MemberPermission.ADMINISTRATOR)
- }
- network.run {
- PbMessageSvc.PbMsgWithDraw.createForGroupMessage(
- bot.asQQAndroidBot().client,
- group.id,
- source.sequenceIds,
- source.internalIds
- ).sendAndExpect()
- }
- }
- is OnlineMessageSourceFromFriendImpl,
- is OnlineMessageSourceToFriendImpl,
- is OnlineMessageSourceFromStrangerImpl,
- is OnlineMessageSourceToStrangerImpl,
- -> network.run {
- check(source.fromId == bot.id) {
- "can only recall a message sent by bot"
- }
- PbMessageSvc.PbMsgWithDraw.createForFriendMessage(
- bot.client,
- source.targetId,
- source.sequenceIds,
- source.internalIds,
- source.time
- ).sendAndExpect<PbMessageSvc.PbMsgWithDraw.Response>()
- }
- is OnlineMessageSourceFromTempImpl,
- is OnlineMessageSourceToTempImpl
- -> network.run {
- check(source.fromId == bot.id) {
- "can only recall a message sent by bot"
- }
- source as OnlineMessageSourceToTempImpl
- PbMessageSvc.PbMsgWithDraw.createForGroupTempMessage(
- bot.client,
- (source.target.group as GroupImpl).uin,
- source.targetId,
- source.sequenceIds,
- source.internalIds,
- source.time
- ).sendAndExpect<PbMessageSvc.PbMsgWithDraw.Response>()
- }
- is OfflineMessageSource -> network.run {
- when (source.kind) {
- MessageSourceKind.FRIEND, MessageSourceKind.STRANGER -> {
- check(source.fromId == bot.id) {
- "can only recall a message sent by bot"
- }
- PbMessageSvc.PbMsgWithDraw.createForFriendMessage(
- bot.client,
- source.targetId,
- source.sequenceIds,
- source.internalIds,
- source.time
- ).sendAndExpect<PbMessageSvc.PbMsgWithDraw.Response>()
- }
- MessageSourceKind.TEMP -> {
- check(source.fromId == bot.id) {
- "can only recall a message sent by bot"
- }
- PbMessageSvc.PbMsgWithDraw.createForGroupTempMessage(
- bot.client,
- source.targetId, // groupUin
- source.targetId, // memberUin
- source.sequenceIds,
- source.internalIds,
- source.time
- ).sendAndExpect<PbMessageSvc.PbMsgWithDraw.Response>()
- }
- MessageSourceKind.GROUP -> {
- PbMessageSvc.PbMsgWithDraw.createForGroupMessage(
- bot.client,
- source.targetId,
- source.sequenceIds,
- source.internalIds
- ).sendAndExpect<PbMessageSvc.PbMsgWithDraw.Response>()
- }
- }
- }
- else -> error("stub!")
- }
- // 1001: No message meets the requirements (实际上是没权限, 管理员在尝试撤回群主的消息)
- // 154: timeout
- // 3: <no message>
- check(response is PbMessageSvc.PbMsgWithDraw.Response.Success) { "Failed to recall message #${source.ids.contentToString()}: $response" }
- }
- private val json = Json {
- isLenient = true
- ignoreUnknownKeys = true
- }
- @LowLevelApi
- @MiraiExperimentalApi
- override suspend fun getRawGroupActiveData(bot: Bot, groupId: Long, page: Int): GroupActiveData =
- bot.asQQAndroidBot().run {
- val rep = network.run {
- Mirai.Http.get<String> {
- url("https://qqweb.qq.com/c/activedata/get_mygroup_data")
- parameter("bkn", bkn)
- parameter("gc", groupId)
- if (page != -1) {
- parameter("page", page)
- }
- headers {
- append(
- "cookie",
- "uin=o${bot.id}; skey=${bot.sKey}; p_uin=o${bot.id};"
- )
- }
- }
- }
- return json.decodeFromString(GroupActiveData.serializer(), rep)
- }
- @LowLevelApi
- @MiraiExperimentalApi
- override suspend fun getRawGroupHonorListData(
- bot: Bot,
- groupId: Long,
- type: GroupHonorType
- ): GroupHonorListData? = bot.asQQAndroidBot().run {
- val rep = network.run {
- Mirai.Http.get<String> {
- url("https://qun.qq.com/interactive/honorlist")
- parameter("gc", groupId)
- parameter("type", type.value)
- headers {
- append(
- "cookie",
- "uin=o${bot.id};" +
- " skey=${bot.sKey};" +
- " p_uin=o${bot.id};" +
- " p_skey=${bot.psKey("qun.qq.com")}; "
- )
- }
- }
- }
- val jsonText = Regex("""window.__INITIAL_STATE__=(.+?)</script>""").find(rep)?.groupValues?.get(1)
- return jsonText?.let { json.decodeFromString(GroupHonorListData.serializer(), it) }
- }
- internal open suspend fun uploadMessageHighway(
- bot: Bot,
- sendMessageHandler: SendMessageHandler<*>,
- message: Collection<ForwardMessage.INode>,
- isLong: Boolean,
- ): String = with(bot.asQQAndroidBot()) {
- message.forEach {
- it.messageChain.ensureSequenceIdAvailable()
- }
- val data = message.calculateValidationData(
- client = client,
- random = Random.nextInt().absoluteValue,
- sendMessageHandler,
- isLong,
- )
- val response = network.run {
- MultiMsg.ApplyUp.createForGroup(
- buType = if (isLong) 1 else 2,
- client = bot.client,
- messageData = data,
- dstUin = sendMessageHandler.targetUin
- ).sendAndExpect()
- }
- val resId: String
- when (response) {
- is MultiMsg.ApplyUp.Response.MessageTooLarge ->
- error(
- "Internal error: message is too large, but this should be handled before sending. "
- )
- is MultiMsg.ApplyUp.Response.RequireUpload -> {
- resId = response.proto.msgResid
- val body = LongMsg.ReqBody(
- subcmd = 1,
- platformType = 9,
- termType = 5,
- msgUpReq = listOf(
- LongMsg.MsgUpReq(
- msgType = 3, // group
- dstUin = sendMessageHandler.targetUin,
- msgId = 0,
- msgUkey = response.proto.msgUkey,
- needCache = 0,
- storeType = 2,
- msgContent = data.data
- )
- )
- ).toByteArray(LongMsg.ReqBody.serializer())
- body.toExternalResource().use { resource ->
- Highway.uploadResourceBdh(
- bot = bot,
- resource = resource,
- kind = when (isLong) {
- true -> ResourceKind.LONG_MESSAGE
- false -> ResourceKind.FORWARD_MESSAGE
- },
- commandId = 27,
- initialTicket = response.proto.msgSig
- )
- }
- }
- }
- return resId
- }
- @LowLevelApi
- @MiraiExperimentalApi
- override suspend fun solveNewFriendRequestEvent(
- bot: Bot,
- eventId: Long,
- fromId: Long,
- fromNick: String,
- accept: Boolean,
- blackList: Boolean
- ): Unit = bot.asQQAndroidBot().run {
- network.apply {
- NewContact.SystemMsgNewFriend.Action(
- bot.client,
- eventId = eventId,
- fromId = fromId,
- accept = accept,
- blackList = blackList
- ).sendWithoutExpect()
- if (!accept) return@apply
- @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
- bot.friends.delegate.add(newFriend(bot, FriendInfoImpl(fromId, fromNick, "")))
- }
- }
- @LowLevelApi
- @MiraiExperimentalApi
- override suspend fun solveBotInvitedJoinGroupRequestEvent(
- bot: Bot,
- eventId: Long,
- invitorId: Long,
- groupId: Long,
- accept: Boolean
- ) = bot.asQQAndroidBot().run {
- network.run {
- NewContact.SystemMsgNewGroup.Action(
- bot.client,
- eventId = eventId,
- fromId = invitorId,
- groupId = groupId,
- isInvited = true,
- accept = accept
- ).sendWithoutExpect()
- }
- }
- @LowLevelApi
- @MiraiExperimentalApi
- override suspend fun solveMemberJoinRequestEvent(
- bot: Bot,
- eventId: Long,
- fromId: Long,
- fromNick: String,
- groupId: Long,
- accept: Boolean?,
- blackList: Boolean,
- message: String
- ) = bot.asQQAndroidBot().run {
- network.run {
- NewContact.SystemMsgNewGroup.Action(
- bot.client,
- eventId = eventId,
- fromId = fromId,
- groupId = groupId,
- isInvited = false,
- accept = accept,
- blackList = blackList,
- message = message
- ).sendWithoutExpect()
- }
- // Add member in MsgOnlinePush.PbPushMsg
- }
- @OptIn(ExperimentalStdlibApi::class)
- @LowLevelApi
- override suspend fun getGroupVoiceDownloadUrl(
- bot: Bot,
- md5: ByteArray,
- groupId: Long,
- dstUin: Long
- ): String {
- bot.asQQAndroidBot().network.run {
- val response = PttStore.GroupPttDown(bot.client, groupId, dstUin, md5).sendAndExpect()
- return "http://${response.strDomain}${response.downPara.encodeToString()}"
- }
- }
- override suspend fun muteAnonymousMember(
- bot: Bot,
- anonymousId: String,
- anonymousNick: String,
- groupId: Long,
- seconds: Int
- ) {
- bot as QQAndroidBot
- val response = Mirai.Http.post<String> {
- url("https://qqweb.qq.com/c/anonymoustalk/blacklist")
- body = MultiPartFormDataContent(formData {
- append("anony_id", anonymousId)
- append("group_code", groupId)
- append("seconds", seconds)
- append("anony_nick", anonymousNick)
- append("bkn", bot.bkn)
- })
- headers {
- append(
- "cookie",
- "uin=o${bot.id}; skey=${bot.sKey};"
- )
- }
- }
- val jsonObj = Json.decodeFromString(JsonObject.serializer(), response)
- if ((jsonObj["retcode"] ?: jsonObj["cgicode"] ?: error("missing response code")).jsonPrimitive.long != 0L) {
- throw IllegalStateException(response)
- }
- }
- override fun createImage(imageId: String): Image {
- return when {
- imageId matches IMAGE_ID_REGEX -> OfflineGroupImage(imageId)
- imageId matches IMAGE_RESOURCE_ID_REGEX_1 -> OfflineFriendImage(imageId)
- imageId matches IMAGE_RESOURCE_ID_REGEX_2 -> OfflineFriendImage(imageId)
- else ->
- @Suppress("INVISIBLE_MEMBER")
- throw IllegalArgumentException("Illegal imageId: $imageId. $ILLEGAL_IMAGE_ID_EXCEPTION_MESSAGE")
- }
- }
- override fun createFileMessage(id: String, internalId: Int, name: String, size: Long): FileMessage {
- return FileMessageImpl(id, internalId, name, size)
- }
- override fun createUnsupportedMessage(struct: ByteArray): UnsupportedMessage =
- UnsupportedMessageImpl(struct.loadAs(ImMsgBody.Elem.serializer()))
- @Suppress("DEPRECATION", "OverridingDeprecatedMember")
- override suspend fun queryImageUrl(bot: Bot, image: Image): String = when (image) {
- is ConstOriginUrlAware -> image.originUrl
- is DeferredOriginUrlAware -> image.getUrl(bot)
- is SuspendDeferredOriginUrlAware -> image.getUrl(bot)
- else -> error("Internal error: unsupported image class: ${image::class.simpleName}")
- }
- override suspend fun queryProfile(bot: Bot, targetId: Long): UserProfile {
- bot.asQQAndroidBot().network.apply {
- return SummaryCard.ReqSummaryCard(bot.client, targetId)
- .sendAndExpect()
- }
- }
- override suspend fun sendNudge(bot: Bot, nudge: Nudge, receiver: Contact): Boolean {
- if (bot.configuration.protocol != BotConfiguration.MiraiProtocol.ANDROID_PHONE) {
- throw UnsupportedOperationException("nudge is supported only with protocol ANDROID_PHONE")
- }
- bot.asQQAndroidBot()
- bot.network.run {
- return if (receiver is Group) {
- receiver.checkIsGroupImpl()
- NudgePacket.troopInvoke(
- client = bot.client,
- messageReceiverGroupCode = receiver.id,
- nudgeTargetId = nudge.target.id,
- ).sendAndExpect<NudgePacket.Response>().success
- } else {
- NudgePacket.friendInvoke(
- client = bot.client,
- messageReceiverUin = receiver.id,
- nudgeTargetId = nudge.target.id,
- ).sendAndExpect<NudgePacket.Response>().success
- }
- }
- }
- override fun getUin(contactOrBot: ContactOrBot): Long {
- return when (contactOrBot) {
- is Group -> contactOrBot.uin
- is User -> contactOrBot.uin
- is Bot -> contactOrBot.uin
- else -> contactOrBot.id
- }
- }
- override fun constructMessageSource(
- botId: Long,
- kind: MessageSourceKind,
- fromId: Long,
- targetId: Long,
- ids: IntArray,
- time: Int,
- internalIds: IntArray,
- originalMessage: MessageChain
- ): OfflineMessageSource = OfflineMessageSourceImplData(
- kind, ids, botId, time, fromId, targetId, originalMessage, internalIds
- )
- override suspend fun downloadLongMessage(bot: Bot, resourceId: String): MessageChain {
- return downloadMultiMsgTransmit(bot, resourceId, ResourceKind.LONG_MESSAGE).msg
- .toMessageChainNoSource(bot, 0, MessageSourceKind.GROUP)
- .refineDeep(bot)
- }
- override suspend fun downloadForwardMessage(bot: Bot, resourceId: String): List<ForwardMessage.Node> {
- return downloadMultiMsgTransmit(bot, resourceId, ResourceKind.FORWARD_MESSAGE).toForwardMessageNodes(bot)
- }
- internal open suspend fun MsgTransmit.PbMultiMsgNew.toForwardMessageNodes(
- bot: Bot,
- context: RefineContext
- ): List<ForwardMessage.Node> {
- return msg.map { it.toNode(bot, context) }
- }
- internal open suspend fun MsgTransmit.PbMultiMsgTransmit.toForwardMessageNodes(bot: Bot): List<ForwardMessage.Node> {
- val pbs = this.pbItemList.associate {
- it.fileName to it.buffer.loadAs(MsgTransmit.PbMultiMsgNew.serializer())
- }
- val main = pbs["MultiMsg"] ?: return this.msg.map { it.toNode(bot, EmptyRefineContext) }
- val context = SimpleRefineContext(mutableMapOf())
- context[ForwardMessageInternal.MsgTransmits] = pbs
- return main.toForwardMessageNodes(bot, context)
- }
- protected open suspend fun MsgComm.Msg.toNode(bot: Bot, refineContext: RefineContext): ForwardMessage.Node {
- val msg = this
- return ForwardMessage.Node(
- senderId = msg.msgHead.fromUin,
- time = msg.msgHead.msgTime,
- senderName = msg.msgHead.groupInfo?.groupCard
- ?: msg.msgHead.fromNick.takeIf { it.isNotEmpty() }
- ?: msg.msgHead.fromUin.toString(),
- messageChain = listOf(msg)
- .toMessageChainNoSource(bot, 0, MessageSourceKind.GROUP)
- .refineDeep(bot, refineContext)
- )
- }
- private suspend fun downloadMultiMsgTransmit(
- bot: Bot,
- resourceId: String,
- resourceKind: ResourceKind,
- ): MsgTransmit.PbMultiMsgTransmit {
- bot.asQQAndroidBot()
- when (val resp = MultiMsg.ApplyDown(bot.client, 2, resourceId, 1).sendAndExpect(bot)) {
- is MultiMsg.ApplyDown.Response.RequireDownload -> {
- val http = Mirai.Http
- val origin = resp.origin
- val data: ByteArray = if (origin.msgExternInfo?.channelType == 2) {
- tryDownload(
- bot = bot,
- host = "https://ssl.htdata.qq.com",
- port = 443,
- times = 3,
- resourceKind = resourceKind,
- channelKind = ChannelKind.HTTP
- ) { host, _ ->
- http.get("$host${origin.thumbDownPara}")
- }
- } else tryServersDownload(
- bot = bot,
- servers = origin.uint32DownIp.zip(origin.uint32DownPort),
- resourceKind = resourceKind,
- channelKind = ChannelKind.HTTP
- ) { ip, port ->
- http.get("http://$ip:$port${origin.thumbDownPara}")
- }
- val body = data.read {
- check(readByte() == 40.toByte()) {
- "bad data while MultiMsg.ApplyDown: ${data.toUHexString()}"
- }
- val headLength = readInt()
- val bodyLength = readInt()
- discardExact(headLength)
- readBytes(bodyLength)
- }
- val decrypted = TEA.decrypt(body, origin.msgKey)
- val longResp =
- decrypted.loadAs(LongMsg.RspBody.serializer())
- val down = longResp.msgDownRsp.single()
- check(down.result == 0) {
- "Message download failed, result=${down.result}, resId=${down.msgResid.encodeToString()}, msgContent=${down.msgContent.toUHexString()}"
- }
- val content = down.msgContent.ungzip()
- return content.loadAs(MsgTransmit.PbMultiMsgTransmit.serializer())
- }
- MultiMsg.ApplyDown.Response.MessageTooLarge -> {
- error("Message is too large and cannot download")
- }
- }
- }
- }
|