// Copyright 2026 MarketAlly. All rights reserved. // SPDX-License-Identifier: MIT package plugins import ( "strings" "time" "code.gitcaddy.com/server/v3/modules/log" "code.gitcaddy.com/server/v3/modules/setting" ) // ExternalPluginConfig holds configuration for a single external plugin type ExternalPluginConfig struct { Name string Enabled bool // Managed mode: server launches the binary Binary string Args string // External mode: connect to already-running process Address string // Common SubscribedEvents []string HealthTimeout time.Duration } // Config holds the global [plugins] configuration type Config struct { Enabled bool Path string HealthCheckInterval time.Duration ExternalPlugins map[string]*ExternalPluginConfig } // LoadConfig loads plugin configuration from app.ini [plugins] and [plugins.*] sections func LoadConfig() *Config { cfg := &Config{ ExternalPlugins: make(map[string]*ExternalPluginConfig), } sec := setting.CfgProvider.Section("plugins") cfg.Enabled = sec.Key("ENABLED").MustBool(true) cfg.Path = sec.Key("PATH").MustString("data/plugins") cfg.HealthCheckInterval = sec.Key("HEALTH_CHECK_INTERVAL").MustDuration(30 * time.Second) // Load [plugins.*] sections for external plugins for _, childSec := range sec.ChildSections() { name := strings.TrimPrefix(childSec.Name(), "plugins.") if name == "" { continue } pluginCfg := &ExternalPluginConfig{ Name: name, Enabled: childSec.Key("ENABLED").MustBool(true), Binary: childSec.Key("BINARY").MustString(""), Args: childSec.Key("ARGS").MustString(""), Address: childSec.Key("ADDRESS").MustString(""), HealthTimeout: childSec.Key("HEALTH_TIMEOUT").MustDuration(5 * time.Second), } // Parse subscribed events if eventsStr := childSec.Key("SUBSCRIBED_EVENTS").MustString(""); eventsStr != "" { pluginCfg.SubscribedEvents = splitAndTrim(eventsStr) } // Validate: must have either binary or address if pluginCfg.Binary == "" && pluginCfg.Address == "" { log.Warn("Plugin %q has neither BINARY nor ADDRESS configured, skipping", name) continue } cfg.ExternalPlugins[name] = pluginCfg log.Info("Loaded external plugin config: %s (managed=%v)", name, pluginCfg.IsManaged()) } return cfg } // IsManaged returns true if the server manages the plugin's lifecycle (has a binary) func (c *ExternalPluginConfig) IsManaged() bool { return c.Binary != "" } // splitAndTrim splits a comma-separated string and trims whitespace func splitAndTrim(s string) []string { var result []string for part := range strings.SplitSeq(s, ",") { part = strings.TrimSpace(part) if part != "" { result = append(result, part) } } return result }