Add docs, rearrange implementations

This commit is contained in:
Him188 2020-04-24 15:12:50 +08:00
parent 92a1b0d4df
commit 077885465b
7 changed files with 164 additions and 73 deletions

View File

@ -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

View File

@ -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

View File

@ -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:<ID>]` 格式字符串, 其中 `<ID>` 代表 [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<FriendImage> {
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]

View File

@ -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<out M : Message> {
@ -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] 相等.

View File

@ -117,12 +117,13 @@ interface MessageChain : Message, Iterable<SingleMessage> {
/**
* 遍历每一个 [消息内容][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)
}
}
}

View File

@ -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(

View File

@ -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?
}