Add IRC connector
This commit is contained in:
@@ -1,22 +1,28 @@
|
|||||||
# Define argument for linker flags
|
# Define argument for linker flags
|
||||||
ARG LDFLAGS=-s -w
|
ARG LDFLAGS=-s -w
|
||||||
|
|
||||||
# Use a temporary build image based on Golang 1.20-alpine
|
# Use a temporary build image based on Golang 1.22-alpine
|
||||||
FROM golang:1.22-alpine as builder
|
FROM golang:1.22-alpine as builder
|
||||||
|
|
||||||
# Set environment variables: linker flags and disable CGO
|
# Set environment variables: linker flags and disable CGO
|
||||||
ENV LDFLAGS=$LDFLAGS CGO_ENABLED=0
|
ENV LDFLAGS=$LDFLAGS CGO_ENABLED=0
|
||||||
|
|
||||||
# Install git and build the edgevpn binary with the provided linker flags
|
# Install git
|
||||||
# --no-cache flag ensures the package cache isn't stored in the layer, reducing image size
|
|
||||||
RUN apk add --no-cache git
|
RUN apk add --no-cache git
|
||||||
|
|
||||||
# Add the current directory contents to the work directory in the container
|
# Set the working directory
|
||||||
COPY . /work
|
|
||||||
|
|
||||||
# Set the current work directory inside the container
|
|
||||||
WORKDIR /work
|
WORKDIR /work
|
||||||
|
|
||||||
|
# Copy go.mod and go.sum files first to leverage Docker cache
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
|
||||||
|
# Download dependencies - this layer will be cached as long as go.mod and go.sum don't change
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Now copy the rest of the source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
RUN go build -ldflags="$LDFLAGS" -o localagent ./
|
RUN go build -ldflags="$LDFLAGS" -o localagent ./
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
|
|||||||
20
README.md
20
README.md
@@ -144,6 +144,26 @@ In the UI, when configuring the connector:
|
|||||||
{ "token": "botfathertoken" }
|
{ "token": "botfathertoken" }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### IRC
|
||||||
|
|
||||||
|
Connect to IRC servers and interact with channels:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"server": "irc.example.com",
|
||||||
|
"port": "6667",
|
||||||
|
"nickname": "LocalAgentBot",
|
||||||
|
"channel": "#yourchannel",
|
||||||
|
"alwaysReply": "false"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The IRC connector supports:
|
||||||
|
- Connecting to IRC servers without encryption
|
||||||
|
- Joining a specified channel
|
||||||
|
- Responding to direct mentions (or all messages if alwaysReply is "true")
|
||||||
|
- Direct messaging with users
|
||||||
|
|
||||||
### REST API
|
### REST API
|
||||||
|
|
||||||
The LocalAgent API follows RESTful principles and uses JSON for request and response bodies.
|
The LocalAgent API follows RESTful principles and uses JSON for request and response bodies.
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -58,6 +58,7 @@ require (
|
|||||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
|
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
|
||||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||||
github.com/temoto/robotstxt v1.1.2 // indirect
|
github.com/temoto/robotstxt v1.1.2 // indirect
|
||||||
|
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 // indirect
|
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 // indirect
|
||||||
|
|||||||
3
go.sum
3
go.sum
@@ -137,6 +137,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
|
|||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg=
|
github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg=
|
||||||
github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
|
github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
|
||||||
|
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64 h1:l/T7dYuJEQZOwVOpjIXr1180aM9PZL/d1MnMVIxefX4=
|
||||||
|
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64/go.mod h1:Q1NAJOuRdQCqN/VIWdnaaEhV8LpeO2rtlBP7/iDJNII=
|
||||||
github.com/tmc/langchaingo v0.1.8 h1:nrImgh0aWdu3stJTHz80N60WGwPWY8HXCK10gQny7bA=
|
github.com/tmc/langchaingo v0.1.8 h1:nrImgh0aWdu3stJTHz80N60WGwPWY8HXCK10gQny7bA=
|
||||||
github.com/tmc/langchaingo v0.1.8/go.mod h1:iNBfS9e6jxBKsJSPWnlqNhoVWgdA3D1g5cdFJjbIZNQ=
|
github.com/tmc/langchaingo v0.1.8/go.mod h1:iNBfS9e6jxBKsJSPWnlqNhoVWgdA3D1g5cdFJjbIZNQ=
|
||||||
github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E=
|
github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E=
|
||||||
@@ -167,6 +169,7 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r
|
|||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// Connectors
|
// Connectors
|
||||||
|
ConnectorIRC = "irc"
|
||||||
ConnectorTelegram = "telegram"
|
ConnectorTelegram = "telegram"
|
||||||
ConnectorSlack = "slack"
|
ConnectorSlack = "slack"
|
||||||
ConnectorDiscord = "discord"
|
ConnectorDiscord = "discord"
|
||||||
@@ -19,6 +20,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var AvailableConnectors = []string{
|
var AvailableConnectors = []string{
|
||||||
|
ConnectorIRC,
|
||||||
ConnectorTelegram,
|
ConnectorTelegram,
|
||||||
ConnectorSlack,
|
ConnectorSlack,
|
||||||
ConnectorDiscord,
|
ConnectorDiscord,
|
||||||
@@ -52,6 +54,8 @@ func Connectors(a *state.AgentConfig) []state.Connector {
|
|||||||
conns = append(conns, connectors.NewGithubIssueWatcher(config))
|
conns = append(conns, connectors.NewGithubIssueWatcher(config))
|
||||||
case ConnectorGithubPRs:
|
case ConnectorGithubPRs:
|
||||||
conns = append(conns, connectors.NewGithubPRWatcher(config))
|
conns = append(conns, connectors.NewGithubPRWatcher(config))
|
||||||
|
case ConnectorIRC:
|
||||||
|
conns = append(conns, connectors.NewIRC(config))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return conns
|
return conns
|
||||||
|
|||||||
170
services/connectors/irc.go
Normal file
170
services/connectors/irc.go
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
package connectors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mudler/LocalAgent/core/agent"
|
||||||
|
"github.com/mudler/LocalAgent/pkg/xlog"
|
||||||
|
"github.com/mudler/LocalAgent/services/actions"
|
||||||
|
"github.com/thoj/go-ircevent"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IRC struct {
|
||||||
|
server string
|
||||||
|
port string
|
||||||
|
nickname string
|
||||||
|
channel string
|
||||||
|
conn *irc.Connection
|
||||||
|
alwaysReply bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIRC(config map[string]string) *IRC {
|
||||||
|
return &IRC{
|
||||||
|
server: config["server"],
|
||||||
|
port: config["port"],
|
||||||
|
nickname: config["nickname"],
|
||||||
|
channel: config["channel"],
|
||||||
|
alwaysReply: config["alwaysReply"] == "true",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IRC) AgentResultCallback() func(state agent.ActionState) {
|
||||||
|
return func(state agent.ActionState) {
|
||||||
|
// Send the result to the bot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IRC) AgentReasoningCallback() func(state agent.ActionCurrentState) bool {
|
||||||
|
return func(state agent.ActionCurrentState) bool {
|
||||||
|
// Send the reasoning to the bot
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanUpUsernameFromMessage removes the bot's nickname from the message
|
||||||
|
func cleanUpMessage(message string, nickname string) string {
|
||||||
|
cleaned := strings.ReplaceAll(message, nickname+":", "")
|
||||||
|
cleaned = strings.ReplaceAll(cleaned, nickname+",", "")
|
||||||
|
cleaned = strings.TrimSpace(cleaned)
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
// isMentioned checks if the bot is mentioned in the message
|
||||||
|
func isMentioned(message string, nickname string) bool {
|
||||||
|
return strings.Contains(message, nickname+":") ||
|
||||||
|
strings.Contains(message, nickname+",") ||
|
||||||
|
strings.HasPrefix(message, nickname)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start connects to the IRC server and starts listening for messages
|
||||||
|
func (i *IRC) Start(a *agent.Agent) {
|
||||||
|
i.conn = irc.IRC(i.nickname, i.nickname)
|
||||||
|
i.conn.UseTLS = false
|
||||||
|
i.conn.AddCallback("001", func(e *irc.Event) {
|
||||||
|
xlog.Info("Connected to IRC server", "server", i.server)
|
||||||
|
i.conn.Join(i.channel)
|
||||||
|
xlog.Info("Joined channel", "channel", i.channel)
|
||||||
|
})
|
||||||
|
|
||||||
|
i.conn.AddCallback("JOIN", func(e *irc.Event) {
|
||||||
|
if e.Nick == i.nickname {
|
||||||
|
xlog.Info("Bot joined channel", "channel", e.Arguments[0])
|
||||||
|
time.Sleep(1 * time.Second) // Small delay to ensure join is complete
|
||||||
|
i.conn.Privmsg(e.Arguments[0], "Hello! I've just (re)started and am ready to assist.")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
i.conn.AddCallback("PRIVMSG", func(e *irc.Event) {
|
||||||
|
message := e.Message()
|
||||||
|
sender := e.Nick
|
||||||
|
channel := e.Arguments[0]
|
||||||
|
isDirect := false
|
||||||
|
|
||||||
|
if channel == i.nickname {
|
||||||
|
channel = sender
|
||||||
|
isDirect = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip messages from ourselves
|
||||||
|
if sender == i.nickname {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(i.alwaysReply || isMentioned(message, i.nickname) || isDirect) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
xlog.Info("Recv message", "message", message, "sender", sender, "channel", channel)
|
||||||
|
cleanedMessage := "I am " + sender + ". " + cleanUpMessage(message, i.nickname)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
res := a.Ask(
|
||||||
|
agent.WithText(cleanedMessage),
|
||||||
|
)
|
||||||
|
|
||||||
|
xlog.Info("Sending message", "message", res.Response, "channel", channel)
|
||||||
|
|
||||||
|
// Split the response into multiple messages if it's too long
|
||||||
|
// IRC typically has a message length limit
|
||||||
|
maxLength := 400 // Safe limit for most IRC servers
|
||||||
|
response := res.Response
|
||||||
|
|
||||||
|
// Handle multiline responses
|
||||||
|
lines := strings.Split(response, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split long lines
|
||||||
|
for len(line) > 0 {
|
||||||
|
var chunk string
|
||||||
|
if len(line) > maxLength {
|
||||||
|
chunk = line[:maxLength]
|
||||||
|
line = line[maxLength:]
|
||||||
|
} else {
|
||||||
|
chunk = line
|
||||||
|
line = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the message to the channel
|
||||||
|
i.conn.Privmsg(channel, chunk)
|
||||||
|
|
||||||
|
// Small delay to prevent flooding
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle any attachments or special content from actions
|
||||||
|
for _, state := range res.State {
|
||||||
|
// Handle URLs from search action
|
||||||
|
if urls, exists := state.Metadata[actions.MetadataUrls]; exists {
|
||||||
|
for _, url := range urls.([]string) {
|
||||||
|
i.conn.Privmsg(channel, fmt.Sprintf("URL: %s", url))
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle image URLs
|
||||||
|
if imagesUrls, exists := state.Metadata[actions.MetadataImages]; exists {
|
||||||
|
for _, url := range imagesUrls.([]string) {
|
||||||
|
i.conn.Privmsg(channel, fmt.Sprintf("Image: %s", url))
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Connect to the server
|
||||||
|
err := i.conn.Connect(i.server + ":" + i.port)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Failed to connect to IRC server", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the IRC client in a goroutine
|
||||||
|
go i.conn.Loop()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user