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

commonMain.message.image.OnlineImage.kt Maven / Gradle / Ivy

/*
 * 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.message.image

import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import net.mamoe.mirai.internal.message.contextualBugReportException
import net.mamoe.mirai.internal.network.protocol.data.proto.CustomFaceExtPb
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
import net.mamoe.mirai.internal.network.protocol.data.proto.ImgExtPbResvAttrCommon
import net.mamoe.mirai.internal.network.protocol.data.proto.NotOnlineImageExtPb
import net.mamoe.mirai.internal.utils.io.serialization.loadAs
import net.mamoe.mirai.internal.utils.structureToString
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.ImageType
import net.mamoe.mirai.utils.generateImageId
import net.mamoe.mirai.utils.generateImageIdFromResourceId

internal sealed interface OnlineImage : Image, ConstOriginUrlAware {
    override val originUrl: String
}

/**
 * 接收消息时获取到的 [FriendImage]. 它可以直接获取下载链接 [originUrl]
 */
internal sealed class OnlineFriendImage : FriendImage(), OnlineImage


@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable(with = OnlineFriendImageImpl.Serializer::class)
internal class OnlineFriendImageImpl(
    internal val delegate: ImMsgBody.NotOnlineImage,
) : @Suppress("DEPRECATION")
OnlineFriendImage() {
    object Serializer : Image.FallbackSerializer("OnlineFriendImage")

    override val size: Long get() = delegate.fileLen
    override val width: Int
        get() = delegate.picWidth
    override val height: Int
        get() = delegate.picHeight
    override val imageType: ImageType
        get() = OnlineImageIds.speculateImageType(delegate.filePath, delegate.imgType)
    override val imageId: String = kotlin.run {
        val imageType = imageType.formatName
        generateImageIdFromResourceId(delegate.resId, imageType)
            ?: kotlin.run {
                if (delegate.picMd5.size == 16) generateImageId(delegate.picMd5, imageType)
                else {
                    Image.logger.warning(
                        contextualBugReportException(
                            "Failed to compute friend imageId: resId=${delegate.resId}",
                            delegate.structureToString(),
                            additional = "并描述此时 Bot 是否正在从好友或群接受消息, 尽量附加该图片原文件"
                        )
                    )
                    delegate.resId
                }
            }
    }

    override val originUrl: String
        get() = if (delegate.origUrl.isNotBlank()) {
            "http://c2cpicdw.qpic.cn" + this.delegate.origUrl
        } else if (delegate.resId.isNotEmpty() && delegate.resId[0] == '{') {
            // https://github.com/mamoe/mirai/issues/1600
            gchatImageUrlByImageId(imageId)
        } else {
            "http://c2cpicdw.qpic.cn/offpic_new/0/" + delegate.resId + "/0?term=2"
        }

    override val isEmoji: Boolean by lazy {
        delegate.pbReserve.pbImageResv_checkIsEmoji(NotOnlineImageExtPb.ResvAttr.serializer())
    }
}


private fun gchatImageUrlByImageId(imageId: String) =
    "http://gchat.qpic.cn/gchatpic_new/0/0-0-${
        imageId.substring(1..36)
            .replace("-", "")
    }/0?term=2"


private fun  ByteArray.pbImageResv_checkIsEmoji(serializer: KSerializer): Boolean {
    val data = this
    return kotlin.runCatching {
        data.takeIf { it.isNotEmpty() }?.loadAs(serializer)?.let { ext ->
            ext.imageBizType == 1 || ext.textSummary.decodeToString() == "[动画表情]"
        }
    }.getOrNull() ?: false
}

/**
 * 接收消息时获取到的 [GroupImage]. 它可以直接获取下载链接 [originUrl]
 */
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
internal sealed class OnlineGroupImage : GroupImage(), OnlineImage


@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable(with = OnlineGroupImageImpl.Serializer::class)
internal class OnlineGroupImageImpl(
    internal val delegate: ImMsgBody.CustomFace,
) : OnlineGroupImage() {
    object Serializer : Image.FallbackSerializer("OnlineGroupImage")

    override val size: Long get() = delegate.size.toLong()
    override val width: Int
        get() = delegate.width
    override val height: Int
        get() = delegate.height
    override val imageType: ImageType
        get() = OnlineImageIds.speculateImageType(delegate.filePath, delegate.imageType)

    override val imageId: String = generateImageId(
        delegate.picMd5,
        OnlineImageIds.speculateImageTypeNameFromFilePath(delegate.filePath)
    ).takeIf {
        Image.IMAGE_ID_REGEX.matches(it)
    } ?: generateImageId(delegate.picMd5)

    override val originUrl: String
        get() = if (delegate.origUrl.isBlank()) {
            gchatImageUrlByImageId(imageId)
        } else "http://gchat.qpic.cn" + delegate.origUrl

    override val isEmoji: Boolean by lazy {
        delegate.pbReserve.pbImageResv_checkIsEmoji(CustomFaceExtPb.ResvAttr.serializer())
    }
}

private object OnlineImageIds {

    fun speculateImageType(filePath: String, imageTypeInt: Int): ImageType {
        return getImageTypeById(imageTypeInt) ?: speculateImageTypeFromImageId(filePath) ?: ImageType.UNKNOWN
    }

    fun speculateImageTypeFromImageId(filePathOrImageId: String): ImageType? {
        return speculateImageTypeNameFromFilePath(filePathOrImageId)?.let { ImageType.matchOrNull(it) }
    }

    /**
     * @param filePath should ends with `.type`
     */
    fun speculateImageTypeNameFromFilePath(filePath: String): String? {
        return filePath.substringAfterLast('.').lowercase().let { ext ->
            if (ext == "null") {
                // official clients might send `null`
                null
            } else ext
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy