diff --git a/pkg/channels/irc/irc.go b/pkg/channels/irc/irc.go index 28c59b540..2f8458f3a 100644 --- a/pkg/channels/irc/irc.go +++ b/pkg/channels/irc/irc.go @@ -32,6 +32,7 @@ func NewIRCChannel(cfg config.IRCConfig, messageBus *bus.MessageBus) (*IRCChanne if cfg.Nick == "" { return nil, fmt.Errorf("irc nick is required") } + cfg.Channels = normalizeIRCChannels(cfg.Channels) base := channels.NewBaseChannel("irc", cfg, messageBus, cfg.AllowFrom, channels.WithMaxMessageLength(400), @@ -192,3 +193,24 @@ func extractHost(server string) string { } return server } + +func normalizeIRCChannels(channels config.FlexibleStringSlice) config.FlexibleStringSlice { + if len(channels) == 0 { + return channels + } + + normalized := make(config.FlexibleStringSlice, 0, len(channels)) + for _, channel := range channels { + channel = strings.TrimSpace(channel) + if channel == "" { + continue + } + switch channel[0] { + case '#', '&', '+', '!': + normalized = append(normalized, channel) + default: + normalized = append(normalized, "#"+channel) + } + } + return normalized +} diff --git a/pkg/channels/irc/irc_test.go b/pkg/channels/irc/irc_test.go index 168252a4d..9ac3042c3 100644 --- a/pkg/channels/irc/irc_test.go +++ b/pkg/channels/irc/irc_test.go @@ -1,6 +1,7 @@ package irc import ( + "reflect" "testing" "github.com/sipeed/picoclaw/pkg/bus" @@ -43,6 +44,23 @@ func TestNewIRCChannel(t *testing.T) { t.Error("new channel should not be running") } }) + + t.Run("normalizes auto join channels", func(t *testing.T) { + cfg := config.IRCConfig{ + Server: "irc.example.com:6667", + Nick: "testbot", + Channels: []string{" general ", "#already", "&ops", "+local", "!safe", "", "news"}, + } + ch, err := NewIRCChannel(cfg, msgBus) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := config.FlexibleStringSlice{"#general", "#already", "&ops", "+local", "!safe", "#news"} + if !reflect.DeepEqual(ch.config.Channels, want) { + t.Fatalf("Channels = %#v, want %#v", ch.config.Channels, want) + } + }) } func TestExtractHost(t *testing.T) { diff --git a/pkg/config/config.go b/pkg/config/config.go index 7a7edb489..08e89864a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -29,6 +29,13 @@ func (f *FlexibleStringSlice) UnmarshalJSON(data []byte) error { return nil } + // Accept a single string so config patch APIs can submit comma-separated values. + var s string + if err := json.Unmarshal(data, &s); err == nil { + *f = parseFlexibleStringSlice(s) + return nil + } + // Try []interface{} to handle mixed types var raw []any if err := json.Unmarshal(data, &raw); err != nil { @@ -57,21 +64,22 @@ func (f *FlexibleStringSlice) UnmarshalText(text []byte) error { *f = nil return nil } + *f = parseFlexibleStringSlice(string(text)) + return nil +} - s := string(text) - // Replace Chinese comma with English comma, then split +func parseFlexibleStringSlice(s string) FlexibleStringSlice { s = strings.ReplaceAll(s, ",", ",") parts := strings.Split(s, ",") - result := make([]string, 0, len(parts)) + result := make(FlexibleStringSlice, 0, len(parts)) for _, part := range parts { part = strings.TrimSpace(part) if part != "" { result = append(result, part) } } - *f = result - return nil + return result } type Config struct { diff --git a/pkg/config/flexible_string_slice_test.go b/pkg/config/flexible_string_slice_test.go new file mode 100644 index 000000000..a5bb1763d --- /dev/null +++ b/pkg/config/flexible_string_slice_test.go @@ -0,0 +1,19 @@ +package config + +import ( + "encoding/json" + "reflect" + "testing" +) + +func TestFlexibleStringSliceUnmarshalJSON_String(t *testing.T) { + var got FlexibleStringSlice + if err := json.Unmarshal([]byte(`"general, #ops,dev"`), &got); err != nil { + t.Fatalf("Unmarshal() error = %v", err) + } + + want := FlexibleStringSlice{"general", "#ops", "dev"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("FlexibleStringSlice = %#v, want %#v", got, want) + } +} diff --git a/web/backend/api/config_test.go b/web/backend/api/config_test.go index 29811e37e..1daa4a835 100644 --- a/web/backend/api/config_test.go +++ b/web/backend/api/config_test.go @@ -4,6 +4,7 @@ import ( "bytes" "net/http" "net/http/httptest" + "reflect" "testing" "github.com/sipeed/picoclaw/pkg/config" @@ -86,3 +87,40 @@ func TestHandleUpdateConfig_DoesNotInheritDefaultModelFields(t *testing.T) { t.Fatalf("model_list[0].api_base = %q, want empty string", got) } } + +func TestHandlePatchConfig_AcceptsFlexibleStringSliceString(t *testing.T) { + configPath, cleanup := setupOAuthTestEnv(t) + defer cleanup() + + h := NewHandler(configPath) + mux := http.NewServeMux() + h.RegisterRoutes(mux) + + req := httptest.NewRequest(http.MethodPatch, "/api/config", bytes.NewBufferString(`{ + "channels": { + "irc": { + "enabled": true, + "server": "irc.example.com:6667", + "nick": "testbot", + "channels": "general, #ops,dev" + } + } + }`)) + req.Header.Set("Content-Type", "application/json") + + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String()) + } + + cfg, err := config.LoadConfig(configPath) + if err != nil { + t.Fatalf("LoadConfig() error = %v", err) + } + + want := config.FlexibleStringSlice{"general", "#ops", "dev"} + if !reflect.DeepEqual(cfg.Channels.IRC.Channels, want) { + t.Fatalf("channels.irc.channels = %#v, want %#v", cfg.Channels.IRC.Channels, want) + } +}