fix(ComponentsV2): refactor, prevent viewraw crash, and add cv2 tag

This commit is contained in:
LavaDesu 2025-08-07 02:39:27 +10:00
parent e7dd212cd1
commit 0dc4bf4286
Signed by: cilly
GPG key ID: 6500251E087653C9
3 changed files with 141 additions and 75 deletions

View file

@ -0,0 +1,137 @@
package com.aliucord.coreplugins
import android.annotation.SuppressLint
import android.view.View
import android.widget.TextView
import com.aliucord.Constants
import com.aliucord.Utils
import com.aliucord.api.PatcherAPI
import com.aliucord.coreplugins.componentsv2.ComponentV2Type
import com.aliucord.patcher.*
import com.aliucord.utils.GsonUtils
import com.aliucord.utils.GsonUtils.toJson
import com.aliucord.utils.ReflectUtils
import com.discord.api.botuikit.ComponentType
import com.discord.api.botuikit.gson.ComponentRuntimeTypeAdapter
import com.discord.api.botuikit.gson.ComponentTypeTypeAdapter
import com.discord.api.message.attachment.MessageAttachment
import com.discord.models.domain.Model
import com.discord.models.message.Message
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemMessage
import com.google.gson.stream.JsonReader
import java.io.File
import b.a.b.a as TypeAdapterRegistrar
import b.i.d.c as FieldNamingPolicy
import b.i.d.e as GsonBuilder
fun ComponentsV2.compat(patcher: PatcherAPI) {
// check for old cursed plugin, probably not needed anymore
val oldFile = File("${Constants.PLUGINS_PATH}/ComponentsV2-Beta.zip")
if (oldFile.exists()) {
logger.info("old plugin found, deleting and prompting restart")
oldFile.delete()
Utils.promptRestart()
return
}
// I'm sorry
// ViewRaw crashes without this
val cuteGson = GsonBuilder().run {
c = FieldNamingPolicy.m // LOWER_CASE_WITH_UNDERSCORES
TypeAdapterRegistrar.a(this)
e.add(Model.TypeAdapterFactory())
a().apply {
ReflectUtils.setField(this, "k", true)
}
}
patcher.patch(GsonUtils::class.java.getDeclaredMethod("toJsonPretty", Object::class.java))
{ (param, obj: Any) ->
if (obj is Message && obj.isComponentV2)
param.result = cuteGson.toJson(obj)
}
// add cv2 tag
patcher.after<WidgetChatListAdapterItemMessage>("configureItemTag", Message::class.java, Boolean::class.javaPrimitiveType!!)
{ (_, msg: Message) ->
val textView = ReflectUtils.getField(this, "itemTag") as TextView?
?: return@after
if (!msg.isComponentV2)
return@after
if (textView.text.isEmpty()) {
// this code path shouldn't really ever run (only bots can send cv2, and bots have the tag already)
// but idk maybe someone self-bots or something
textView.visibility = View.VISIBLE
@SuppressLint("SetTextI18n")
textView.text = "CV2"
textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
} else {
@SuppressLint("SetTextI18n")
textView.text = textView.text.toString() + " | CV2"
}
}
ComponentV2Type.make()
patchGson(patcher)
}
fun ComponentsV2.stopCompat() {
unpatchGson()
ComponentV2Type.unmake(logger)
}
private fun patchGson(patcher: PatcherAPI) {
val factory = ComponentRuntimeTypeAdapter.INSTANCE.a()
val typeToClass = factory.l
val classToType = factory.m
ComponentV2Type.newValues?.forEach {
typeToClass[it.type.toString()] = it.clazz
classToType[it.clazz] = it.type.toString()
}
patcher.instead<ComponentTypeTypeAdapter>("read", JsonReader::class.java)
{ (_, jsonReader: JsonReader) ->
val type: Int = b.c.a.a0.d.n1(jsonReader)
ComponentType.values().find { it.type == type } ?: ComponentType.UNKNOWN
}
}
private fun unpatchGson() {
val factory = ComponentRuntimeTypeAdapter.INSTANCE.a()
val typeToClass = factory.l
val classToType = factory.m
ComponentV2Type.newValues?.forEach {
typeToClass.remove(it.type.toString())
classToType.remove(it.clazz)
}
}
object CV2Compat {
/** Creates a new [MessageAttachment] */
fun createAttachment(
filename: String,
filesize: Long,
proxyUrl: String,
url: String,
width: Int,
height: Int,
): MessageAttachment {
val inst = ReflectUtils.allocateInstance(clazz)
filenameField.set(inst, filename)
filesizeField.set(inst, filesize)
proxyUrlField.set(inst, proxyUrl)
urlField.set(inst, url)
widthField.set(inst, width)
heightField.set(inst, height)
return inst
}
}
private val clazz = MessageAttachment::class.java
private val filenameField = clazz.getDeclaredField("filename").apply { isAccessible = true }
private val filesizeField = clazz.getDeclaredField("size").apply { isAccessible = true }
private val proxyUrlField = clazz.getDeclaredField("proxyUrl").apply { isAccessible = true }
private val urlField = clazz.getDeclaredField("url").apply { isAccessible = true }
private val widthField = clazz.getDeclaredField("width").apply { isAccessible = true }
private val heightField = clazz.getDeclaredField("height").apply { isAccessible = true }

View file

@ -4,7 +4,6 @@ import android.content.Context
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.constraintlayout.widget.ConstraintLayout
import com.aliucord.Constants
import com.aliucord.Utils
import com.aliucord.annotations.AliucordPlugin
import com.aliucord.coreplugins.componentsv2.ComponentV2Type
@ -13,11 +12,7 @@ import com.aliucord.coreplugins.componentsv2.patchMessageItems
import com.aliucord.coreplugins.componentsv2.views.*
import com.aliucord.entities.Plugin
import com.aliucord.patcher.*
import com.aliucord.utils.ReflectUtils
import com.discord.api.botuikit.*
import com.discord.api.botuikit.gson.ComponentRuntimeTypeAdapter
import com.discord.api.botuikit.gson.ComponentTypeTypeAdapter
import com.discord.api.message.attachment.MessageAttachment
import com.discord.models.botuikit.*
import com.discord.models.message.Message
import com.discord.stores.StoreApplicationInteractions.InteractionSendState
@ -29,57 +24,17 @@ import com.discord.widgets.botuikit.views.select.SelectComponentView
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapter
import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemBotComponentRow
import com.discord.widgets.chat.list.entries.BotUiComponentEntry
import com.google.gson.stream.JsonReader
import com.lytefast.flexinput.R
import de.robv.android.xposed.XposedBridge
import java.io.File
val Message.isComponentV2 get() = (flags shr 15) and 1 == 1L
@AliucordPlugin(requiresRestart = true)
@Suppress("unused")
class ComponentsV2 : Plugin() {
companion object {
/** Creates a new [MessageAttachment] */
fun createAttachment(
filename: String,
filesize: Long,
proxyUrl: String,
url: String,
width: Int,
height: Int,
): MessageAttachment {
val inst = ReflectUtils.allocateInstance(clazz)
filenameField.set(inst, filename)
filesizeField.set(inst, filesize)
proxyUrlField.set(inst, proxyUrl)
urlField.set(inst, url)
widthField.set(inst, width)
heightField.set(inst, height)
return inst
}
private val clazz = MessageAttachment::class.java
private val filenameField = clazz.getDeclaredField("filename").apply { isAccessible = true }
private val filesizeField = clazz.getDeclaredField("size").apply { isAccessible = true }
private val proxyUrlField = clazz.getDeclaredField("proxyUrl").apply { isAccessible = true }
private val urlField = clazz.getDeclaredField("url").apply { isAccessible = true }
private val widthField = clazz.getDeclaredField("width").apply { isAccessible = true }
private val heightField = clazz.getDeclaredField("height").apply { isAccessible = true }
}
override fun start(context: Context) {
val oldFile = File("${Constants.PLUGINS_PATH}/ComponentsV2-Beta.zip")
if (oldFile.exists()) {
logger.info("old plugin found, deleting and prompting restart")
oldFile.delete()
Utils.promptRestart()
return
}
compat(patcher)
XposedBridge.makeClassInheritable(BotUiComponentEntry::class.java)
ComponentV2Type.make()
patchGson()
// https://github.com/LSPosed/LSPlant/issues/41
patchMessageItems(patcher)
@ -205,32 +160,6 @@ class ComponentsV2 : Plugin() {
override fun stop(context: Context) {
patcher.unpatchAll()
unpatchGson()
ComponentV2Type.unmake(logger)
}
private fun patchGson() {
val factory = ComponentRuntimeTypeAdapter.INSTANCE.a()
val typeToClass = factory.l
val classToType = factory.m
ComponentV2Type.newValues?.forEach {
typeToClass[it.type.toString()] = it.clazz
classToType[it.clazz] = it.type.toString()
}
patcher.instead<ComponentTypeTypeAdapter>("read", JsonReader::class.java)
{ (_, jsonReader: JsonReader) ->
val type: Int = b.c.a.a0.d.n1(jsonReader)
ComponentType.values().find { it.type == type } ?: ComponentType.UNKNOWN
}
}
private fun unpatchGson() {
val factory = ComponentRuntimeTypeAdapter.INSTANCE.a()
val typeToClass = factory.l
val classToType = factory.m
ComponentV2Type.newValues?.forEach {
typeToClass.remove(it.type.toString())
classToType.remove(it.clazz)
}
stopCompat()
}
}

View file

@ -10,7 +10,7 @@ import android.widget.FrameLayout
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
import com.aliucord.Logger
import com.aliucord.coreplugins.ComponentsV2
import com.aliucord.coreplugins.CV2Compat
import com.aliucord.coreplugins.componentsv2.BotUiComponentV2Entry
import com.aliucord.coreplugins.componentsv2.ComponentV2Type
import com.aliucord.coreplugins.componentsv2.models.MediaGalleryMessageComponent
@ -77,7 +77,7 @@ class MediaGalleryComponentView(ctx: Context) : ConstraintLayout(ctx), Component
val media = it.media
// TODO: there's probably a utility to extract filename from url
val name = media.url.split("/").last().split("?").first()
val attachment = ComponentsV2.createAttachment(
val attachment = CV2Compat.createAttachment(
name,
0,
media.proxyUrl,