First commit
This commit is contained in:
commit
7d1c046145
33
Dockerfile
Normal file
33
Dockerfile
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Dockerfile for Matrix GPT Bot
|
||||||
|
|
||||||
|
# --- Build Stage ---
|
||||||
|
FROM golang:1.24-alpine AS builder
|
||||||
|
|
||||||
|
# Install git for module downloads
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Cache dependencies
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Copy the rest of the source
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the binary
|
||||||
|
RUN go build -o matrix-gpt-bot ./main.go
|
||||||
|
|
||||||
|
# --- Final Stage ---
|
||||||
|
FROM alpine:3.18
|
||||||
|
|
||||||
|
# Install CA certificates
|
||||||
|
RUN apk add --no-cache ca-certificates
|
||||||
|
|
||||||
|
WORKDIR /root/
|
||||||
|
|
||||||
|
# Copy the built binary from the builder stage
|
||||||
|
COPY --from=builder /app/matrix-gpt-bot .
|
||||||
|
|
||||||
|
# Define the entrypoint
|
||||||
|
ENTRYPOINT ["./matrix-gpt-bot"]
|
83
README.md
Normal file
83
README.md
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# Matrix GPT Bot
|
||||||
|
|
||||||
|
A Matrix chat bot written in Go that leverages OpenAI to provide GPT-powered conversational responses. The bot listens for messages prefixed with `gpt:` or mentions (`@gpt:<domain>`) and maintains per-user conversation history.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **GPT Conversations**: Respond to `gpt: <your message>` commands.
|
||||||
|
- **Matrix Mentions**: Also triggers on full Matrix mention (`@gpt:domain`).
|
||||||
|
- **Contextual History**: Keeps a conversation history per user.
|
||||||
|
- **HTML Formatting**: Sends replies as plain text (no additional markup).
|
||||||
|
- **Docker Support**: Easily containerize for deployment.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Go 1.20+ installed
|
||||||
|
- Access to a Matrix homeserver (e.g., Synapse)
|
||||||
|
- A bot user created on your Matrix server
|
||||||
|
- An OpenAI API key
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Edit `config/config.go` to set your credentials:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func Load() *Config {
|
||||||
|
return &Config{
|
||||||
|
Homeserver: "https://matrix.your-server.net",
|
||||||
|
BotUserID: "@gpt:your-server.net", // Your bot's user ID
|
||||||
|
AccessToken: "YOUR_MATRIX_ACCESS_TOKEN", // Your Matrix access token
|
||||||
|
OpenAIKey: "YOUR_OPENAI_API_KEY", // Your OpenAI API key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://git.ewnix.net/phlux/matrix-gpt-bot.git
|
||||||
|
cd matrix-gpt-bot
|
||||||
|
|
||||||
|
# Initialize modules and download dependencies
|
||||||
|
go mod init matrix-gpt-bot
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
# Run the bot
|
||||||
|
go run .
|
||||||
|
```
|
||||||
|
|
||||||
|
Invite your bot into a Matrix room and test:
|
||||||
|
|
||||||
|
```
|
||||||
|
gpt: Hello, bot!
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Deployment
|
||||||
|
|
||||||
|
To build a Docker image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t matrix-gpt-bot:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d --name matrix-gpt-bot matrix-gpt-bot:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Kubernetes (K3s)
|
||||||
|
|
||||||
|
1. Create a namespace and pull-secret for private registries if needed.
|
||||||
|
2. Deploy with a standard Deployment manifest, mounting environment variables for credentials.
|
||||||
|
|
||||||
|
## Command Reference
|
||||||
|
|
||||||
|
- `gpt: <message>` – Sends `<message>` to OpenAI and returns the response.
|
||||||
|
- `@gpt:domain <message>` – Alternate trigger via mention.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT © Your Name
|
||||||
|
|
67
bot/bot.go
Normal file
67
bot/bot.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"matrix-gpt-bot/openai"
|
||||||
|
|
||||||
|
mautrix "maunium.net/go/mautrix"
|
||||||
|
"maunium.net/go/mautrix/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterHandler sets up the GPT message handler on the Matrix client.
|
||||||
|
func RegisterHandler(client *mautrix.Client) {
|
||||||
|
syncer := client.Syncer.(*mautrix.DefaultSyncer)
|
||||||
|
|
||||||
|
// Compute the bot's local name (without '@' or domain) for prefix matching
|
||||||
|
fullID := string(client.UserID) // e.g. "@gpt:ewnix.net"
|
||||||
|
local := strings.SplitN(fullID, ":", 2)[0] // "@gpt"
|
||||||
|
local = strings.TrimPrefix(local, "@") // "gpt"
|
||||||
|
|
||||||
|
syncer.OnEventType(event.EventMessage, func(ctx context.Context, evt *event.Event) {
|
||||||
|
msg := evt.Content.AsMessage()
|
||||||
|
if msg.MsgType != event.MsgText {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body := msg.Body
|
||||||
|
lcBody := strings.ToLower(body)
|
||||||
|
var content string
|
||||||
|
|
||||||
|
// Check for "gpt: message" prefix
|
||||||
|
if strings.HasPrefix(lcBody, local+":") {
|
||||||
|
content = strings.TrimSpace(body[len(local)+1:])
|
||||||
|
|
||||||
|
// Or for full Matrix mention
|
||||||
|
} else if strings.Contains(body, fullID) {
|
||||||
|
content = strings.TrimSpace(strings.ReplaceAll(body, fullID, ""))
|
||||||
|
} else {
|
||||||
|
// Not addressed to bot
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing to send
|
||||||
|
if content == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve or initialize conversation history for this user
|
||||||
|
userID := string(evt.Sender)
|
||||||
|
history := GetHistory(userID)
|
||||||
|
history = append(history, openai.Message{Role: "user", Content: content})
|
||||||
|
|
||||||
|
// Ask OpenAI
|
||||||
|
resp, err := openai.Ask(history)
|
||||||
|
if err != nil {
|
||||||
|
client.SendText(context.Background(), evt.RoomID, "Error talking to ChatGPT: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
history = append(history, openai.Message{Role: "assistant", Content: resp})
|
||||||
|
SetHistory(userID, history)
|
||||||
|
|
||||||
|
// Send reply
|
||||||
|
client.SendText(context.Background(), evt.RoomID, resp)
|
||||||
|
})
|
||||||
|
}
|
18
bot/memory.go
Normal file
18
bot/memory.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"matrix-gpt-bot/openai"
|
||||||
|
)
|
||||||
|
|
||||||
|
// conversations stores per-user message history.
|
||||||
|
var conversations = map[string][]openai.Message{}
|
||||||
|
|
||||||
|
// GetHistory retrieves the conversation history for a given user.
|
||||||
|
func GetHistory(userID string) []openai.Message {
|
||||||
|
return conversations[userID]
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHistory saves the conversation history for a given user.
|
||||||
|
func SetHistory(userID string, history []openai.Message) {
|
||||||
|
conversations[userID] = history
|
||||||
|
}
|
19
config/config.go
Normal file
19
config/config.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
// Config holds Matrix and OpenAI credentials for the GPT bot.
|
||||||
|
type Config struct {
|
||||||
|
Homeserver string
|
||||||
|
BotUserID string
|
||||||
|
AccessToken string
|
||||||
|
OpenAIKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load returns hardcoded credentials; replace with your own values.
|
||||||
|
func Load() *Config {
|
||||||
|
return &Config{
|
||||||
|
Homeserver: "",
|
||||||
|
BotUserID: "", // replace with your bot's user ID
|
||||||
|
AccessToken: "", // replace with bot access token
|
||||||
|
OpenAIKey: "", // replace with your OpenAI key
|
||||||
|
}
|
||||||
|
}
|
25
go.mod
Normal file
25
go.mod
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
module matrix-gpt-bot
|
||||||
|
|
||||||
|
replace matrix-gpt-bot => .
|
||||||
|
|
||||||
|
go 1.24.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/sashabaranov/go-openai v1.39.1
|
||||||
|
maunium.net/go/mautrix v0.23.3
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/rs/zerolog v1.34.0 // indirect
|
||||||
|
github.com/tidwall/gjson v1.18.0 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
|
go.mau.fi/util v0.8.6 // indirect
|
||||||
|
golang.org/x/crypto v0.37.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||||
|
golang.org/x/sys v0.32.0 // indirect
|
||||||
|
)
|
48
go.sum
Normal file
48
go.sum
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
|
github.com/sashabaranov/go-openai v1.39.1 h1:TMD4w77Iy9WTFlgnjNaxbAASdsCJ9R/rMdzL+SN14oU=
|
||||||
|
github.com/sashabaranov/go-openai v1.39.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
|
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||||
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
|
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
|
go.mau.fi/util v0.8.6 h1:AEK13rfgtiZJL2YsNK+W4ihhYCuukcRom8WPP/w/L54=
|
||||||
|
go.mau.fi/util v0.8.6/go.mod h1:uNB3UTXFbkpp7xL1M/WvQks90B/L4gvbLpbS0603KOE=
|
||||||
|
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||||
|
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||||
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||||
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||||
|
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
maunium.net/go/mautrix v0.23.3 h1:U+fzdcLhFKLUm5gf2+Q0hEUqWkwDMRfvE+paUH9ogSk=
|
||||||
|
maunium.net/go/mautrix v0.23.3/go.mod h1:LX+3evXVKSvh/b43BVC3rkvN2qV7b0bkIV4fY7Snn/4=
|
44
main.go
Normal file
44
main.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"matrix-gpt-bot/bot"
|
||||||
|
"matrix-gpt-bot/config"
|
||||||
|
|
||||||
|
mautrix "maunium.net/go/mautrix"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Load configuration
|
||||||
|
cfg := config.Load()
|
||||||
|
|
||||||
|
// Initialize Matrix client
|
||||||
|
client, err := mautrix.NewClient(cfg.Homeserver, id.UserID(cfg.BotUserID), cfg.AccessToken)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create Matrix client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the GPT message handler
|
||||||
|
bot.RegisterHandler(client)
|
||||||
|
|
||||||
|
log.Println("Matrix GPT bot started. Press CTRL+C to exit.")
|
||||||
|
|
||||||
|
// Start syncing in a background goroutine
|
||||||
|
go func() {
|
||||||
|
if err := client.Sync(); err != nil {
|
||||||
|
log.Fatalf("Sync failed: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for termination signal
|
||||||
|
stop := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-stop
|
||||||
|
|
||||||
|
log.Println("Shutting down bot.")
|
||||||
|
}
|
47
openai/client.go
Normal file
47
openai/client.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package openai
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"matrix-gpt-bot/config"
|
||||||
|
|
||||||
|
goai "github.com/sashabaranov/go-openai"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Message represents a single message in the conversation.
|
||||||
|
type Message struct {
|
||||||
|
Role string `json:"role"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask sends the provided messages to the OpenAI API and returns the assistant's reply.
|
||||||
|
func Ask(messages []Message) (string, error) {
|
||||||
|
// Load OpenAI key from configuration
|
||||||
|
conf := config.Load()
|
||||||
|
client := goai.NewClient(conf.OpenAIKey)
|
||||||
|
|
||||||
|
// Build the chat history in the format expected by the API
|
||||||
|
var chatHistory []goai.ChatCompletionMessage
|
||||||
|
for _, m := range messages {
|
||||||
|
chatHistory = append(chatHistory, goai.ChatCompletionMessage{
|
||||||
|
Role: m.Role,
|
||||||
|
Content: m.Content,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the chat completion request
|
||||||
|
resp, err := client.CreateChatCompletion(
|
||||||
|
context.Background(),
|
||||||
|
goai.ChatCompletionRequest{
|
||||||
|
Model: "gpt-4.1",
|
||||||
|
Messages: chatHistory,
|
||||||
|
MaxTokens: 600,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the content of the first choice
|
||||||
|
return resp.Choices[0].Message.Content, nil
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user