From 077885465bfaa58248771fc258315da386836c50 Mon Sep 17 00:00:00 2001 From: Him188 Date: Fri, 24 Apr 2020 15:12:50 +0800 Subject: [PATCH] Add docs, rearrange implementations --- .../mirai/qqandroid/message/imagesImpl.kt | 2 +- .../kotlin/net.mamoe.mirai/contact/Contact.kt | 10 +- .../net.mamoe.mirai/message/data/Image.kt | 98 +++++++++++++++---- .../net.mamoe.mirai/message/data/Message.kt | 43 +++----- .../message/data/MessageChain.kt | 9 +- .../net.mamoe.mirai/message/data/impl.kt | 40 ++++++-- .../net/mamoe/mirai/message/data/Image.kt | 35 +++++-- 7 files changed, 164 insertions(+), 73 deletions(-) diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/imagesImpl.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/imagesImpl.kt index 5c8424e52..984320455 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/imagesImpl.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/imagesImpl.kt @@ -34,9 +34,9 @@ internal class OnlineFriendImageImpl( internal val delegate: ImMsgBody.NotOnlineImage ) : OnlineFriendImage() { override val imageId: String get() = delegate.resId - override val original: Int get() = delegate.original override val originUrl: String get() = "http://c2cpicdw.qpic.cn" + this.delegate.origUrl + // TODO: 2020/4/24 动态获取图片下载链接的 host override fun equals(other: Any?): Boolean { return other is OnlineFriendImageImpl && other.imageId == this.imageId 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 e407ce616..50ec3e43a 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 @@ -72,13 +72,13 @@ abstract class Contact : CoroutineScope, ContactJavaFriendlyAPI(), ContactOrBot /** * 上传一个图片以备发送. * - * @see Image 查看更多信息 + * @see Image 查看有关图片的更多信息 * - * @see BeforeImageUploadEvent 图片发送前事件, cancellable - * @see ImageUploadEvent 图片发送完成事件 + * @see BeforeImageUploadEvent 图片发送前事件, 可拦截. + * @see ImageUploadEvent 图片发送完成事件, 不可拦截. * - * @throws EventCancelledException 当发送消息事件被取消 - * @throws OverFileSizeMaxException 当图片文件过大而被服务器拒绝上传时. (最大大小约为 20 MB) + * @throws EventCancelledException 当发送消息事件被取消时抛出 + * @throws OverFileSizeMaxException 当图片文件过大而被服务器拒绝上传时抛出. (最大大小约为 20 MB, 但 mirai 限制的大小为 30 MB) */ @JvmSynthetic abstract suspend fun uploadImage(image: ExternalImage): OfflineImage 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 d8cc19237..a9bbbec3e 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 @@ -19,6 +19,7 @@ import net.mamoe.mirai.Bot import net.mamoe.mirai.BotImpl import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.contact.Group +import net.mamoe.mirai.utils.ExternalImage import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.PlannedRemoval import net.mamoe.mirai.utils.SinceMirai @@ -28,9 +29,23 @@ import kotlin.jvm.JvmName import kotlin.jvm.JvmSynthetic /** - * 自定义表情 (收藏的表情), 图片 + * 自定义表情 (收藏的表情) 和普通图片. * - * 查看平台 actual 定义以获取更多说明. + * + * 最推荐的存储方式是存储图片原文件, 每次发送图片时都使用文件上传. + * 在上传时服务器会根据其缓存情况回复已有的图片 ID 或要求客户端上传. 详见 [Contact.uploadImage] + * + * + * ### [toString] 和 [contentToString] + * - [toString] 固定返回 `[mirai:image:]` 格式字符串, 其中 `` 代表 [imageId]. + * - [contentToString] 固定返回 `"[图片]"` + * + * ### 上传和发送图片 + * @see Contact.uploadImage 上传 [图片文件][ExternalImage] 并得到 [Image] 消息 + * @see Contact.sendImage 上传 [图片文件][ExternalImage] 并发送返回的 [Image] 作为一条消息 + * @see Image.sendTo 上传图片并得到 [Image] 消息 + * + * 查看平台 `actual` 定义以获取上传方式扩展. * * @see FlashImage 闪照 * @see Image.flash 转换普通图片为闪照 @@ -42,15 +57,33 @@ expect interface Image : Message, MessageContent { /** * 图片的 id. - * 图片 id 不一定会长时间保存, 因此不建议使用 id 发送图片. * - * 示例: - * 好友图片的 id: `/f8f1ab55-bf8e-4236-b55e-955848d7069f` 或 `/000000000-3814297509-BFB7027B9354B8F899A062061D74E206` - * 群图片的 id: `{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.png` + * 图片 id 不一定会长时间保存, 也可能在将来改变格式, 因此不建议使用 id 发送图片. + * + * ### 格式 + * 群图片: + * - [GROUP_IMAGE_ID_REGEX], 示例: `{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.mirai` (后缀一定为 ".mirai") + * + * 好友图片: + * - [FRIEND_IMAGE_ID_REGEX_1], 示例: `/f8f1ab55-bf8e-4236-b55e-955848d7069f` + * - [FRIEND_IMAGE_ID_REGEX_2], 示例: `/000000000-3814297509-BFB7027B9354B8F899A062061D74E206` * * @see Image 使用 id 构造图片 */ val imageId: String + + /* 实现: + final override fun toString(): String = _stringValue!! + + final override fun contentToString(): String = "[图片]" + */ + + @Deprecated(""" + 不要自行实现 OnlineGroupImage, 它必须由协议模块实现, 否则会无法发送也无法解析. + """, level = DeprecationLevel.HIDDEN) + @Suppress("PropertyName", "DeprecatedCallableAddReplaceWith") + @get:JvmSynthetic + val DoNotImplementThisClass: Nothing? } /** @@ -120,15 +153,29 @@ fun Image(imageId: String): OfflineImage = when { @JvmName("newImage") fun Image2(imageId: String): Image = Image(imageId) -@MiraiInternalAPI("使用 Image") + +/** + * 所有 [Image] 实现的基类. + */ +@Deprecated( + "This is internal API. Use Image instead", + level = DeprecationLevel.HIDDEN, // so that others can't see this class + replaceWith = ReplaceWith("Image") +) +@MiraiInternalAPI("Use Image instead") sealed class AbstractImage : Image { + @Deprecated(""" + 不要自行实现 OnlineGroupImage, 它必须由协议模块实现, 否则会无法发送也无法解析. + """, level = DeprecationLevel.HIDDEN) + @Suppress("PropertyName", "DeprecatedCallableAddReplaceWith") + @get:JvmSynthetic + final override val DoNotImplementThisClass: Nothing? + get() = error("stub") private var _stringValue: String? = null - get() { - return field ?: kotlin.run { - field = "[mirai:image:$imageId]" - field - } + get() = field ?: kotlin.run { + field = "[mirai:image:$imageId]" + field } override val length: Int get() = _stringValue!!.length override fun get(index: Int): Char = _stringValue!![index] @@ -205,8 +252,9 @@ suspend fun OfflineImage.queryUrl(): String { /** * 群图片. * - * [imageId] 形如 `{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.png` (42 长度) + * [imageId] 形如 `{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.mirai` (45 长度) */ +@Suppress("DEPRECATION_ERROR") // CustomFace @OptIn(MiraiInternalAPI::class) sealed class GroupImage : AbstractImage() { @@ -217,11 +265,20 @@ sealed class GroupImage : AbstractImage() { /** * 通过 [Group.uploadImage] 上传得到的 [GroupImage]. 它的链接需要查询 [Bot.queryImageUrl] + * + * @param imageId 参考 [Image.imageId] */ @Serializable data class OfflineGroupImage( override val imageId: String -) : GroupImage(), OfflineImage +) : GroupImage(), OfflineImage { + init { + @Suppress("DEPRECATION") + require(imageId matches GROUP_IMAGE_ID_REGEX || imageId matches GROUP_IMAGE_ID_REGEX_OLD) { + "Illegal imageId. It must matches GROUP_IMAGE_ID_REGEX" + } + } +} @get:JvmName("calculateImageMd5") @SinceMirai("0.39.0") @@ -244,22 +301,29 @@ abstract class OnlineGroupImage : GroupImage(), OnlineImage * * [imageId] 形如 `/f8f1ab55-bf8e-4236-b55e-955848d7069f` (37 长度) 或 `/000000000-3814297509-BFB7027B9354B8F899A062061D74E206` (54 长度) */ // NotOnlineImage +@Suppress("DEPRECATION_ERROR") @OptIn(MiraiInternalAPI::class) sealed class FriendImage : AbstractImage() { companion object Key : Message.Key { override val typeName: String get() = "FriendImage" } - - open val original: Int get() = 1 } /** * 通过 [Group.uploadImage] 上传得到的 [GroupImage]. 它的链接需要查询 [Bot.queryImageUrl] + * + * @param imageId 参考 [Image.imageId] */ @Serializable data class OfflineFriendImage( override val imageId: String -) : FriendImage(), OfflineImage +) : FriendImage(), OfflineImage { + init { + require(imageId matches FRIEND_IMAGE_ID_REGEX_1 || imageId matches FRIEND_IMAGE_ID_REGEX_2) { + "Illegal imageId. It must matches either FRIEND_IMAGE_ID_REGEX_1 or FRIEND_IMAGE_ID_REGEX_2" + } + } +} /** * 接收消息时获取到的 [FriendImage]. 它可以直接获取下载链接 [originUrl] 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 97fa61bfb..b0bbc6b29 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 @@ -13,6 +13,7 @@ package net.mamoe.mirai.message.data import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.message.MessageReceipt +import net.mamoe.mirai.message.data.Message.Key import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.PlannedRemoval import net.mamoe.mirai.utils.SinceMirai @@ -50,6 +51,9 @@ import kotlin.jvm.JvmSynthetic * * 但注意: 不能 `String + Message`. 只能 `Message + String` * + * #### 实现规范 + * 除 [MessageChain] 外, 所有 [Message] 的实现类都有伴生对象实现 [Key] 接口. + * * @see PlainText 纯文本 * @see Image 图片 * @see Face 原生表情 @@ -68,10 +72,14 @@ import kotlin.jvm.JvmSynthetic @OptIn(MiraiInternalAPI::class) interface Message { /** - * 类型 Key. + * 类型 Key. 由伴生对象实现, 表示一个 [Message] 对象的类型. + * * 除 [MessageChain] 外, 每个 [Message] 类型都拥有一个`伴生对象`(companion object) 来持有一个 Key * 在 [MessageChain.get] 时将会使用到这个 Key 进行判断类型. * + * #### 用例 + * [MessageChain.get]: 允许使用数组访问操作符获取指定类型的消息元素 ```val image: Image = chain[Image]``` + * * @param M 指代持有这个 Key 的消息类型 */ interface Key { @@ -83,21 +91,23 @@ interface Message { } /** - * 把 `this` 连接到 [tail] 的头部. 类似于字符串相加. + * 将 `this` 和 [tail] 连接. * * 连接后可以保证 [ConstrainSingle] 的元素单独存在. * * 例: - * ```kotlin + * ``` * val a = PlainText("Hello ") * val b = PlainText("world!") - * val c: CombinedMessage = a + b + * val c: MessageChain = a + b * println(c) // "Hello world!" * * val d = PlainText("world!") * val e = c + d; // PlainText + CombinedMessage * println(c) // "Hello world!" * ``` + * + * @see plus `+` 操作符重载 */ @SinceMirai("0.34.0") @JvmSynthetic // in java they should use `plus` instead @@ -144,30 +154,7 @@ interface Message { * @sample net.mamoe.mirai.message.data.ContentEqualsTest */ @SinceMirai("0.38.0") - fun contentEquals(another: Message, ignoreCase: Boolean = false): Boolean { - if (!this.contentToString().equals(another.contentToString(), ignoreCase = ignoreCase)) return false - return when { - this is SingleMessage && another is SingleMessage -> true - this is SingleMessage && another is MessageChain -> another.all { it is MessageMetadata || it is PlainText } - this is MessageChain && another is SingleMessage -> this.all { it is MessageMetadata || it is PlainText } - this is MessageChain && another is MessageChain -> { - val anotherIterator = another.iterator() - - /** - * 逐个判断非 [PlainText] 的 [Message] 是否 [equals] - */ - this.forEachContent { thisElement -> - if (thisElement.isPlain()) return@forEachContent - for (it in anotherIterator) { - if (it.isPlain() || it !is MessageContent) continue - if (thisElement != it) return false - } - } - return true - } - else -> error("shouldn't be reached") - } - } + fun contentEquals(another: Message, ignoreCase: Boolean = false): Boolean = contentEqualsImpl(another, ignoreCase) /** * 判断内容是否与 [another] 相等. diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt index 69c52dd3b..dc2eb87fc 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt @@ -117,12 +117,13 @@ interface MessageChain : Message, Iterable { /** * 遍历每一个 [消息内容][MessageContent] */ +@SinceMirai("0.39.0") @JvmSynthetic inline fun MessageChain.forEachContent(block: (MessageContent) -> Unit) { - this.forEach { - if (it !is MessageMetadata) { - check(it is MessageContent) { "internal error: Message must be either MessageMetaData or MessageContent" } - block(it) + for (element in this) { + if (element !is MessageMetadata) { + check(element is MessageContent) { "internal error: Message must be either MessageMetaData or MessageContent" } + block(element) } } } 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 77beea543..620a7b5c4 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 @@ -18,6 +18,7 @@ import net.mamoe.mirai.utils.MiraiInternalAPI import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName import kotlin.jvm.JvmSynthetic +import kotlin.native.concurrent.SharedImmutable ///////////////////////// //// IMPLEMENTATIONS //// @@ -43,6 +44,32 @@ internal fun Message.followedByInternalForBinaryCompatibility(tail: Message): Co return CombinedMessage(EmptyMessageChain, this.followedBy(tail)) } +@JvmSynthetic +internal fun Message.contentEqualsImpl(another: Message, ignoreCase: Boolean): Boolean { + if (!this.contentToString().equals(another.contentToString(), ignoreCase = ignoreCase)) return false + return when { + this is SingleMessage && another is SingleMessage -> true + this is SingleMessage && another is MessageChain -> another.all { it is MessageMetadata || it is PlainText } + this is MessageChain && another is SingleMessage -> this.all { it is MessageMetadata || it is PlainText } + this is MessageChain && another is MessageChain -> { + val anotherIterator = another.iterator() + + /** + * 逐个判断非 [PlainText] 的 [Message] 是否 [equals] + */ + this.forEachContent { thisElement -> + if (thisElement.isPlain()) return@forEachContent + for (it in anotherIterator) { + if (it.isPlain() || it !is MessageContent) continue + if (thisElement != it) return false + } + } + return true + } + else -> error("shouldn't be reached") + } +} + @JvmSynthetic internal fun Message.followedByImpl(tail: Message): MessageChain { when { @@ -283,18 +310,10 @@ internal class SingleMessageChainImpl constructor( ////////////////////// +@SharedImmutable 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) { @@ -335,11 +354,12 @@ internal fun String.imageIdToMd5(offset: Int): ByteArray { @OptIn(ExperimentalStdlibApi::class) internal fun calculateImageMd5ByImageId(imageId: String): ByteArray { + @Suppress("DEPRECATION") 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.matches(GROUP_IMAGE_ID_REGEX) || imageId.matches(GROUP_IMAGE_ID_REGEX_OLD) -> { imageId.imageIdToMd5(1) } else -> error( 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 index 89aa9ac98..78f5f49f4 100644 --- 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 @@ -14,6 +14,7 @@ package net.mamoe.mirai.message.data import kotlinx.io.core.Input import net.mamoe.mirai.contact.Contact +import net.mamoe.mirai.utils.ExternalImage import java.io.File import java.io.InputStream import java.net.URL @@ -21,10 +22,15 @@ import java.net.URL /** * 自定义表情 (收藏的表情) 和普通图片. * + * + * 最推荐的存储方式是存储图片原文件, 每次发送图片时都使用文件上传. + * 在上传时服务器会根据其缓存情况回复已有的图片 ID 或要求客户端上传. 详见 [Contact.uploadImage] + * + * * ### 上传和发送图片 - * @see Contact.uploadImage 上传图片并得到 [Image] 消息 - * @see Contact.sendImage 上传并发送单个图片作为一条消息 - * @see Image.sendTo 上传图片并得到 [Image] 消息 + * @see Contact.uploadImage 上传 [图片文件][ExternalImage] 并得到 [Image] 消息 + * @see Contact.sendImage 上传 [图片文件][ExternalImage] 并发送返回的 [Image] 作为一条消息 + * @see Image.sendTo 上传 [图片文件][ExternalImage] 并得到 [Image] 消息 * * @see File.uploadAsImage * @see InputStream.uploadAsImage @@ -46,16 +52,29 @@ actual interface Image : Message, MessageContent { 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` + * 图片 id 不一定会长时间保存, 也可能在将来改变格式, 因此不建议使用 id 发送图片. + * + * ### 格式 + * 群图片: + * - [GROUP_IMAGE_ID_REGEX], 示例: `{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.mirai` (后缀一定为 ".mirai") + * + * 好友图片: + * - [FRIEND_IMAGE_ID_REGEX_1], 示例: `/f8f1ab55-bf8e-4236-b55e-955848d7069f` + * - [FRIEND_IMAGE_ID_REGEX_2], 示例: `/000000000-3814297509-BFB7027B9354B8F899A062061D74E206` * * @see Image 使用 id 构造图片 */ actual val imageId: String + + + @Deprecated(""" + 不要自行实现 OnlineGroupImage, 它必须由协议模块实现, 否则会无法发送也无法解析. + """, level = DeprecationLevel.HIDDEN) + @Suppress("PropertyName", "DeprecatedCallableAddReplaceWith") + @get:JvmSynthetic + actual val DoNotImplementThisClass: Nothing? } \ No newline at end of file