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

com.simbot.component.mirai.utils.MsgParseUtils.kt Maven / Gradle / Ivy

There is a newer version: 1.11.0-1.17-Final
Show newest version
/*
 *
 * Copyright (c) 2020. ForteScarlet All rights reserved.
 * Project  component-mirai
 * File     MsgParseUtils.kt
 *
 * You can contact the author through the following channels:
 *  github https://github.com/ForteScarlet
 *  gitee  https://gitee.com/ForteScarlet
 *  email  [email protected]
 *  QQ     1149159218
 *  The Mirai code is copyrighted by mamoe-mirai
 *  you can see mirai at https://github.com/mamoe/mirai
 *
 *
 */

@file:Suppress("unused")
@file:JvmName("MsgParseUtils")

package com.simbot.component.mirai.utils

import cn.hutool.core.io.FileUtil
import com.forte.qqrobot.log.QQLog
import com.simbot.component.mirai.CQCodeParamNullPointerException
import com.simbot.component.mirai.CQCodeParseHandlerRegisterException
import com.simbot.component.mirai.CacheMaps
import com.simbot.component.mirai.collections.ImageCache
import com.simbot.component.mirai.collections.toCacheKey
import com.simbot.component.mirai.collections.toImgVoiceCacheKey
import com.simplerobot.modules.utils.*
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.utils.io.jvm.javaio.*
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.action.Nudge
import net.mamoe.mirai.message.action.Nudge.Companion.sendNudge
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.message.data.AtAll
import net.mamoe.mirai.message.uploadAsGroupVoice
import net.mamoe.mirai.utils.toExternalImage
import java.io.*
import java.net.URL
import java.util.function.BiFunction
import kotlin.collections.set


/**
 * 发送消息之前,会将Message通过此处进行处理。
 * 此处可解析部分CQ码并转化为Message
 * 然后发送此消息
 */
suspend fun  C.sendMsg(msg: String, cacheMaps: CacheMaps): MessageReceipt? {
    if (msg.isBlank()) {
        throw IllegalArgumentException("msg is empty.")
    }
    val message = msg.toWholeMessage(this, cacheMaps)
    return if (message !is EmptyMessageChain) {
        this.sendMessage(message)
    } else {
        // QQLog.debug("mirai.bot.sender.nothing")
        null
    }
}


/**
 * 字符串解析为 [Message]。
 * 一般解析其中的CQ码
 */
fun String.toWholeMessage(contact: Contact, cacheMaps: CacheMaps): Message {
    // 切割,解析CQ码并拼接最终结果
    return KQCodeUtils.split(this) {
        if (this.trim().startsWith("[CQ:")) {
            // 如果是CQ码,转化为KQCode并进行处理
            KQCode.of(this).toMessageAsync(contact, cacheMaps)
        } else {
            if (this.isBlank()) {
                EmptyMessageChain
            } else {
                PlainText(CQDecoder.decodeText(this))
            }.async()
        }
    }.asSequence().map {
        runBlocking {
            it.await()
        }.also { _ ->
            it.invokeOnCompletion { e ->
                e?.also { ex -> throw ex }
            }
        }
    }.reduce { acc, msg ->
        when {
            acc is EmptyMessageChain && msg is EmptyMessageChain -> EmptyMessageChain
            msg is EmptyMessageChain -> acc
            acc is EmptyMessageChain -> msg
            else -> acc + msg
        }
    }
}

/**
 * KQCode转化为Message对象
 */
fun KQCode.toMessageAsync(contact: Contact, cacheMaps: CacheMaps): Deferred {
    // 判断类型,有些东西有可能并不存在与CQ码规范中,例如XML
    @Suppress("DuplicatedCode")
    return when (this.type) {
        //region CQ码解析为Message
        //region at
        "at" -> {
            val id = this["qq"] ?: this["at"] ?: throw CQCodeParamNullPointerException("at", "qq", "at")
            if (id == "all") {
                AtAll.async()
            } else {
                when (contact) {
                    is Member -> {
                        At(contact).async()
                    }
                    is Friend -> {
                        PlainText("@${contact.nick} ").async()
                    }
                    is Group -> {
                        At(contact[id.toLong()]).async()
                    }
                    else -> PlainText("@$id").async()
                }
            }
        }
        //endregion

        //region face
        "face" -> Face((this["id"] ?: this["face"])!!.toInt()).async()
        //endregion


        //region image
        "image" -> contact.async {
            // image 类型的CQ码,参数一般是file, destruct
            val fileValue: String =
                this@toMessageAsync["file"] ?: this@toMessageAsync["image"]
                ?: throw CQCodeParamNullPointerException(
                    "image",
                    "file",
                    "image"
                )

            val fileCache: String = fileValue.toImgVoiceCacheKey(contact)


            val imageCache: ImageCache = cacheMaps.imageCache

            // 先查缓存
            val image: Image? = imageCache[fileCache]

            if (image == null) {
                // 是否缓存此上传的图片
                val cache: Boolean = this@toMessageAsync["cache"] != "false"
                return@async if (fileValue.startsWith("http")) {
                    // 网络图片
                    contact.uploadImage(URL(fileValue).toStream().toExternalImage()).also {
                        if (cache) {
                            imageCache[fileCache] = it
                        }
                    }
                } else {
                    var cacheKey = fileCache
                    val localFile: File = FileUtil.file(fileValue)
                    val externalImage = if (!localFile.exists()) {
                        // 尝试看看有没有url参数, 如果没有则抛出异常
                        val url = this@toMessageAsync["url"] ?: throw FileNotFoundException(fileValue)
                        cacheKey = url.toImgVoiceCacheKey(contact)
                        // 如果有,通过url发送
                        URL(url).toStream().toExternalImage()
                    } else {
                        localFile.toExternalImage()
                    }
                    contact.uploadImage(externalImage).also {
                        if (cache) {
                            imageCache[cacheKey] = it
                        }
                    }.run {
                        if (this@toMessageAsync["destruct"] == "true") {
                            this.flash()
                        } else this
                    }
                }
            }

            // 如果是闪照则转化
            if (this@toMessageAsync["destruct"] == "true") {
                image.flash()
            } else {
                image
            }
        }

        //endregion


        //region record 语音
        "voice", "record" -> contact.async {
            // voice 类型的CQ码,参数一般是file
            val fileValue = this@toMessageAsync["file"] ?: this@toMessageAsync["voice"] ?: throw CQCodeParamNullPointerException(
                "file",
                "voice"
            )
            // 先找缓存

            val fileCache = fileValue.toImgVoiceCacheKey(contact)

            val voiceCache = cacheMaps.voiceCache

            // 截止到1.2.0, 只支持Group.uploadVoice
            // see https://github.com/mamoe/mirai/releases/tag/1.2.0
            // return @async
            voiceCache[fileCache] ?: if (contact is Group) {
                val cache: Boolean = this@toMessageAsync["cache"] != "false"
                if (fileValue.startsWith("http")) {
                    // 网络图片
                    val stream = URL(fileValue).toStream()
//                    contact.async {
                    stream.uploadAsGroupVoice(contact).also {
                        if (cache) {
                            voiceCache[fileCache] = it
                            voiceCache[it.fileName.toImgVoiceCacheKey(contact)] = it
                        }
                    }
//                    }
                } else {
                    // 本地文件
                    val voiceFile = File(fileValue)
                    val stream = BufferedInputStream(FileInputStream(voiceFile))
//                    contact.async {
                    contact.uploadVoice(stream).also {
                        if (cache) {
                            voiceCache[fileCache] = it
                            voiceCache[it.fileName.toImgVoiceCacheKey(contact)] = it
                        }
                    }
//                    }
                }
            } else {
                EmptyMessageChain
            }
        }
        //endregion


        //region rps 猜拳
        "rps" -> {
            EmptyMessageChain.async()
            // 似乎也不支持猜拳
//            PlainText("[猜拳]")
        }
        //endregion

        //region dice 骰子
        "dice" -> {
            EmptyMessageChain.async()
            // 似乎也..
//            PlainText("[骰子]")
        }
        //endregion

        //region shake 戳一戳
        "shake", "poke" -> {
            // 戳一戳进行扩展,可多解析'type'参数与'id'参数。
            // 如果没有type,直接返回戳一戳
            // 如果是再群内发送,则优先认为是头像戳一戳
            val type = this["type"]?.toInt() ?: return PokeMessage.Poke.async()
            val id = this["id"]?.toInt() ?: -1

            // 如果目标是一个群成员,则说明使用双击头像的”戳一戳“
            // 此戳一戳将会被立即发送,并返回一个空消息串
            when (contact) {
                is Group -> {
                    val code: Long = this["code"]?.toLong() ?: throw IllegalArgumentException("cannot found nudge target: code is empty.")
                    val nudge: Nudge = contact.getOrNull(code)?.nudge() ?: throw IllegalArgumentException("cannot found nudge target: no such member($code) in group(${contact.id}).")
                    // 获取群员并发送
                    contact.async {
                        contact.sendNudge(nudge)
                        EmptyMessageChain
                    }
                }
                else -> {
                    // 尝试寻找对应的Poke,找不到则返回戳一戳
                    return (PokeMessage.values.find { it.type == type && it.id == id } ?: PokeMessage.Poke).async()
                }
            }
        }
        //endregion

        //region 双击头像戳一戳
        "nudge" -> {
            when(contact) {
                // 如果是群
                is Group -> {
                    val code: Long = this["target"]?.toLong() ?: throw IllegalArgumentException("cannot found nudge target: target is empty.")
                    val nudge: Nudge = contact.getOrNull(code)?.nudge() ?: throw NoSuchElementException("cannot found nudge target: no such member($code) in group(${contact.id}).")
                    // 获取群员并发送
                    contact.async {
                        contact.sendNudge(nudge)
                        EmptyMessageChain
                    }
                }
                is User -> {
                    val nudge: Nudge = contact.nudge()
                    contact.async {
                        contact.sendNudge(nudge)
                        EmptyMessageChain
                    }
                }
                // 是其他人
                else -> EmptyMessageChain.async()
            }
        }
        //endregion

        //region anonymous
        "anonymous" -> {
            // 匿名消息,不进行解析
            EmptyMessageChain.async()
        }
        //endregion

        //region music
        "music" -> {
            // 音乐,就是分享,应该归类于xml
            // 参数:"type", "id", "style*"
            // 或者:"type", "url", "audio", "title", "content*", "image*"
            val type = this["type"] ?: ""
            val title = this["title"] ?: ""
            val urlOrId = this["url"] ?: this["id"] ?: ""
            PlainText("""
                |[${type}音乐]
                |$title
                |$urlOrId
            """.trimMargin()).async()
        }
        //endregion

        //region share
        "share" -> {
            EmptyMessageChain.async()
            // 分享
            // 参数:"url", "title", "content*", "image*"
//            val type = this["url"]
//            val title = this["title"]
//            PlainText("$title: $type")
        }
        //endregion

        //region emoji
        "emoji" -> {
            EmptyMessageChain.async()
            // emoji, 基本用不到
            // val id = this["id"] ?: ""
            // PlainText("emoji($id)")
        }
        //endregion


        //region location
        "location" -> {
            EmptyMessageChain.async()
            // 地点 "lat", "lon", "title", "content"
//            val lat = this["lat"] ?: ""
//            val lon = this["lon"] ?: ""
//            val title = this["title"] ?: ""
//            val content = this["content"] ?: ""
//            PlainText("位置($lat,$lon)[$title]:$content")
        }
        //endregion

        //region sign
        "sign" -> EmptyMessageChain.async()

        //endregion

        //region show
        "show" -> EmptyMessageChain.async()
        //endregion


        //region contact
        "contact" -> {
            EmptyMessageChain.async()
            // 联系人分享
            // ype一般可能是qq或者group
            // [CQ:contact,id=1234546,type=qq]
            // val id = this["id"] ?: return EmptyMessageChain

            // val typeName = when(this["type"]){
            //     "qq" -> "好友分享"
            //     "group" -> "群聊分享"
            //     else -> "其他分享"
            // }
            // PlainText("$typeName: $id")
        }
        //endregion

        //region xml message
        //对XML类型的CQ码做解析
        "xml" -> {
            val xmlCode = this
            // 解析的参数
//            val action = this["action"] ?: "plugin"
//            val flag: Int = this["flag"]?.toInt() ?: 3
//            val url = this["url"] ?: ""
//            val sourceName = this["sourceName"] ?: ""
//            val sourceIconURL = this["sourceIconURL"] ?: ""

            // 构建xml
            return buildXmlMessage(60) {
                // action
                xmlCode["action"]?.also { this.action = it }
                // 一般为点击这条消息后跳转的链接
                xmlCode["actionData"]?.also { this.actionData = it }
                /*
                   摘要, 在官方客户端内消息列表中显示
                 */
                xmlCode["brief"]?.also { this.brief = it }
                xmlCode["flag"]?.also { this.flag = it.toInt() }
                xmlCode["url"]?.also { this.url = it }
                // sourceName 好像是名称
                xmlCode["sourceName"]?.also { this.sourceName = it }
                // sourceIconURL 好像是图标
                xmlCode["sourceIconURL"]?.also { this.sourceIconURL = it }

                // builder
//                val keys = xmlCode.params.keys

                this.item {
                    xmlCode["bg"]?.also { this.bg = it.toInt() }
                    xmlCode["layout"]?.also { this.layout = it.toInt() }
                    // picture(coverUrl: String)
                    xmlCode["picture_coverUrl"]?.also { this.picture(it) }
                    // summary(text: String, color: String = "#000000")
                    xmlCode["summary_text"]?.also {
                        val color: String = xmlCode["summary_color"] ?: "#000000"
                        this.summary(it, color)
                    }
                    // title(text: String, size: Int = 25, color: String = "#000000")
                    xmlCode["title_text"]?.also {
                        val size: Int = xmlCode["title_size"]?.toInt() ?: 25
                        val color: String = xmlCode["title_color"] ?: "#000000"
                        this.title(it, size, color)
                    }

                }

            }.async()
        }
        //endregion


        //region lightApp小程序 & json
        // 一般都是json消息
        "app", "json" -> {
            val content: String = this["content"] ?: "{}"
            LightApp(content).async()
        }
        //endregion


        //region rich 或 service, 对应serviceMessage。
        "rich", "service" -> {
            val content: String = this["content"] ?: "{}"
            // 如果没有serviceId,认为其为lightApp
            val serviceId: Int = this["serviceId"]?.toInt() ?: return LightApp(content).async()
            ServiceMessage(serviceId, content).async()
        }
        //endregion

        //region quote
        // 引用回复
        "quote" -> {
            val key = this["id"] ?: this["quote"] ?: throw CQCodeParamNullPointerException("quote", "id")
            val source = cacheMaps.recallCache.get(key, contact.bot.id) ?: return EmptyMessageChain.async()
            QuoteReply(source).async()
        }
        //endregion


        //region else
        else -> {
            val handler = CQCodeParsingHandler[this.type]
            return if (handler != null) {
                handler(this, contact)
            } else {
                PlainText(this.toString()).async()
            }
        }
        //endregion
        //endregion


    }


}

/**
 * 一个非挂起返回值得到[Deferred]实例
 */
private fun Message.async(): Deferred {
    return when (this) {
        is EmptyMessageChain -> EmptyMessageChainDeferred
//        else -> coroutineScope.async(start = CoroutineStart.LAZY) { this@async }
        else -> SimpleDefaultDeferred(this)
    }
}


/**
 * ktor http client
 */
private val httpClient: HttpClient = HttpClient()

/**
 * 通过http网络链接得到一个输入流。
 * 通常认为是一个http-get请求
 */
private suspend fun URL.toStream(): InputStream {
    val urlString = this.toString()
    QQLog.debug("mirai.http.connection.try", urlString)
    val response = httpClient.get(this)
    val status = response.status
    if (status.value < 300) {
        QQLog.debug("mirai.http.connection.success", urlString)
        // success
        return response.content.toInputStream()
    } else {
        throw IllegalStateException("connection to '$urlString' failed ${status.value}: ${status.description}")
    }

//    val urlName = this.toString()
//    var connection: HttpURLConnection = this.openConnection() as HttpURLConnection
//    connection.connectTimeout = 10_000
//    connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36")
//    while(true) {
//        val responseCode = connection.responseCode
//        if(responseCode == 302){
//            val location = connection.getHeaderField("Location")
//            connection = URL(location).openConnection() as HttpURLConnection
//            connection.connectTimeout = 10_000
//            connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36")
//            continue
//        }else if(responseCode >= 300){
//            val errStream = connection.errorStream
//            val errText = BufferedReader(InputStreamReader(errStream)).use { it.readText() }
//            throw IllegalStateException("http connection to $urlName failed($responseCode): $errText")
//        }else {
//            return connection.inputStream
//        }
//    }


}


/**
 * 转化函数
 */
@FunctionalInterface
interface CQCodeHandler : (KQCode, Contact) -> Deferred, BiFunction> {
    @JvmDefault
    override fun apply(code: KQCode, contact: Contact): Deferred = this.invoke(code, contact)
}


/**
 * 可注册的额外解析器
 */
object CQCodeParsingHandler {

    /** 注册额外的解析器 */
    private val otherHandler: MutableMap by lazy { mutableMapOf() }

    /**
     * get
     */
    operator fun get(cqType: String) = otherHandler[cqType]

    /**
     * set, same as [registerHandler]
     */
    internal operator fun set(cqType: String, handler: CQCodeHandler) {
        registerHandler(cqType, handler)
    }

    /**
     * 注册一个处理器。
     * @param cqType 要解析的类型
     * @param handler 解析器
     */
    @JvmStatic
    fun registerHandler(cqType: String, handler: CQCodeHandler) {
        if (otherHandler.containsKey(cqType)) {
            throw CQCodeParseHandlerRegisterException("failed.existed", cqType)
        } else {
            otherHandler[cqType] = handler
        }
    }

    /**
     * 获取所有处理器
     */
    @JvmStatic
    fun handlers(): Map = otherHandler.toMap()

}


/**
 * mirai码格式化兼容工具
 */
object MiraiCodeFormatUtils {

    /**
     * 字符串替换,替换消息中的`mirai`码为`cq`码
     */
    @JvmStatic
    fun mi2cq(msg: MessageChain?, cacheMaps: CacheMaps): String? {
        if (msg == null) {
            return null
        }

        // 携带mirai码的字符串
        return msg.asSequence()
            .map { it.toCqOrTextString(cacheMaps) }
            .joinToString("")
    }

}

/**
 * [SingleMessage] to cqcode string
 */
fun SingleMessage.toCqOrTextString(cacheMaps: CacheMaps): String {
    return if (this !is MessageSource) {
        when (this) {
            // 普通的文本消息,转义并普通的返回
            is PlainText -> CQEncoder.encodeText(content)

            // voice, 转化为record类型的cq码
            is Voice -> {
                val builder: CodeBuilder = KQCodeUtils.getStringBuilder("record")
                    .key("file").value(fileName)
                    .key("size").value(fileSize)
                url?.run { builder.key("url").value(this) }
                builder.build()
            }

            // 普通image
            is Image -> {
                toCq(cacheMaps.imageCache, false)
            }

            // 闪照
            is FlashImage -> {
                image.toCq(cacheMaps.imageCache, true)
            }

            is At -> {
                KQCodeUtils.getStringBuilder("at")
                    .key("qq").value(target)
                    .key("display").value(display)
                    .build()
            }

            // at all
            is AtAll -> com.simplerobot.modules.utils.AtAll.toString()


            // face -> id
            is Face -> {
                KQCodeUtils.toCq("face", false, "id=$id")
            }

            // poke message, get id & type
            is PokeMessage -> {
                // val pokeMq = MQCodeUtils.toMqCode(this.toString())
                // val pokeKq = pokeMq.toKQCode().mutable()
                // pokeKq["type"] = this.type.toString()
                // pokeKq["id"] = this.id.toString()
                // pokeKq

                KQCodeUtils.toCq("poke", false, "name=$name", "type=$type", "id=$id")
            }

            // 引用
            is QuoteReply -> {
                // val quoteMq = MQCodeUtils.toMqCode(this.toString())
                // val quoteKq = quoteMq.toKQCode().mutable()
                // quoteKq["id"] = this.source.toCacheKey()
                // quoteKq["qq"] = this.source.fromId.toString()
                // quoteKq
                KQCodeUtils.toCq("quote", false, "id=${source.toCacheKey()}", "qq=${source.fromId}")
            }

            // 富文本
            is RichMessage -> when (this) {
                // app
                is LightApp -> {
                    KQCodeUtils.toCq("app", true, "content=$content")
                }
                // service message
                is ServiceMessage -> {
                    KQCodeUtils.toCq("service", true, "content=$content", "serviceId=$serviceId")
                }
                else -> {
                    val string = toString()
                    return if (string.trim().startsWith("[mirai:")) {
                        MQCodeUtils.toMqCode(string).toKQCode().toString()
                    } else string
                }
            }

            // 其他东西,不做特殊处理
            else -> {
                val string: String = toString()
                return if (string.trim().startsWith("[mirai:")) {
                    MQCodeUtils.toMqCode(string).toKQCode().toString()
                } else string
            }
        }
    } else ""
}

/**
 * 将一个[Image]实例转化为[KQCode]. 如果[imageCache]不为null, 则会缓存.
 * [flash]代表其是否为闪照.
 */
private fun Image.toCq(imageCache: ImageCache?, flash: Boolean): String {
    // 缓存image
    val imageId = this.imageId
    imageCache?.set(imageId, this)

    // builder

    val builder: CodeBuilder = KQCodeUtils.getStringBuilder("image")
        .key("file").value(imageId)
        .key("url").value(runBlocking { queryUrl() })

    // val imageMq = MQCodeUtils.toMqCode(this.toString())
    // val imageKq = imageMq.toKQCode().mutable()
    // imageKq["file"] = imageId
    // imageKq["url"] = runBlocking { queryUrl() }
    if (flash) {
        builder.key("destruct").value("true")
        // imageKq["destruct"] = "true"
    }
    return builder.build()
}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy