fix(irc): accept string auto-join channel config

This commit is contained in:
李龙 0668001470 2026-03-13 01:15:05 +08:00 committed by XYSK-lilong007
parent 9222351871
commit 6a4f619229
5 changed files with 110 additions and 5 deletions

View file

@ -32,6 +32,7 @@ func NewIRCChannel(cfg config.IRCConfig, messageBus *bus.MessageBus) (*IRCChanne
if cfg.Nick == "" { if cfg.Nick == "" {
return nil, fmt.Errorf("irc nick is required") return nil, fmt.Errorf("irc nick is required")
} }
cfg.Channels = normalizeIRCChannels(cfg.Channels)
base := channels.NewBaseChannel("irc", cfg, messageBus, cfg.AllowFrom, base := channels.NewBaseChannel("irc", cfg, messageBus, cfg.AllowFrom,
channels.WithMaxMessageLength(400), channels.WithMaxMessageLength(400),
@ -192,3 +193,24 @@ func extractHost(server string) string {
} }
return server 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
}

View file

@ -1,6 +1,7 @@
package irc package irc
import ( import (
"reflect"
"testing" "testing"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/bus"
@ -43,6 +44,23 @@ func TestNewIRCChannel(t *testing.T) {
t.Error("new channel should not be running") 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) { func TestExtractHost(t *testing.T) {

View file

@ -29,6 +29,13 @@ func (f *FlexibleStringSlice) UnmarshalJSON(data []byte) error {
return nil 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 // Try []interface{} to handle mixed types
var raw []any var raw []any
if err := json.Unmarshal(data, &raw); err != nil { if err := json.Unmarshal(data, &raw); err != nil {
@ -57,21 +64,22 @@ func (f *FlexibleStringSlice) UnmarshalText(text []byte) error {
*f = nil *f = nil
return nil return nil
} }
*f = parseFlexibleStringSlice(string(text))
return nil
}
s := string(text) func parseFlexibleStringSlice(s string) FlexibleStringSlice {
// Replace Chinese comma with English comma, then split
s = strings.ReplaceAll(s, "", ",") s = strings.ReplaceAll(s, "", ",")
parts := strings.Split(s, ",") parts := strings.Split(s, ",")
result := make([]string, 0, len(parts)) result := make(FlexibleStringSlice, 0, len(parts))
for _, part := range parts { for _, part := range parts {
part = strings.TrimSpace(part) part = strings.TrimSpace(part)
if part != "" { if part != "" {
result = append(result, part) result = append(result, part)
} }
} }
*f = result return result
return nil
} }
type Config struct { type Config struct {

View file

@ -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, #opsdev"`), &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)
}
}

View file

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"reflect"
"testing" "testing"
"github.com/sipeed/picoclaw/pkg/config" "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) 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, #opsdev"
}
}
}`))
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)
}
}