fix(irc): resolve merge conflicts with latest main

This commit is contained in:
Alix-007 2026-03-13 08:25:25 +08:00
parent 5a9bf6c644
commit 2180c4b9ab

View file

@ -1,216 +1,216 @@
package irc package irc
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"strings" "strings"
"github.com/ergochat/irc-go/ircevent" "github.com/ergochat/irc-go/ircevent"
"github.com/ergochat/irc-go/ircmsg" "github.com/ergochat/irc-go/ircmsg"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/channels" "github.com/sipeed/picoclaw/pkg/channels"
"github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/logger"
) )
// IRCChannel implements the Channel interface for IRC servers. // IRCChannel implements the Channel interface for IRC servers.
type IRCChannel struct { type IRCChannel struct {
*channels.BaseChannel *channels.BaseChannel
config config.IRCConfig config config.IRCConfig
conn *ircevent.Connection conn *ircevent.Connection
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
} }
// NewIRCChannel creates a new IRC channel. // NewIRCChannel creates a new IRC channel.
func NewIRCChannel(cfg config.IRCConfig, messageBus *bus.MessageBus) (*IRCChannel, error) { func NewIRCChannel(cfg config.IRCConfig, messageBus *bus.MessageBus) (*IRCChannel, error) {
if cfg.Server == "" { if cfg.Server == "" {
return nil, fmt.Errorf("irc server is required") return nil, fmt.Errorf("irc server is required")
} }
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) 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),
channels.WithGroupTrigger(cfg.GroupTrigger), channels.WithGroupTrigger(cfg.GroupTrigger),
channels.WithReasoningChannelID(cfg.ReasoningChannelID), channels.WithReasoningChannelID(cfg.ReasoningChannelID),
) )
return &IRCChannel{ return &IRCChannel{
BaseChannel: base, BaseChannel: base,
config: cfg, config: cfg,
}, nil }, nil
} }
// Start connects to the IRC server and begins listening. // Start connects to the IRC server and begins listening.
func (c *IRCChannel) Start(ctx context.Context) error { func (c *IRCChannel) Start(ctx context.Context) error {
logger.InfoC("irc", "Starting IRC channel") logger.InfoC("irc", "Starting IRC channel")
c.ctx, c.cancel = context.WithCancel(ctx) c.ctx, c.cancel = context.WithCancel(ctx)
user := c.config.User user := c.config.User
if user == "" { if user == "" {
user = c.config.Nick user = c.config.Nick
} }
realName := c.config.RealName realName := c.config.RealName
if realName == "" { if realName == "" {
realName = c.config.Nick realName = c.config.Nick
} }
caps := []string(c.config.RequestCaps) caps := []string(c.config.RequestCaps)
if len(caps) == 0 { if len(caps) == 0 {
caps = []string{"server-time", "message-tags"} caps = []string{"server-time", "message-tags"}
} }
conn := &ircevent.Connection{ conn := &ircevent.Connection{
Server: c.config.Server, Server: c.config.Server,
Nick: c.config.Nick, Nick: c.config.Nick,
User: user, User: user,
RealName: realName, RealName: realName,
Password: c.config.Password, Password: c.config.Password,
UseTLS: c.config.TLS, UseTLS: c.config.TLS,
RequestCaps: caps, RequestCaps: caps,
QuitMessage: "Goodbye", QuitMessage: "Goodbye",
Debug: false, Debug: false,
Log: nil, Log: nil,
} }
if c.config.TLS { if c.config.TLS {
conn.TLSConfig = &tls.Config{ conn.TLSConfig = &tls.Config{
ServerName: extractHost(c.config.Server), ServerName: extractHost(c.config.Server),
} }
} }
// SASL auth (takes priority over NickServ) // SASL auth (takes priority over NickServ)
if c.config.SASLUser != "" && c.config.SASLPassword != "" { if c.config.SASLUser != "" && c.config.SASLPassword != "" {
conn.SASLLogin = c.config.SASLUser conn.SASLLogin = c.config.SASLUser
conn.SASLPassword = c.config.SASLPassword conn.SASLPassword = c.config.SASLPassword
} }
// Register event handlers // Register event handlers
conn.AddConnectCallback(func(e ircmsg.Message) { conn.AddConnectCallback(func(e ircmsg.Message) {
c.onConnect(conn) c.onConnect(conn)
}) })
conn.AddCallback("PRIVMSG", func(e ircmsg.Message) { conn.AddCallback("PRIVMSG", func(e ircmsg.Message) {
c.onPrivmsg(conn, e) c.onPrivmsg(conn, e)
}) })
if err := conn.Connect(); err != nil { if err := conn.Connect(); err != nil {
return fmt.Errorf("irc connect failed: %w", err) return fmt.Errorf("irc connect failed: %w", err)
} }
c.conn = conn c.conn = conn
// ircevent.Connection.Loop() handles reconnection internally. // ircevent.Connection.Loop() handles reconnection internally.
go conn.Loop() go conn.Loop()
c.SetRunning(true) c.SetRunning(true)
logger.InfoCF("irc", "IRC channel started", map[string]any{ logger.InfoCF("irc", "IRC channel started", map[string]any{
"server": c.config.Server, "server": c.config.Server,
"nick": c.config.Nick, "nick": c.config.Nick,
}) })
return nil return nil
} }
// Stop disconnects from the IRC server. // Stop disconnects from the IRC server.
func (c *IRCChannel) Stop(ctx context.Context) error { func (c *IRCChannel) Stop(ctx context.Context) error {
logger.InfoC("irc", "Stopping IRC channel") logger.InfoC("irc", "Stopping IRC channel")
c.SetRunning(false) c.SetRunning(false)
if c.conn != nil { if c.conn != nil {
c.conn.Quit() c.conn.Quit()
} }
if c.cancel != nil { if c.cancel != nil {
c.cancel() c.cancel()
} }
logger.InfoC("irc", "IRC channel stopped") logger.InfoC("irc", "IRC channel stopped")
return nil return nil
} }
// Send sends a message to an IRC channel or user. // Send sends a message to an IRC channel or user.
func (c *IRCChannel) Send(ctx context.Context, msg bus.OutboundMessage) error { func (c *IRCChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
if !c.IsRunning() { if !c.IsRunning() {
return channels.ErrNotRunning return channels.ErrNotRunning
} }
target := msg.ChatID target := msg.ChatID
if target == "" { if target == "" {
return fmt.Errorf("chat ID is empty: %w", channels.ErrSendFailed) return fmt.Errorf("chat ID is empty: %w", channels.ErrSendFailed)
} }
if strings.TrimSpace(msg.Content) == "" { if strings.TrimSpace(msg.Content) == "" {
return nil return nil
} }
// Send each line separately (IRC is line-oriented) // Send each line separately (IRC is line-oriented)
lines := strings.Split(msg.Content, "\n") lines := strings.Split(msg.Content, "\n")
for _, line := range lines { for _, line := range lines {
line = strings.TrimRight(line, "\r") line = strings.TrimRight(line, "\r")
if line == "" { if line == "" {
continue continue
} }
c.conn.Privmsg(target, line) c.conn.Privmsg(target, line)
} }
logger.DebugCF("irc", "Message sent", map[string]any{ logger.DebugCF("irc", "Message sent", map[string]any{
"target": target, "target": target,
"lines": len(lines), "lines": len(lines),
}) })
return nil return nil
} }
// StartTyping implements channels.TypingCapable using IRCv3 +typing client tag. // StartTyping implements channels.TypingCapable using IRCv3 +typing client tag.
// Requires typing.enabled in config and server support for message-tags capability. // Requires typing.enabled in config and server support for message-tags capability.
func (c *IRCChannel) StartTyping(ctx context.Context, chatID string) (func(), error) { func (c *IRCChannel) StartTyping(ctx context.Context, chatID string) (func(), error) {
noop := func() {} noop := func() {}
if !c.config.Typing.Enabled || !c.IsRunning() || c.conn == nil { if !c.config.Typing.Enabled || !c.IsRunning() || c.conn == nil {
return noop, nil return noop, nil
} }
// Check if server supports message-tags (required for TAGMSG) // Check if server supports message-tags (required for TAGMSG)
if _, ok := c.conn.AcknowledgedCaps()["message-tags"]; !ok { if _, ok := c.conn.AcknowledgedCaps()["message-tags"]; !ok {
return noop, nil return noop, nil
} }
c.conn.SendWithTags(map[string]string{"+typing": "active"}, "TAGMSG", chatID) c.conn.SendWithTags(map[string]string{"+typing": "active"}, "TAGMSG", chatID)
return func() { return func() {
if c.IsRunning() && c.conn != nil { if c.IsRunning() && c.conn != nil {
c.conn.SendWithTags(map[string]string{"+typing": "done"}, "TAGMSG", chatID) c.conn.SendWithTags(map[string]string{"+typing": "done"}, "TAGMSG", chatID)
} }
}, nil }, nil
} }
// extractHost returns the hostname portion of a host:port string. // extractHost returns the hostname portion of a host:port string.
func extractHost(server string) string { func extractHost(server string) string {
host, _, found := strings.Cut(server, ":") host, _, found := strings.Cut(server, ":")
if found { if found {
return host return host
} }
return server return server
} }
func normalizeIRCChannels(channels config.FlexibleStringSlice) config.FlexibleStringSlice { func normalizeIRCChannels(channels config.FlexibleStringSlice) config.FlexibleStringSlice {
if len(channels) == 0 { if len(channels) == 0 {
return channels return channels
} }
normalized := make(config.FlexibleStringSlice, 0, len(channels)) normalized := make(config.FlexibleStringSlice, 0, len(channels))
for _, channel := range channels { for _, channel := range channels {
channel = strings.TrimSpace(channel) channel = strings.TrimSpace(channel)
if channel == "" { if channel == "" {
continue continue
} }
switch channel[0] { switch channel[0] {
case '#', '&', '+', '!': case '#', '&', '+', '!':
normalized = append(normalized, channel) normalized = append(normalized, channel)
default: default:
normalized = append(normalized, "#"+channel) normalized = append(normalized, "#"+channel)
} }
} }
return normalized return normalized
} }