mirror of
https://github.com/mamoe/mirai.git
synced 2025-02-08 23:17:21 +08:00
Add docs, rearrange implementations
This commit is contained in:
parent
92a1b0d4df
commit
077885465b
@ -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
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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] 相等.
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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?
|
||||
}
|
Loading…
Reference in New Issue
Block a user