Add image caches; Fix deserialized image not rendering in ForwardMessage; fix #1507, fix #1636

This commit is contained in:
Karlatemp 2021-11-12 23:09:53 +08:00
parent b5d8c708bd
commit 2422aa34b5
No known key found for this signature in database
GPG Key ID: 21FBDDF664FF06F8
9 changed files with 443 additions and 62 deletions

View File

@ -9,8 +9,104 @@
package net.mamoe.mirai.utils
import java.util.concurrent.atomic.AtomicInteger
@TestOnly
public fun readResource(url: String): String =
Thread.currentThread().contextClassLoader.getResourceAsStream(url)?.readBytes()?.decodeToString()
?: error("Could not find resource '$url'")
public class ResourceAccessLock {
public companion object {
public const val LOCKED: Int = -2
public const val UNINITIALIZED: Int = -1
public const val INITIALIZED: Int = 0
}
/*
* status > 0 -> Number of holders using resource
*/
private val status = AtomicInteger(-1)
/**
* ```
* if (res.lock.tryToDispose()) {
* res.internal.close()
* }
* ```
*/
public fun tryDispose(): Boolean {
return status.compareAndSet(0, -1)
}
/**
* ```
* if (res.lock.tryInitialize()) {
* res.internalRes = download()
* }
* ```
*/
public fun tryInitialize(): Boolean {
return status.compareAndSet(-1, 0)
}
public fun tryUse(): Boolean {
val c = status
while (true) {
val v = c.get()
if (v < 0) return false
if (c.compareAndSet(v, v + 1)) return true
}
}
public fun lockIfNotUsing(): Boolean {
val count = this.status
while (true) {
val value = count.get()
if (value != 0) return false
if (count.compareAndSet(0, -2)) return true
}
}
public fun release() {
val count = this.status
while (true) {
val value = count.get()
if (value < 1) throw IllegalStateException("Current resource not in using")
if (count.compareAndSet(value, value - 1)) return
}
}
public fun unlock() {
status.compareAndSet(LOCKED, INITIALIZED)
}
public fun setInitialized() {
status.set(INITIALIZED)
}
public fun setLocked() {
status.set(LOCKED)
}
public fun setDisposed() {
setUninitialized()
}
public fun setUninitialized() {
status.set(UNINITIALIZED)
}
public fun currentStatus(): Int = status.get()
override fun toString(): String {
return when (val status = status.get()) {
0 -> "ResourceAccessLock(INITIALIZED)"
-1 -> "ResourceAccessLock(UNINITIALIZED)"
-2 -> "ResourceAccessLock(LOCKED)"
else -> "ResourceAccessLock($status)"
}
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright 2019-2021 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.utils
public fun <T : Any> unsafeMutableNonNullPropertyOf(
name: String = "<unknown>"
): UnsafeMutableNonNullProperty<T> {
return UnsafeMutableNonNullProperty(name)
}
@Suppress("NOTHING_TO_INLINE")
public class UnsafeMutableNonNullProperty<T : Any>(
private val propertyName: String = "<unknown>"
) {
@JvmField
public var value0: T? = null
public val isInitialized: Boolean get() = value0 !== null
public var value: T
get() = value0 ?: throw UninitializedPropertyAccessException("Property `$propertyName` not initialized")
set(value) {
value0 = value
}
public fun clear() {
value0 = null
}
public inline operator fun getValue(thiz: Any?, property: Any?): T = value
public inline operator fun setValue(thiz: Any?, property: Any?, value: T) {
value0 = value
}
override fun toString(): String {
return value0?.toString() ?: "<uninitialized>"
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright 2019-2021 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.utils
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails
import kotlin.test.assertFalse
import kotlin.test.assertTrue
internal class ResourceAccessLockTest {
@Test
fun testInitializedLockCannotReInit() {
val lock = ResourceAccessLock()
lock.setInitialized()
assertFalse { lock.tryInitialize() }
}
@Test
fun testUseFailedIfLockUninitializedOrLocked() {
val lock = ResourceAccessLock()
lock.setUninitialized()
assertFalse { lock.tryUse() }
lock.setLocked()
assertFalse { lock.tryUse() }
}
@Test
fun testLockFailedIfUninitialized() {
val lock = ResourceAccessLock()
lock.setUninitialized()
assertFalse { lock.lockIfNotUsing() }
}
@Test
fun testLockFailedIfUsing() {
val lock = ResourceAccessLock()
lock.setInitialized()
assertTrue { lock.tryUse() }
assertFalse { lock.lockIfNotUsing() }
}
@Test
fun testLockUsedIfInitialized() {
val lock = ResourceAccessLock()
lock.setInitialized()
assertTrue { lock.tryUse() }
}
@Test
fun testRelease() {
val lock = ResourceAccessLock()
lock.setInitialized()
assertFails { lock.release() }
assertEquals(ResourceAccessLock.INITIALIZED, lock.currentStatus())
assertTrue { lock.tryUse() }
lock.release()
}
}

View File

@ -57,6 +57,7 @@ import net.mamoe.mirai.internal.network.protocol.packet.sendAndExpect
import net.mamoe.mirai.internal.network.protocol.packet.summarycard.SummaryCard
import net.mamoe.mirai.internal.network.psKey
import net.mamoe.mirai.internal.network.sKey
import net.mamoe.mirai.internal.utils.ImagePatcher
import net.mamoe.mirai.internal.utils.MiraiProtocolInternal
import net.mamoe.mirai.internal.utils.crypto.TEA
import net.mamoe.mirai.internal.utils.io.serialization.loadAs
@ -762,7 +763,20 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
override fun createImage(imageId: String): Image {
return when {
imageId matches IMAGE_ID_REGEX -> OfflineGroupImage(imageId)
imageId matches IMAGE_ID_REGEX -> {
Bot.instancesSequence.forEach { existsBot ->
runCatching {
val patcher = existsBot.asQQAndroidBot().components[ImagePatcher]
patcher.findCacheByImageId(imageId)?.let { cache ->
val rsp = cache.cacheOGI.value0
cache.accessLock.release()
if (rsp != null) return rsp
}
}
}
OfflineGroupImage(imageId)
}
imageId matches IMAGE_RESOURCE_ID_REGEX_1 -> OfflineFriendImage(imageId)
imageId matches IMAGE_RESOURCE_ID_REGEX_2 -> OfflineFriendImage(imageId)
else ->

View File

@ -49,6 +49,7 @@ import net.mamoe.mirai.internal.network.notice.priv.FriendNoticeProcessor
import net.mamoe.mirai.internal.network.notice.priv.OtherClientNoticeProcessor
import net.mamoe.mirai.internal.network.notice.priv.PrivateMessageProcessor
import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc
import net.mamoe.mirai.internal.utils.ImagePatcher
import net.mamoe.mirai.internal.utils.subLogger
import net.mamoe.mirai.utils.BotConfiguration
import net.mamoe.mirai.utils.MiraiLogger
@ -223,6 +224,7 @@ internal open class QQAndroidBot constructor(
AccountSecretsManager,
configuration.createAccountsSecretsManager(bot.logger.subLogger("AccountSecretsManager")),
)
set(ImagePatcher, ImagePatcher())
}
/**

View File

@ -44,6 +44,7 @@ import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.voiceCodec
import net.mamoe.mirai.internal.network.protocol.packet.list.ProfileService
import net.mamoe.mirai.internal.network.protocol.packet.sendAndExpect
import net.mamoe.mirai.internal.utils.GroupPkgMsgParsingCache
import net.mamoe.mirai.internal.utils.ImagePatcher
import net.mamoe.mirai.internal.utils.RemoteFileImpl
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
import net.mamoe.mirai.internal.utils.subLogger
@ -181,6 +182,12 @@ internal class GroupImpl constructor(
if (BeforeImageUploadEvent(this, resource).broadcast().isCancelled) {
throw EventCancelledException("cancelled by BeforeImageUploadEvent.ToGroup")
}
fun OfflineGroupImage.putIntoCache() {
// We can't understand wny Image(group.uploadImage().imageId)
bot.components[ImagePatcher].putCache(this)
}
val imageInfo = runBIO { resource.calculateImageInfo() }
bot.network.run<NetworkHandler, Image> {
val response: ImgStore.GroupPicUp.Response = ImgStore.GroupPicUp(
@ -216,6 +223,7 @@ internal class GroupImpl constructor(
.also {
it.fileId = response.fileId.toInt()
}
.also { it.putIntoCache() }
.also { ImageUploadEvent.Succeed(this@GroupImpl, resource, it).broadcast() }
}
is ImgStore.GroupPicUp.Response.RequireUpload -> {
@ -244,6 +252,7 @@ internal class GroupImpl constructor(
size = resource.size
)
}.also { it.fileId = response.fileId.toInt() }
.also { it.putIntoCache() }
.also { ImageUploadEvent.Succeed(this@GroupImpl, resource, it).broadcast() }
}
}

View File

@ -28,9 +28,8 @@ import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
import net.mamoe.mirai.internal.network.protocol.packet.chat.FileManagement
import net.mamoe.mirai.internal.network.protocol.packet.chat.MusicSharePacket
import net.mamoe.mirai.internal.network.protocol.packet.chat.image.ImgStore
import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.*
import net.mamoe.mirai.internal.network.protocol.packet.sendAndExpect
import net.mamoe.mirai.internal.utils.ImagePatcher
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.castOrNull
@ -141,10 +140,20 @@ internal abstract class SendMessageHandler<C : Contact> {
if (resp is MessageSvcPbSendMsg.Response.MessageTooLarge) {
return when (step) {
SendMessageStep.FIRST -> {
sendMessageImpl(originalMessage, transformedMessage, isMiraiInternal, SendMessageStep.LONG_MESSAGE)
sendMessageImpl(
originalMessage,
transformedMessage,
isMiraiInternal,
SendMessageStep.LONG_MESSAGE,
)
}
SendMessageStep.LONG_MESSAGE -> {
sendMessageImpl(originalMessage, transformedMessage, isMiraiInternal, SendMessageStep.FRAGMENTED)
sendMessageImpl(
originalMessage,
transformedMessage,
isMiraiInternal,
SendMessageStep.FRAGMENTED,
)
}
else -> {
@ -157,7 +166,7 @@ internal abstract class SendMessageHandler<C : Contact> {
}
}
}
if (resp is MessageSvcPbSendMsg.Response.ServiceUnavailable){
if (resp is MessageSvcPbSendMsg.Response.ServiceUnavailable) {
throw IllegalStateException("Send message to $contact failed, server service is unavailable.")
}
if (resp is MessageSvcPbSendMsg.Response.Failed) {
@ -446,65 +455,11 @@ internal open class GroupSendMessageHandler(
companion object {
private suspend fun GroupImpl.fixImageFileId(image: OfflineGroupImage) {
if (image.fileId == null) {
val response: ImgStore.GroupPicUp.Response = ImgStore.GroupPicUp(
bot.client,
uin = bot.id,
groupCode = this.id,
md5 = image.md5,
size = 1,
).sendAndExpect(bot)
when (response) {
is ImgStore.GroupPicUp.Response.Failed -> {
image.fileId = 0 // Failed
}
is ImgStore.GroupPicUp.Response.FileExists -> {
image.fileId = response.fileId.toInt()
}
is ImgStore.GroupPicUp.Response.RequireUpload -> {
image.fileId = response.fileId.toInt()
}
}
}
bot.components[ImagePatcher].patchOfflineGroupImage(this, image)
}
/**
* Ensures server holds the cache
*/
private suspend fun GroupImpl.updateFriendImageForGroupMessage(image: FriendImage): OfflineGroupImage {
bot.network.run {
val response = ImgStore.GroupPicUp(
bot.client,
uin = bot.id,
groupCode = id,
md5 = image.md5,
size = image.size
).sendAndExpect()
return OfflineGroupImage(
imageId = image.imageId,
width = image.width,
height = image.height,
size = if (response is ImgStore.GroupPicUp.Response.FileExists) {
response.fileInfo.fileSize
} else {
image.size
},
imageType = image.imageType
).also { img ->
when (response) {
is ImgStore.GroupPicUp.Response.FileExists -> {
img.fileId = response.fileId.toInt()
}
is ImgStore.GroupPicUp.Response.RequireUpload -> {
img.fileId = response.fileId.toInt()
}
is ImgStore.GroupPicUp.Response.Failed -> {
img.fileId = 0
}
}
}
}
return bot.components[ImagePatcher].patchFriendImageToGroupImage(this, image)
}
}
}

View File

@ -125,6 +125,8 @@ internal open class MultiMsgUploader(
msgChain = convertNestedForwardMessage(nestedForward, msgChain)
}
msgChain = handler.conversionMessageChain(msgChain)
var seq: Int = -1
var uid: Int = -1
msg.messageChain.sourceOrNull?.let { source ->

View File

@ -0,0 +1,193 @@
/*
* Copyright 2019-2021 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.utils
import net.mamoe.mirai.internal.contact.GroupImpl
import net.mamoe.mirai.internal.message.OfflineGroupImage
import net.mamoe.mirai.internal.network.component.ComponentKey
import net.mamoe.mirai.internal.network.protocol.packet.chat.image.ImgStore
import net.mamoe.mirai.internal.network.protocol.packet.sendAndExpect
import net.mamoe.mirai.message.data.FriendImage
import net.mamoe.mirai.message.data.md5
import net.mamoe.mirai.utils.ResourceAccessLock
import net.mamoe.mirai.utils.UnsafeMutableNonNullProperty
import net.mamoe.mirai.utils.currentTimeMillis
import net.mamoe.mirai.utils.unsafeMutableNonNullPropertyOf
internal open class ImagePatcher {
companion object : ComponentKey<ImagePatcher> {
inline fun <T> ImageCache.withCache(action: (ImageCache) -> T): T {
return try {
action(this)
} finally {
this.accessLock.release()
}
}
}
data class ImageCache(
var updateTime: Long = 0,
val id: UnsafeMutableNonNullProperty<String> = unsafeMutableNonNullPropertyOf(),
// OGI: OfflineGroupImage
val cacheOGI: UnsafeMutableNonNullProperty<OfflineGroupImage> = unsafeMutableNonNullPropertyOf(),
val accessLock: ResourceAccessLock = ResourceAccessLock(),
)
val caches: Array<ImageCache> = Array(20) { ImageCache() }
fun findCache(id: String): ImageCache? {
return caches.firstOrNull { it.id.value0 == id && it.accessLock.tryUse() }
}
fun findCacheByImageId(id: String): ImageCache? = findCache(calcInternalIdByImageId(id))
fun putCache(image: OfflineGroupImage) {
putCache(calcInternalIdByImageId(image.imageId)).cacheOGI.value0 = image
}
fun putCache(id: String): ImageCache {
fun ImageCache.postReturn(): ImageCache = also { cache ->
cache.updateTime = currentTimeMillis()
cache.id.value0 = id
}
caches.forEach { exists ->
if (exists.id.value0 == id && exists.accessLock.tryInitialize()) {
return exists.postReturn()
}
}
// Try to use existing slot
caches.forEach { exists ->
if (exists.accessLock.tryInitialize()) {
return exists.postReturn()
}
}
val availableCaches = caches.filter { it.accessLock.lockIfNotUsing() }
if (availableCaches.isNotEmpty()) {
val target = availableCaches.minByOrNull { it.updateTime }!!
availableCaches.forEach { if (it !== target) it.accessLock.unlock() }
target.accessLock.setInitialized()
return target.postReturn()
}
// No available sort. Force to override the last one
val newCache = ImageCache()
newCache.accessLock.setInitialized()
newCache.postReturn()
var idx = 0
var lupd = Long.MAX_VALUE
caches.forEachIndexed { index, imageCache ->
val upd0 = imageCache.updateTime
if (upd0 < lupd) {
lupd = upd0
idx = index
}
}
caches[idx] = newCache
return newCache
}
private fun calcInternalIdByImageId(imageId: String): String {
return imageId.substring(1, imageId.indexOf('}'))
}
suspend fun patchOfflineGroupImage(
group: GroupImpl,
image: OfflineGroupImage,
) {
if (image.fileId != null) return
val iid = calcInternalIdByImageId(image.imageId)
findCache(iid)?.withCache { cache ->
cache.cacheOGI.value0?.let { cachedOGI ->
image.fileId = cachedOGI.fileId
return
}
}
val bot = group.bot
val response: ImgStore.GroupPicUp.Response = ImgStore.GroupPicUp(
bot.client,
uin = bot.id,
groupCode = group.id,
md5 = image.md5,
size = 1,
).sendAndExpect(bot)
when (response) {
is ImgStore.GroupPicUp.Response.Failed -> {
image.fileId = 0 // Failed
}
is ImgStore.GroupPicUp.Response.FileExists -> {
image.fileId = response.fileId.toInt()
}
is ImgStore.GroupPicUp.Response.RequireUpload -> {
image.fileId = response.fileId.toInt()
}
}
putCache(iid).cacheOGI.value0 = image
}
/**
* Ensures server holds the cache
*/
suspend fun patchFriendImageToGroupImage(
group: GroupImpl,
image: FriendImage,
): OfflineGroupImage {
val iid = calcInternalIdByImageId(image.imageId)
findCache(iid)?.withCache { cache ->
cache.cacheOGI.value0?.let { return it }
}
val bot = group.bot
val response = ImgStore.GroupPicUp(
bot.client,
uin = bot.id,
groupCode = group.id,
md5 = image.md5,
size = image.size
).sendAndExpect(bot.network)
return OfflineGroupImage(
imageId = image.imageId,
width = image.width,
height = image.height,
size = if (response is ImgStore.GroupPicUp.Response.FileExists) {
response.fileInfo.fileSize
} else {
image.size
},
imageType = image.imageType
).also { img ->
when (response) {
is ImgStore.GroupPicUp.Response.FileExists -> {
img.fileId = response.fileId.toInt()
}
is ImgStore.GroupPicUp.Response.RequireUpload -> {
img.fileId = response.fileId.toInt()
}
is ImgStore.GroupPicUp.Response.Failed -> {
img.fileId = 0
}
}
}.also { img ->
putCache(iid).cacheOGI.value0 = img
}
}
}