diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Contact.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Contact.kt index 526c69e8f..d8e2dd40f 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Contact.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Contact.kt @@ -74,6 +74,8 @@ abstract class Contact : CoroutineScope, ContactJavaFriendlyAPI(), Identified { /** * 上传一个图片以备发送. * + * @see Image 查看更多信息 + * * @see BeforeImageUploadEvent 图片发送前事件, cancellable * @see ImageUploadEvent 图片发送完成事件 * diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/HummerMessage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/HummerMessage.kt index 410454992..cb544f1f1 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/HummerMessage.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/HummerMessage.kt @@ -11,6 +11,7 @@ package net.mamoe.mirai.message.data +import net.mamoe.mirai.message.data.PokeMessage.Types import net.mamoe.mirai.utils.MiraiExperimentalAPI import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.SinceMirai @@ -40,15 +41,18 @@ sealed class HummerMessage : MessageContent { /** * 戳一戳. 可以发送给好友或群. + * + * @see Types 使用伴生对象中的常量 */ @SinceMirai("0.31.0") @OptIn(MiraiInternalAPI::class) -data class PokeMessage @MiraiInternalAPI(message = "使用伴生对象中的常量") constructor( - @MiraiExperimentalAPI +data class PokeMessage internal constructor( + @MiraiExperimentalAPI("may change in future") val type: Int, - @MiraiExperimentalAPI + @MiraiExperimentalAPI("may change in future") val id: Int ) : HummerMessage() { + @Suppress("DEPRECATION_ERROR", "DEPRECATION", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") companion object Types : Message.Key { override val typeName: String get() = "PokeMessage" @@ -117,6 +121,8 @@ data class PokeMessage @MiraiInternalAPI(message = "使用伴生对象中的常 * 闪照 * * @see Image.flash 转换普通图片为闪照 + * + * @see Image 查看图片相关信息 */ @SinceMirai("0.33.0") sealed class FlashImage : MessageContent, HummerMessage() { diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Image.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Image.kt index b91fbc07e..07f757243 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Image.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Image.kt @@ -29,13 +29,14 @@ import kotlin.jvm.JvmSynthetic /** * 自定义表情 (收藏的表情), 图片 * + * 查看平台 actual 定义以获取更多说明. + * * @see FlashImage 闪照 * @see Image.flash 转换普通图片为闪照 */ -interface Image : Message, MessageContent { +expect interface Image : Message, MessageContent { companion object Key : Message.Key { override val typeName: String - get() = "Image" } /** @@ -64,7 +65,7 @@ interface Image : Message, MessageContent { fun Image(imageId: String): Image = when { imageId.startsWith('/') -> OfflineFriendImage(imageId) // /f8f1ab55-bf8e-4236-b55e-955848d7069f imageId.length == 42 || imageId.startsWith('{') && imageId.endsWith('}') -> OfflineGroupImage(imageId) // {01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.png - else -> throw IllegalArgumentException("Bad imageId, expecting length=37 or 42, got ${imageId.length}") + else -> throw IllegalArgumentException("illegal imageId: $imageId. $ILLEGAL_IMAGE_ID_EXCEPTION_MESSAGE") } @MiraiInternalAPI("使用 Image") @@ -286,82 +287,4 @@ data class OfflineFriendImage( */ abstract class OnlineFriendImage : FriendImage(), OnlineImage - -// endregion - - -////////////////////// -// region internal -////////////////////// - - -private val EMPTY_BYTE_ARRAY = ByteArray(0) - - -// /000000000-3814297509-BFB7027B9354B8F899A062061D74E206 -private val FRIEND_IMAGE_ID_REGEX_1 = Regex("""/[0-9]*-[0-9]*-[0-9a-zA-Z]{32}""") - -// /f8f1ab55-bf8e-4236-b55e-955848d7069f -private val FRIEND_IMAGE_ID_REGEX_2 = Regex("""/.{8}-(.{4}-){3}.{12}""") - -// {01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.png -private val GROUP_IMAGE_ID_REGEX = Regex("""\{.{8}-(.{4}-){3}.{12}}\..*""") - -@Suppress("NOTHING_TO_INLINE") // no stack waste -internal inline fun Char.hexDigitToByte(): Int { - return when (this) { - in '0'..'9' -> this - '0' - in 'A'..'F' -> 10 + (this - 'A') - in 'a'..'f' -> 10 + (this - 'a') - else -> throw IllegalArgumentException("illegal hex digit: $this") - } -} - -internal fun String.skipToSecondHyphen(): Int { - var count = 0 - this.forEachIndexed { index, c -> - if (c == '-' && ++count == 2) return index - } - error("cannot find two hyphens") -} - -internal fun String.imageIdToMd5(offset: Int): ByteArray { - val result = ByteArray(16) - var cur = 0 - var hasCurrent = false - var lastChar: Char = 0.toChar() - for (index in offset..this.lastIndex) { - val char = this[index] - if (char == '-') continue - if (hasCurrent) { - result[cur++] = (lastChar.hexDigitToByte().shl(4) or char.hexDigitToByte()).toByte() - if (cur == 16) return result - hasCurrent = false - } else { - lastChar = char - hasCurrent = true - } - } - error("No enough chars") -} - -@OptIn(ExperimentalStdlibApi::class) -internal fun calculateImageMd5ByImageId(imageId: String): ByteArray { - return when { - imageId.matches(FRIEND_IMAGE_ID_REGEX_1) -> imageId.imageIdToMd5(imageId.skipToSecondHyphen() + 1) - imageId.matches(FRIEND_IMAGE_ID_REGEX_2) -> - imageId.imageIdToMd5(1) - imageId.matches(GROUP_IMAGE_ID_REGEX) -> { - imageId.imageIdToMd5(1) - } - else -> error( - "illegal imageId: $imageId. " + - "ImageId must matches Regex `${FRIEND_IMAGE_ID_REGEX_1.pattern}`, " + - "`${FRIEND_IMAGE_ID_REGEX_2.pattern}` or " + - "`${GROUP_IMAGE_ID_REGEX.pattern}`" - ) - } -} - - // endregion \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Message.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Message.kt index a5a054634..6f31c9cfc 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Message.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Message.kt @@ -113,7 +113,7 @@ interface Message { * [GroupImage]: "[mirai:image:{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.png]" * [FriendImage]: "[mirai:image:/f8f1ab55-bf8e-4236-b55e-955848d7069f]" * [PokeMessage]: "[mirai:poke:1,-1]" - * [MessageChain]: 直接无间隔地连接所有元素 (`joinToString("")`) + * [MessageChain]: 无间隔地连接所有元素 (`joinToString("")`) * * @see contentToString */ @@ -122,10 +122,13 @@ interface Message { /** * 转为最接近官方格式的字符串. 如 `At(member) + "test"` 将转为 `"@群名片 test"`. * - * 对于 [NullMessageChain], 这个函数返回空字符串 ""; - * 对于其他 [MessageChain], 这个函数返回值同 [toString]. + * 在使用消息相关 DSL 和扩展时, 一些内容比较的实现均使用 [contentToString] 而不是 [toString] * - * 在使用消息相关 DSL 和扩展时, 一些内容比较的实现均使用的是 [contentToString] 而不是 [toString] + * 各个 [SingleMessage] 的转换示例: + * [PlainText]: "Hello" + * [Image]: "\[图片\]" + * [PokeMessage]: "\[戳一戳\]" + * [MessageChain]: 无间隔地连接所有元素 (`joinToString("", transformer=Message::contentToString)`) */ @SinceMirai("0.34.0") fun contentToString(): String @@ -234,25 +237,39 @@ inline operator fun Message.times(count: Int): MessageChain = this.repeat(count) @Suppress("OverridingDeprecatedMember") interface SingleMessage : Message, CharSequence, Comparable { - /** - * 即 `sub in this.contentToString()` - */ + @PlannedRemoval("1.0.0") + @JvmSynthetic + @Deprecated( + "有歧义, 自行使用 contentToString() 比较", + ReplaceWith("this.contentToString() == other"), + DeprecationLevel.ERROR + ) /* final */ override operator fun contains(sub: String): Boolean = sub in this.contentToString() - /** - * 比较两个消息的 [contentToString] - */ + @PlannedRemoval("1.0.0") + @JvmSynthetic + @Deprecated( + "有歧义, 自行使用 contentToString() 比较", + ReplaceWith("this.contentToString() == other"), + DeprecationLevel.ERROR + ) /* final */ override infix fun eq(other: Message): Boolean = this.contentToString() == other.contentToString() - /** - * 将 [contentToString] 与 [other] 比较 - */ + @PlannedRemoval("1.0.0") + @JvmSynthetic + @Deprecated( + "有歧义, 自行使用 contentToString() 比较", + ReplaceWith("this.contentToString() == other"), + DeprecationLevel.ERROR + ) /* final */ override infix fun eq(other: String): Boolean = this.contentToString() == other } /** * 消息元数据, 即不含内容的元素. * 包括: [MessageSource] + * + * @see ConstrainSingle 约束一个 [MessageChain] 中只存在这一种类型的元素 */ interface MessageMetadata : SingleMessage { override val length: Int get() = 0 diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/impl.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/impl.kt index b1414407b..230d3a2d3 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/impl.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/impl.kt @@ -262,3 +262,80 @@ internal class SingleMessageChainImpl constructor( override fun iterator(): Iterator = iterator { yield(delegate) } } + +////////////////////// +// region Image impl +////////////////////// + + +internal val EMPTY_BYTE_ARRAY = ByteArray(0) + + +// /000000000-3814297509-BFB7027B9354B8F899A062061D74E206 +private val FRIEND_IMAGE_ID_REGEX_1 = Regex("""/[0-9]*-[0-9]*-[0-9a-zA-Z]{32}""") + +// /f8f1ab55-bf8e-4236-b55e-955848d7069f +private val FRIEND_IMAGE_ID_REGEX_2 = Regex("""/.{8}-(.{4}-){3}.{12}""") + +// {01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.png +private val GROUP_IMAGE_ID_REGEX = Regex("""\{.{8}-(.{4}-){3}.{12}}\..*""") + +@Suppress("NOTHING_TO_INLINE") // no stack waste +internal inline fun Char.hexDigitToByte(): Int { + return when (this) { + in '0'..'9' -> this - '0' + in 'A'..'F' -> 10 + (this - 'A') + in 'a'..'f' -> 10 + (this - 'a') + else -> throw IllegalArgumentException("illegal hex digit: $this") + } +} + +internal fun String.skipToSecondHyphen(): Int { + var count = 0 + this.forEachIndexed { index, c -> + if (c == '-' && ++count == 2) return index + } + error("cannot find two hyphens") +} + +internal fun String.imageIdToMd5(offset: Int): ByteArray { + val result = ByteArray(16) + var cur = 0 + var hasCurrent = false + var lastChar: Char = 0.toChar() + for (index in offset..this.lastIndex) { + val char = this[index] + if (char == '-') continue + if (hasCurrent) { + result[cur++] = (lastChar.hexDigitToByte().shl(4) or char.hexDigitToByte()).toByte() + if (cur == 16) return result + hasCurrent = false + } else { + lastChar = char + hasCurrent = true + } + } + error("No enough chars") +} + +@OptIn(ExperimentalStdlibApi::class) +internal fun calculateImageMd5ByImageId(imageId: String): ByteArray { + return when { + imageId.matches(FRIEND_IMAGE_ID_REGEX_1) -> imageId.imageIdToMd5(imageId.skipToSecondHyphen() + 1) + imageId.matches(FRIEND_IMAGE_ID_REGEX_2) -> + imageId.imageIdToMd5(1) + imageId.matches(GROUP_IMAGE_ID_REGEX) -> { + imageId.imageIdToMd5(1) + } + else -> error( + "illegal imageId: $imageId. $ILLEGAL_IMAGE_ID_EXCEPTION_MESSAGE" + ) + } +} + +internal val ILLEGAL_IMAGE_ID_EXCEPTION_MESSAGE = + "ImageId must matches Regex `${FRIEND_IMAGE_ID_REGEX_1.pattern}`, " + + "`${FRIEND_IMAGE_ID_REGEX_2.pattern}` or " + + "`${GROUP_IMAGE_ID_REGEX.pattern}`" + +// endregion \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/ExternalImage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/ExternalImage.kt index cc183a496..d0dbdd67c 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/ExternalImage.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/ExternalImage.kt @@ -133,7 +133,7 @@ class ExternalImage private constructor( * PNG: 1001 * WEBP: 1002 * BMP: 1005 - * GIG: 2000 + * GIG: 2000 // TODO gig? gif? * APNG: 2001 * SHARPP: 1004 */ @@ -148,7 +148,7 @@ class ExternalImage private constructor( } /** - * 将图片发送给指定联系人 + * 将图片作为单独的消息发送给指定联系人 */ @JvmSynthetic suspend fun ExternalImage.sendTo(contact: C): MessageReceipt = when (contact) { @@ -171,7 +171,7 @@ suspend fun ExternalImage.upload(contact: Contact): OfflineImage = when (contact } /** - * 将图片发送给 [this] + * 将图片作为单独的消息发送给 [this] */ @JvmSynthetic suspend inline fun C.sendImage(image: ExternalImage): MessageReceipt = image.sendTo(this) diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/message/data/Image.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/message/data/Image.kt new file mode 100644 index 000000000..d4c422137 --- /dev/null +++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/message/data/Image.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2020 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/master/LICENSE + */ + +@file:JvmMultifileClass +@file:JvmName("MessageUtils") + +package net.mamoe.mirai.message.data + +import kotlinx.io.core.Input +import net.mamoe.mirai.contact.Contact +import java.io.File +import java.io.InputStream +import java.net.URL + +/** + * 自定义表情 (收藏的表情) 和普通图片. + * + * ### 上传和发送图片 + * @see Contact.uploadImage 上传图片并得到 [Image] 消息 + * @see Contact.sendImage 上传并发送单个图片作为一条消息 + * @see Image.sendTo 上传图片并得到 [Image] 消息 + * + * @see File.uploadAsImage + * @see InputStream.uploadAsImage + * @see Input.uploadAsImage + * @see URL.uploadAsImage + * + * @see File.sendAsImageTo + * @see InputStream.sendAsImageTo + * @see Input.sendAsImageTo + * @see URL.sendAsImageTo + * + * + * + * @see FlashImage 闪照 + * @see Image.flash 转换普通图片为闪照 + */ +actual interface Image : Message, MessageContent { + actual companion object Key : Message.Key { + actual override val typeName: String + get() = "Image" + } + + /** + * 图片的 id. + * 图片 id 不一定会长时间保存, 因此不建议使用 id 发送图片. + * 图片 id 主要根据图片文件 md5 计算得到. + * + * 示例: + * 好友图片的 id: `/f8f1ab55-bf8e-4236-b55e-955848d7069f` 或 `/000000000-3814297509-BFB7027B9354B8F899A062061D74E206` + * 群图片的 id: `{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.png` + * + * @see Image 使用 id 构造图片 + */ + actual val imageId: String +} \ No newline at end of file