mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
Redesign plugins UI and demo raw + high-level hooks
- Match exteraGram-style card layout with master toggle and plugin actions - Add usage manifest field, pin/delete/removePlugin engine APIs - Hello World plugin hooks Logger.d (raw) and send pipeline (high-level) - Bundle updated hello_world.fcplugin; stop debug auto-enable Co-authored-by: denis0001-dev <denis0001.dev@ya.ru>
This commit is contained in:
@@ -24,6 +24,7 @@ object PluginEngine : PluginHostBridge {
|
||||
val installed: StateFlow<List<PluginManifest>> = _installed.asStateFlow()
|
||||
|
||||
private val enabledIds = linkedSetOf<String>()
|
||||
private val pinnedIds = linkedSetOf<String>()
|
||||
private val loaded = mutableMapOf<String, LoadedPlugin>()
|
||||
private val settings = mutableMapOf<String, MutableMap<String, String>>()
|
||||
private val sharedUnhooks = mutableMapOf<String, MutableList<() -> Unit>>()
|
||||
@@ -72,6 +73,40 @@ object PluginEngine : PluginHostBridge {
|
||||
|
||||
fun isPluginEnabled(pluginId: String): Boolean = pluginId in enabledIds
|
||||
|
||||
fun isPluginPinned(pluginId: String): Boolean = pluginId in pinnedIds
|
||||
|
||||
fun setPluginPinned(pluginId: String, pinned: Boolean) {
|
||||
if (pinned) {
|
||||
pinnedIds += pluginId
|
||||
} else {
|
||||
pinnedIds -= pluginId
|
||||
}
|
||||
persistPinnedState()
|
||||
refreshInstalledList()
|
||||
}
|
||||
|
||||
fun sortedInstalled(): List<PluginManifest> {
|
||||
val manifests = _installed.value
|
||||
return manifests.sortedWith(
|
||||
compareByDescending<PluginManifest> { it.id in pinnedIds }
|
||||
.thenBy { it.name.lowercase() },
|
||||
)
|
||||
}
|
||||
|
||||
fun removePlugin(pluginId: String) {
|
||||
unloadPlugin(pluginId)
|
||||
enabledIds -= pluginId
|
||||
pinnedIds -= pluginId
|
||||
settings.remove(pluginId)
|
||||
val dir = pluginsRootProvider().resolve(pluginId)
|
||||
if (dir.exists()) {
|
||||
dir.deleteRecursively()
|
||||
}
|
||||
persistEnabledState()
|
||||
persistPinnedState()
|
||||
refreshInstalledList()
|
||||
}
|
||||
|
||||
fun loadedPlugin(pluginId: String): LoadedPlugin? = loaded[pluginId]
|
||||
|
||||
fun getPluginSettingBoolean(pluginId: String, key: String, default: Boolean = false): Boolean =
|
||||
@@ -238,12 +273,21 @@ object PluginEngine : PluginHostBridge {
|
||||
enabledIds.clear()
|
||||
enabledIds += enabledFile.readLines().map { it.trim() }.filter { it.isNotEmpty() }
|
||||
}
|
||||
val pinnedFile = pluginsRootProvider().resolve("pinned_plugins.txt")
|
||||
if (pinnedFile.exists()) {
|
||||
pinnedIds.clear()
|
||||
pinnedIds += pinnedFile.readLines().map { it.trim() }.filter { it.isNotEmpty() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun persistEnabledState() {
|
||||
pluginsRootProvider().resolve("enabled_plugins.txt").writeText(enabledIds.joinToString("\n"))
|
||||
}
|
||||
|
||||
private fun persistPinnedState() {
|
||||
pluginsRootProvider().resolve("pinned_plugins.txt").writeText(pinnedIds.joinToString("\n"))
|
||||
}
|
||||
|
||||
private fun projectZipExtract(archive: File, dest: File) {
|
||||
java.util.zip.ZipFile(archive).use { zip ->
|
||||
zip.entries().asSequence().forEach { entry ->
|
||||
@@ -261,10 +305,30 @@ object PluginEngine : PluginHostBridge {
|
||||
fun ensureBundledPlugins(bundledDir: File) {
|
||||
if (!bundledDir.exists()) return
|
||||
bundledDir.listFiles()?.filter { it.extension == "fcplugin" }?.forEach { archive ->
|
||||
val installed = pluginsRootProvider().resolve("hello_world")
|
||||
if (!installed.exists()) {
|
||||
val manifestInArchive = readManifestFromArchive(archive) ?: return@forEach
|
||||
val installedDir = pluginsRootProvider().resolve(manifestInArchive.id)
|
||||
val installedManifest = installedDir.resolve("manifest.json")
|
||||
val shouldInstall = when {
|
||||
!installedManifest.exists() -> true
|
||||
else -> {
|
||||
val current = runCatching {
|
||||
PluginManifestParser.parse(installedManifest.readText())
|
||||
}.getOrNull()
|
||||
current == null || current.version != manifestInArchive.version
|
||||
}
|
||||
}
|
||||
if (shouldInstall) {
|
||||
runCatching { installFcPluginArchive(archive) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun readManifestFromArchive(archive: File): PluginManifest? {
|
||||
return runCatching {
|
||||
java.util.zip.ZipFile(archive).use { zip ->
|
||||
val entry = zip.getEntry("manifest.json") ?: return null
|
||||
zip.getInputStream(entry).bufferedReader().use { PluginManifestParser.parse(it.readText()) }
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ object PluginManifestParser {
|
||||
description = root["description"]?.jsonPrimitive?.contentOrNull.orEmpty(),
|
||||
author = root["author"]?.jsonPrimitive?.contentOrNull.orEmpty(),
|
||||
version = root["version"]?.jsonPrimitive?.contentOrNull ?: "1.0.0",
|
||||
usage = root["usage"]?.jsonPrimitive?.contentOrNull.orEmpty(),
|
||||
appVersion = root["app_version"]?.jsonPrimitive?.contentOrNull.orEmpty(),
|
||||
sdkVersion = root["sdk_version"]?.jsonPrimitive?.contentOrNull.orEmpty(),
|
||||
entryClass = root.requireString("entry_class"),
|
||||
|
||||
@@ -35,9 +35,10 @@ tasks.register("packageFcPlugin") {
|
||||
{
|
||||
"id": "hello_world",
|
||||
"name": "Hello World",
|
||||
"description": "Rewrites .hello commands into a friendly greeting",
|
||||
"description": "Demonstrates high-level send hooks and raw Logger.d hooks.",
|
||||
"author": "FromChat",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"usage": "Send .hello Name in any chat to rewrite the message. Raw hook shows a bulletin when a message is queued for sending.",
|
||||
"app_version": ">=1.0.0",
|
||||
"sdk_version": ">=1.0.0",
|
||||
"entry_class": "ru.fromchat.plugins.sample.hello.HelloWorldPlugin",
|
||||
|
||||
+12
@@ -13,6 +13,17 @@ class HelloWorldPlugin : BasePlugin() {
|
||||
addOnSendMessageHook { context ->
|
||||
onSendMessage(context)
|
||||
}
|
||||
hookShared(
|
||||
hookId = "logger.debug",
|
||||
after = { args, _ ->
|
||||
val tag = args.getOrNull(0) as? String ?: return@hookShared HookResult()
|
||||
val message = args.getOrNull(1) as? String ?: return@hookShared HookResult()
|
||||
if (tag == "OutgoingMessageCoordinator" && message.contains("enqueue")) {
|
||||
showBulletin("Raw hook: intercepted outgoing message pipeline")
|
||||
}
|
||||
HookResult()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun createSettings(): List<PluginSetting> = listOf(
|
||||
@@ -36,6 +47,7 @@ class HelloWorldPlugin : BasePlugin() {
|
||||
val name = parts.getOrNull(1)?.trim().orEmpty().ifEmpty { "World" }
|
||||
val template = getSettingString("template", "Hello, {name}!")
|
||||
context.text = template.replace("{name}", name)
|
||||
showBulletin("High-level hook: rewrote .hello command")
|
||||
return HookResult(strategy = HookStrategy.MODIFY, value = context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ data class PluginManifest(
|
||||
val description: String = "",
|
||||
val author: String = "",
|
||||
val version: String = "1.0.0",
|
||||
val usage: String = "",
|
||||
val appVersion: String = "",
|
||||
val sdkVersion: String = "",
|
||||
val entryClass: String,
|
||||
|
||||
Reference in New Issue
Block a user