// Package config parses the per-channel binding files at // /channels/.json. Plexum's channel.Registry already // parses the {agent_id, plugin} core; this package additionally pulls // out the plugin-specific `fabric` extension block describing which // Fabric (guild, channelId) the Plexum channel name maps to. package config import ( "encoding/json" "errors" "fmt" "io/fs" "os" "path/filepath" "strings" ) // PluginName is what manifest.json declares as the plugin name; we // only consume channels/*.json entries with this value in `plugin`. const PluginName = "plexum-fabric-channel" // FabricBinding is one Plexum-channel ↔ Fabric-channel mapping. Built // by Load from channels/.json files. type FabricBinding struct { // PlexumChannelName: the basename of channels/.json (no .json). // Also matches the ChannelContract.Name the plugin manifest advertises. PlexumChannelName string // AgentID this channel routes to (also recorded in Plexum's channel // registry; we re-read here for plugin-internal convenience). AgentID string // FabricGuildNodeID — which Fabric guild owns the channel. FabricGuildNodeID string // FabricChannelID — the channel id within that guild. FabricChannelID string } // On-disk shape; we ignore fields outside the `fabric` block. type wireConfig struct { AgentID string `json:"agent_id"` Plugin string `json:"plugin"` Fabric struct { GuildNodeID string `json:"guild_node_id"` ChannelID string `json:"channel_id"` } `json:"fabric"` } // Load returns all FabricBindings discovered under channelsDir. Files // whose `plugin` field doesn't match PluginName are silently skipped // (other channel plugins coexist in the same dir). Files with our // plugin name but missing fabric.{guild_node_id, channel_id} are // errors — we won't silently route nowhere. func Load(channelsDir string) ([]FabricBinding, error) { entries, err := os.ReadDir(channelsDir) if err != nil { if errors.Is(err, fs.ErrNotExist) { return nil, nil } return nil, fmt.Errorf("fabric/config: read %s: %w", channelsDir, err) } var out []FabricBinding for _, e := range entries { if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") { continue } path := filepath.Join(channelsDir, e.Name()) raw, rerr := os.ReadFile(path) if rerr != nil { return nil, fmt.Errorf("fabric/config: read %s: %w", path, rerr) } var w wireConfig if jerr := json.Unmarshal(raw, &w); jerr != nil { return nil, fmt.Errorf("fabric/config: parse %s: %w", path, jerr) } if w.Plugin != PluginName { continue } name := strings.TrimSuffix(e.Name(), ".json") if w.AgentID == "" { return nil, fmt.Errorf("fabric/config: %s missing agent_id", path) } if w.Fabric.GuildNodeID == "" || w.Fabric.ChannelID == "" { return nil, fmt.Errorf("fabric/config: %s missing fabric.{guild_node_id, channel_id}", path) } out = append(out, FabricBinding{ PlexumChannelName: name, AgentID: w.AgentID, FabricGuildNodeID: w.Fabric.GuildNodeID, FabricChannelID: w.Fabric.ChannelID, }) } return out, nil } // ByFabricChannel indexes bindings by (guild_node_id, channel_id) for // fast inbound lookup. Plugin builds this map once at startup. type ByFabricChannel map[string]*FabricBinding // Key composes the index key. func Key(guildNodeID, channelID string) string { return guildNodeID + "/" + channelID } // Index returns a ready-to-query ByFabricChannel. func Index(bindings []FabricBinding) ByFabricChannel { out := make(ByFabricChannel, len(bindings)) for i := range bindings { b := bindings[i] out[Key(b.FabricGuildNodeID, b.FabricChannelID)] = &b } return out }