All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.MiraiImpl.kt Maven / Gradle / Ivy

There is a newer version: 2.16.0
Show newest version
/*
 * Copyright 2019-2022 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 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.event.EventChannelToEventDispatcherAdapter
import net.mamoe.mirai.internal.event.InternalEventMechanism
import net.mamoe.mirai.internal.message.*
import net.mamoe.mirai.internal.message.DeepMessageRefiner.refineDeep
import net.mamoe.mirai.internal.message.data.*
import net.mamoe.mirai.internal.message.data.FileMessageImpl
import net.mamoe.mirai.internal.message.data.OfflineAudioImpl
import net.mamoe.mirai.internal.message.data.OnlineAudioImpl
import net.mamoe.mirai.internal.message.data.UnsupportedMessageImpl
import net.mamoe.mirai.internal.message.image.*
import net.mamoe.mirai.internal.message.image.OfflineGroupImage
import net.mamoe.mirai.internal.message.image.OnlineFriendImageImpl
import net.mamoe.mirai.internal.message.image.OnlineGroupImageImpl
import net.mamoe.mirai.internal.message.source.*
import net.mamoe.mirai.internal.message.source.OnlineMessageSourceFromFriendImpl
import net.mamoe.mirai.internal.message.source.OnlineMessageSourceFromGroupImpl
import net.mamoe.mirai.internal.message.source.OnlineMessageSourceFromStrangerImpl
import net.mamoe.mirai.internal.message.source.OnlineMessageSourceFromTempImpl
import net.mamoe.mirai.internal.message.source.OnlineMessageSourceToFriendImpl
import net.mamoe.mirai.internal.message.source.OnlineMessageSourceToStrangerImpl
import net.mamoe.mirai.internal.message.source.OnlineMessageSourceToTempImpl
import net.mamoe.mirai.internal.network.components.EventDispatcher
import net.mamoe.mirai.internal.network.highway.ChannelKind
import net.mamoe.mirai.internal.network.highway.ResourceKind
import net.mamoe.mirai.internal.network.highway.tryDownload
import net.mamoe.mirai.internal.network.highway.tryServersDownload
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.MultiMsg
import net.mamoe.mirai.internal.network.protocol.packet.chat.NewContact
import net.mamoe.mirai.internal.network.protocol.packet.chat.NudgePacket
import net.mamoe.mirai.internal.network.protocol.packet.chat.PbMessageSvc
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.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.message.MessageSerializers
import net.mamoe.mirai.message.action.Nudge
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.*

internal fun getMiraiImpl() = Mirai as MiraiImpl

@OptIn(LowLevelApi::class)
// not object for ServiceLoader.
internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
    companion object {
        init {
            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()
            )
        }
    }

    override val BotFactory: BotFactory
        get() = BotFactoryImpl

    override var FileCacheStrategy: FileCacheStrategy = net.mamoe.mirai.utils.FileCacheStrategy.PlatformDefault

    @Deprecated("Mirai is not going to use ktor. This is deprecated for removal.", level = DeprecationLevel.WARNING)
    override var Http: HttpClient = HttpClient(OkHttp) {
        install(HttpTimeout) {
            this.requestTimeoutMillis = 30_0000
            this.connectTimeoutMillis = 30_0000
            this.socketTimeoutMillis = 30_0000
        }
    }

    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 ?: "" }
        @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 ?: "" }
        @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 {
        bot.asQQAndroidBot()
        val response = bot.network.run {
            bot.network.sendAndExpect(StatSvc.GetDevLoginInfo(bot.client))
        }

        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 ?: "" }
        @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)

    @OptIn(InternalEventMechanism::class)
    override suspend fun broadcastEvent(event: Event) {
        if (event is BotEvent) {
            val bot = event.bot
            if (bot is AbstractBot) {
                bot.components[EventDispatcher].broadcast(event)
            }
        } else {
            EventChannelToEventDispatcherAdapter.instance.broadcastEventImpl(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 {
        bot.asQQAndroidBot()
        return bot.network.run {
            bot.network.sendAndExpect(FriendList.GetTroopListSimplify(bot.client))
        }.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 {
        var nextUin = 0L
        var sequence = sequenceOf()
        while (true) {
            val data = bot.asQQAndroidBot().network.sendAndExpect(
                FriendList.GetTroopMemberList(
                    client = bot.client,
                    targetGroupUin = groupUin,
                    targetGroupCode = groupCode,
                    nextUin = nextUin
                ), 5000, 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 {
        val response = bot.asQQAndroidBot().network.sendAndExpect(
            PbMessageSvc.PbMsgWithDraw.createForGroupMessage(
                bot.client,
                groupCode,
                messageIds,
                messageInternalIds
            ), 5000, 2
        )

        return response is PbMessageSvc.PbMsgWithDraw.Response.Success
    }

    override suspend fun recallFriendMessageRaw(
        bot: Bot,
        targetId: Long,
        messageIds: IntArray,
        messageInternalIds: IntArray,
        time: Int,
    ): Boolean {
        val response = bot.asQQAndroidBot().network.sendAndExpect(
            PbMessageSvc.PbMsgWithDraw.createForFriendMessage(
                bot.client,
                targetId,
                messageIds,
                messageInternalIds,
                time,
            ), 5000, 2
        )

        return response is PbMessageSvc.PbMsgWithDraw.Response.Success
    }

    override suspend fun recallGroupTempMessageRaw(
        bot: Bot,
        groupUin: Long,
        targetId: Long,
        messageIds: IntArray,
        messageInternalIds: IntArray,
        time: Int
    ): Boolean {
        val response = bot.asQQAndroidBot().network.sendAndExpect(
            PbMessageSvc.PbMsgWithDraw.createForGroupTempMessage(
                bot.client,
                groupUin,
                targetId,
                messageIds,
                messageInternalIds,
                time,
            ), 5000, 2
        )

        return 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: Group = when (source) {
                    is OnlineMessageSourceToGroupImpl -> source.subject
                    is OnlineMessageSourceFromGroupImpl -> source.subject
                    else -> error("stub")
                }
                if (bot.id != source.fromId) {
                    // if member leave, messageSource will throw exception(#1661)
                    when (group[source.fromId]?.permission ?: MemberPermission.MEMBER) {
                        MemberPermission.MEMBER -> group.checkBotPermission(MemberPermission.ADMINISTRATOR)
                        MemberPermission.ADMINISTRATOR -> group.checkBotPermission(MemberPermission.OWNER)
                        // bot cannot be owner
                        MemberPermission.OWNER -> throw PermissionDeniedException("Permission denied: cannot recall message from owner")
                    }
                }


                bot.asQQAndroidBot().network.sendAndExpect(
                    PbMessageSvc.PbMsgWithDraw.createForGroupMessage(
                        bot.asQQAndroidBot().client,
                        group.id,
                        source.sequenceIds,
                        source.internalIds
                    ), 5000, 2
                )
            }
            is OnlineMessageSourceFromFriendImpl,
            is OnlineMessageSourceToFriendImpl,
            is OnlineMessageSourceFromStrangerImpl,
            is OnlineMessageSourceToStrangerImpl,
            -> {
                check(source.fromId == bot.id) {
                    "can only recall a message sent by bot"
                }
                bot.asQQAndroidBot().network.sendAndExpect(
                    PbMessageSvc.PbMsgWithDraw.createForFriendMessage(
                        bot.client,
                        source.targetId,
                        source.sequenceIds,
                        source.internalIds,
                        source.time
                    ), 5000, 2
                )
            }
            is OnlineMessageSourceFromTempImpl,
            is OnlineMessageSourceToTempImpl
            -> {
                check(source.fromId == bot.id) {
                    "can only recall a message sent by bot"
                }
                source as OnlineMessageSourceToTempImpl
                bot.asQQAndroidBot().network.sendAndExpect(
                    PbMessageSvc.PbMsgWithDraw.createForGroupTempMessage(
                        bot.client,
                        (source.target.group as GroupImpl).uin,
                        source.targetId,
                        source.sequenceIds,
                        source.internalIds,
                        source.time
                    ), 5000, 2
                )
            }
            is OfflineMessageSource -> {
                when (source.kind) {
                    MessageSourceKind.FRIEND, MessageSourceKind.STRANGER -> {
                        check(source.fromId == bot.id) {
                            "can only recall a message sent by bot"
                        }
                        bot.asQQAndroidBot().network.sendAndExpect(
                            PbMessageSvc.PbMsgWithDraw.createForFriendMessage(
                                bot.client,
                                source.targetId,
                                source.sequenceIds,
                                source.internalIds,
                                source.time
                            ), 5000, 2
                        )
                    }
                    MessageSourceKind.TEMP -> {
                        check(source.fromId == bot.id) {
                            "can only recall a message sent by bot"
                        }
                        bot.asQQAndroidBot().network.sendAndExpect(
                            PbMessageSvc.PbMsgWithDraw.createForGroupTempMessage(
                                bot.client,
                                source.targetId, // groupUin
                                source.targetId, // memberUin
                                source.sequenceIds,
                                source.internalIds,
                                source.time
                            ), 5000, 2
                        )
                    }
                    MessageSourceKind.GROUP -> {
                        bot.asQQAndroidBot().network.sendAndExpect(
                            PbMessageSvc.PbMsgWithDraw.createForGroupMessage(
                                bot.client,
                                source.targetId,
                                source.sequenceIds,
                                source.internalIds
                            ), 5000, 2
                        )
                    }
                }
            }
            else -> error("stub!")
        }


        // 1001: No message meets the requirements (实际上是没权限, 管理员在尝试撤回群主的消息)
        // 154: timeout
        // 3: 
        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 {
                @Suppress("DEPRECATION", "DEPRECATION_ERROR")
                Mirai.Http.get {
                    url("https://qqweb.qq.com/c/activedata/get_mygroup_data")
                    parameter("bkn", client.wLoginSigInfo.bkn)
                    parameter("gc", groupId)
                    if (page != -1) {
                        parameter("page", page)
                    }
                    headers {
                        // ktor bug
                        append(
                            "cookie",
                            "uin=o${bot.id}; skey=${bot.sKey}; p_uin=o${bot.id}; p_skey=${bot.psKey("qqweb.qq.com")};"
                        )
                    }
                }
            }
            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 {
            @Suppress("DEPRECATION", "DEPRECATION_ERROR")
            Mirai.Http.get {
                url("https://qun.qq.com/interactive/honorlist")
                parameter("gc", groupId)
                parameter("type", type.value)
                headers {
                    // ktor bug
                    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__=(.+?)""").find(rep)?.groupValues?.get(1)
        return jsonText?.let { json.decodeFromString(GroupHonorListData.serializer(), it) }
    }

    override suspend fun solveNewFriendRequestEvent(
        bot: Bot,
        eventId: Long,
        fromId: Long,
        fromNick: String,
        accept: Boolean,
        blackList: Boolean
    ): Unit = bot.asQQAndroidBot().run {
        network.sendWithoutExpect(
            NewContact.SystemMsgNewFriend.Action(
                bot.client,
                eventId = eventId,
                fromId = fromId,
                accept = accept,
                blackList = blackList
            )
        )

        if (!accept) return

        @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
        bot.friends.delegate.add(newFriend(bot, FriendInfoImpl(fromId, fromNick, "")))
    }

    override suspend fun solveBotInvitedJoinGroupRequestEvent(
        bot: Bot,
        eventId: Long,
        invitorId: Long,
        groupId: Long,
        accept: Boolean
    ) {
        bot.asQQAndroidBot().network.sendWithoutExpect(
            NewContact.SystemMsgNewGroup.Action(
                bot.client,
                eventId = eventId,
                fromId = invitorId,
                groupId = groupId,
                isInvited = true,
                accept = accept
            )
        )
    }

    override suspend fun solveMemberJoinRequestEvent(
        bot: Bot,
        eventId: Long,
        fromId: Long,
        fromNick: String,
        groupId: Long,
        accept: Boolean?,
        blackList: Boolean,
        message: String
    ) {
        bot.asQQAndroidBot().network.sendWithoutExpect(
            NewContact.SystemMsgNewGroup.Action(
                bot.client,
                eventId = eventId,
                fromId = fromId,
                groupId = groupId,
                isInvited = false,
                accept = accept,
                blackList = blackList,
                message = message
            )
        )
        // Add member in MsgOnlinePush.PbPushMsg
    }

    @LowLevelApi
    override suspend fun getGroupVoiceDownloadUrl(
        bot: Bot,
        md5: ByteArray,
        groupId: Long,
        dstUin: Long
    ): String {
        val response = bot.asQQAndroidBot().network.sendAndExpect(
            PttStore.GroupPttDown(bot.client, groupId, dstUin, md5),
            5000,
            2
        )
        return "http://${response.strDomain}${response.downPara.decodeToString()}"
    }

    override suspend fun muteAnonymousMember(
        bot: Bot,
        anonymousId: String,
        anonymousNick: String,
        groupId: Long,
        seconds: Int
    ) {
        bot as QQAndroidBot
        @Suppress("DEPRECATION", "DEPRECATION_ERROR")
        val response = Mirai.Http.post {
            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.client.wLoginSigInfo.bkn)
            })
            headers {
                // ktor bug
                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 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 {

        return bot.asQQAndroidBot().network.sendAndExpect(
            SummaryCard.ReqSummaryCard(bot.client, targetId),
            5000, 2
        )
    }

    override suspend fun sendNudge(bot: Bot, nudge: Nudge, receiver: Contact): Boolean {
        if ((bot.configuration.protocol != BotConfiguration.MiraiProtocol.ANDROID_PHONE) && (bot.configuration.protocol != BotConfiguration.MiraiProtocol.IPAD)) {
            throw UnsupportedOperationException("nudge is supported only with protocol ANDROID_PHONE or IPAD")
        }
        bot.asQQAndroidBot()

        bot.network.run {
            return if (receiver is Group) {
                receiver.checkIsGroupImpl()
                bot.network.sendAndExpect(
                    NudgePacket.troopInvoke(
                        client = bot.client,
                        messageReceiverGroupCode = receiver.id,
                        nudgeTargetId = nudge.target.id,
                    )
                ).success
            } else {
                bot.network.sendAndExpect(
                    NudgePacket.friendInvoke(
                        client = bot.client,
                        messageReceiverUin = receiver.id,
                        nudgeTargetId = nudge.target.id,
                    )
                ).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 {
        try {
            return downloadMultiMsgTransmit(bot, resourceId, ResourceKind.LONG_MESSAGE).msg
                .toMessageChainNoSource(bot, 0, MessageSourceKind.GROUP)
                .refineDeep(bot)
        } catch (error: Throwable) {
            throw IllegalStateException("Failed to download long message `$resourceId`", error)
        }
    }

    override suspend fun downloadForwardMessage(bot: Bot, resourceId: String): List {
        try {
            return downloadMultiMsgTransmit(bot, resourceId, ResourceKind.FORWARD_MESSAGE).toForwardMessageNodes(bot)
        } catch (error: Throwable) {
            throw IllegalStateException("Failed to download forward message `$resourceId`", error)
        }
    }

    internal open suspend fun MsgTransmit.PbMultiMsgNew.toForwardMessageNodes(
        bot: Bot,
        context: RefineContext
    ): List {
        return msg.map { it.toNode(bot, context) }
    }

    internal open suspend fun MsgTransmit.PbMultiMsgTransmit.toForwardMessageNodes(bot: Bot): List {
        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 = bot.network.sendAndExpect(MultiMsg.ApplyDown(bot.client, 2, resourceId, 1))) {
            is MultiMsg.ApplyDown.Response.RequireDownload -> {
                @Suppress("DEPRECATION", "DEPRECATION_ERROR")
                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.decodeToString()}, 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")
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy