mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
Add Kotlin plugin system (SDK, host, hooks, settings UI)
Implement exteraGram-style plugin architecture for Android and desktop: - plugins/sdk: BasePlugin API, hook types, settings DSL - plugins/host: PluginEngine, typed send hooks, FeatureGate, HookRegistry - JVM ByteBuddy and Android Pine hook backends - DevServer on port 42690 (JVM) - Plugins settings screens and PluginUiHost bulletin overlay - hello_world sample plugin packaged as .fcplugin - ProGuard keepnames for ru.fromchat.** with shrinking enabled Co-authored-by: denis0001-dev <denis0001.dev@ya.ru>
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.multiplatform)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.kotlin.multiplatform.library)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
androidLibrary {
|
||||
namespace = "ru.fromchat.plugins.sdk"
|
||||
compileSdk = 37
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package ru.fromchat.plugins
|
||||
|
||||
interface PluginHostBridge {
|
||||
fun log(pluginId: String, message: String)
|
||||
fun getSetting(pluginId: String, key: String, default: Boolean): Boolean
|
||||
fun getSettingString(pluginId: String, key: String, default: String): String
|
||||
fun setSetting(pluginId: String, key: String, value: String)
|
||||
fun registerSendMessageHook(pluginId: String, priority: Int, handler: (SendMessageHookContext) -> HookResult<SendMessageHookContext>)
|
||||
fun registerFeatureOverride(pluginId: String, featureId: String, provider: () -> Boolean?)
|
||||
fun registerMenuItem(pluginId: String, item: MenuItemData)
|
||||
fun showBulletin(message: String)
|
||||
fun hookSharedMethod(
|
||||
pluginId: String,
|
||||
hookId: String,
|
||||
priority: Int,
|
||||
before: ((Array<Any?>) -> HookResult<Array<Any?>>)?,
|
||||
after: ((Array<Any?>, Any?) -> HookResult<Any?>)?,
|
||||
): () -> Unit
|
||||
}
|
||||
|
||||
abstract class BasePlugin {
|
||||
lateinit var pluginId: String
|
||||
internal set
|
||||
private lateinit var bridge: PluginHostBridge
|
||||
|
||||
fun attach(id: String, hostBridge: PluginHostBridge) {
|
||||
pluginId = id
|
||||
bridge = hostBridge
|
||||
}
|
||||
|
||||
open fun onPluginLoad() {}
|
||||
open fun onPluginUnload() {}
|
||||
open fun onAppEvent(event: AppEvent) {}
|
||||
open fun createSettings(): List<PluginSetting> = emptyList()
|
||||
|
||||
protected fun log(message: String) = bridge.log(pluginId, message)
|
||||
|
||||
protected fun getSetting(key: String, default: Boolean = false): Boolean =
|
||||
bridge.getSetting(pluginId, key, default)
|
||||
|
||||
protected fun getSettingString(key: String, default: String = ""): String =
|
||||
bridge.getSettingString(pluginId, key, default)
|
||||
|
||||
protected fun setSetting(key: String, value: String, reloadSettings: Boolean = false) {
|
||||
bridge.setSetting(pluginId, key, value)
|
||||
if (reloadSettings) {
|
||||
PluginSettingsReload.request(pluginId)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun addOnSendMessageHook(
|
||||
priority: Int = 0,
|
||||
handler: (SendMessageHookContext) -> HookResult<SendMessageHookContext>,
|
||||
) {
|
||||
bridge.registerSendMessageHook(pluginId, priority, handler)
|
||||
}
|
||||
|
||||
protected fun registerFeatureOverride(featureId: String, provider: () -> Boolean?) {
|
||||
bridge.registerFeatureOverride(pluginId, featureId, provider)
|
||||
}
|
||||
|
||||
protected fun addMenuItem(item: MenuItemData) {
|
||||
bridge.registerMenuItem(pluginId, item)
|
||||
}
|
||||
|
||||
protected fun showBulletin(message: String) {
|
||||
bridge.showBulletin(message)
|
||||
}
|
||||
|
||||
protected fun hookShared(
|
||||
hookId: String,
|
||||
priority: Int = 0,
|
||||
before: ((Array<Any?>) -> HookResult<Array<Any?>>)? = null,
|
||||
after: ((Array<Any?>, Any?) -> HookResult<Any?>)? = null,
|
||||
): () -> Unit = bridge.hookSharedMethod(pluginId, hookId, priority, before, after)
|
||||
}
|
||||
|
||||
object PluginSettingsReload {
|
||||
var listener: ((String) -> Unit)? = null
|
||||
fun request(pluginId: String) {
|
||||
listener?.invoke(pluginId)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package ru.fromchat.plugins
|
||||
|
||||
enum class AppEvent {
|
||||
START,
|
||||
STOP,
|
||||
PAUSE,
|
||||
RESUME,
|
||||
}
|
||||
|
||||
enum class HookStrategy {
|
||||
DEFAULT,
|
||||
CANCEL,
|
||||
MODIFY,
|
||||
MODIFY_FINAL,
|
||||
}
|
||||
|
||||
data class HookResult<T>(
|
||||
val strategy: HookStrategy = HookStrategy.DEFAULT,
|
||||
val value: T? = null,
|
||||
)
|
||||
|
||||
data class SendMessageHookContext(
|
||||
var text: String,
|
||||
val isDm: Boolean,
|
||||
val recipientId: Int?,
|
||||
)
|
||||
|
||||
enum class MenuItemType {
|
||||
MESSAGE_CONTEXT,
|
||||
CHAT_ACTION,
|
||||
PROFILE_ACTION,
|
||||
SETTINGS_ROW,
|
||||
}
|
||||
|
||||
data class MenuItemData(
|
||||
val menuType: MenuItemType,
|
||||
val text: String,
|
||||
val subtext: String? = null,
|
||||
val priority: Int = 0,
|
||||
val onClickKey: String = "",
|
||||
)
|
||||
|
||||
sealed class PluginSetting {
|
||||
data class Header(val text: String) : PluginSetting()
|
||||
data class Switch(
|
||||
val key: String,
|
||||
val text: String,
|
||||
val default: Boolean = false,
|
||||
val subtext: String? = null,
|
||||
) : PluginSetting()
|
||||
data class Input(
|
||||
val key: String,
|
||||
val text: String,
|
||||
val default: String = "",
|
||||
val subtext: String? = null,
|
||||
) : PluginSetting()
|
||||
data class Text(
|
||||
val text: String,
|
||||
val subtext: String? = null,
|
||||
) : PluginSetting()
|
||||
}
|
||||
|
||||
data class PluginManifest(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val description: String = "",
|
||||
val author: String = "",
|
||||
val version: String = "1.0.0",
|
||||
val appVersion: String = "",
|
||||
val sdkVersion: String = "",
|
||||
val entryClass: String,
|
||||
val androidArtifact: String? = null,
|
||||
val desktopArtifact: String? = null,
|
||||
)
|
||||
Reference in New Issue
Block a user