mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
40b7b6ee4b
Add IRC as a new channel for picoclaw, supporting server connections, channel joins, DMs, mention-based group triggers, and IRCv3 typing indicators. Uses ergochat/irc-go for connection management with SASL, NickServ, and automatic reconnection support. Closes #1137
141 lines
3.4 KiB
Go
141 lines
3.4 KiB
Go
package irc
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/ergochat/irc-go/ircevent"
|
|
"github.com/ergochat/irc-go/ircmsg"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/bus"
|
|
"github.com/sipeed/picoclaw/pkg/identity"
|
|
"github.com/sipeed/picoclaw/pkg/logger"
|
|
)
|
|
|
|
// onConnect is called after a successful connection (and on reconnect).
|
|
func (c *IRCChannel) onConnect(conn *ircevent.Connection) {
|
|
// NickServ auth (only if SASL is not configured)
|
|
if c.config.NickServPassword != "" && c.config.SASLUser == "" {
|
|
conn.Privmsg("NickServ", "IDENTIFY "+c.config.NickServPassword)
|
|
}
|
|
|
|
// Join configured channels
|
|
for _, ch := range c.config.Channels {
|
|
conn.Join(ch)
|
|
logger.InfoCF("irc", "Joined IRC channel", map[string]any{
|
|
"channel": ch,
|
|
})
|
|
}
|
|
}
|
|
|
|
// onPrivmsg handles incoming PRIVMSG events.
|
|
func (c *IRCChannel) onPrivmsg(conn *ircevent.Connection, e ircmsg.Message) {
|
|
if len(e.Params) < 2 {
|
|
return
|
|
}
|
|
|
|
nick := e.Nick()
|
|
currentNick := conn.CurrentNick()
|
|
|
|
// Ignore own messages
|
|
if strings.EqualFold(nick, currentNick) {
|
|
return
|
|
}
|
|
|
|
target := e.Params[0] // channel name or bot's nick
|
|
content := e.Params[1] // message text
|
|
|
|
// Determine if this is a DM or channel message
|
|
isDM := !strings.HasPrefix(target, "#") && !strings.HasPrefix(target, "&")
|
|
|
|
var chatID string
|
|
var peer bus.Peer
|
|
|
|
if isDM {
|
|
chatID = nick
|
|
peer = bus.Peer{Kind: "direct", ID: nick}
|
|
} else {
|
|
chatID = target
|
|
peer = bus.Peer{Kind: "group", ID: target}
|
|
}
|
|
|
|
sender := bus.SenderInfo{
|
|
Platform: "irc",
|
|
PlatformID: nick,
|
|
CanonicalID: identity.BuildCanonicalID("irc", nick),
|
|
Username: nick,
|
|
DisplayName: nick,
|
|
}
|
|
|
|
if !c.IsAllowedSender(sender) {
|
|
return
|
|
}
|
|
|
|
// For channel messages, check group trigger (mention detection)
|
|
if !isDM {
|
|
isMentioned := isBotMentioned(content, currentNick)
|
|
if isMentioned {
|
|
content = stripBotMention(content, currentNick)
|
|
}
|
|
respond, cleaned := c.ShouldRespondInGroup(isMentioned, content)
|
|
if !respond {
|
|
return
|
|
}
|
|
content = cleaned
|
|
}
|
|
|
|
if strings.TrimSpace(content) == "" {
|
|
return
|
|
}
|
|
|
|
messageID := fmt.Sprintf("%s-%d", nick, time.Now().UnixNano())
|
|
|
|
metadata := map[string]string{
|
|
"platform": "irc",
|
|
"server": c.config.Server,
|
|
}
|
|
if !isDM {
|
|
metadata["channel"] = target
|
|
}
|
|
|
|
c.HandleMessage(c.ctx, peer, messageID, nick, chatID, content, nil, metadata, sender)
|
|
}
|
|
|
|
// isBotMentioned checks if the bot's nick appears in the message.
|
|
func isBotMentioned(content, botNick string) bool {
|
|
lower := strings.ToLower(content)
|
|
lowerNick := strings.ToLower(botNick)
|
|
|
|
// "nick: " or "nick, " at start (most common IRC convention)
|
|
if strings.HasPrefix(lower, lowerNick+":") || strings.HasPrefix(lower, lowerNick+",") {
|
|
return true
|
|
}
|
|
|
|
// Word-boundary match anywhere in the message
|
|
idx := strings.Index(lower, lowerNick)
|
|
if idx < 0 {
|
|
return false
|
|
}
|
|
before := idx == 0 || !isAlphanumeric(lower[idx-1])
|
|
after := idx+len(lowerNick) >= len(lower) || !isAlphanumeric(lower[idx+len(lowerNick)])
|
|
return before && after
|
|
}
|
|
|
|
// stripBotMention removes "nick: " or "nick, " prefix from content.
|
|
func stripBotMention(content, botNick string) string {
|
|
lower := strings.ToLower(content)
|
|
lowerNick := strings.ToLower(botNick)
|
|
for _, sep := range []string{":", ","} {
|
|
prefix := lowerNick + sep
|
|
if strings.HasPrefix(lower, prefix) {
|
|
return strings.TrimSpace(content[len(prefix):])
|
|
}
|
|
}
|
|
return content
|
|
}
|
|
|
|
func isAlphanumeric(b byte) bool {
|
|
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') || b == '_'
|
|
}
|