First commit

This commit is contained in:
phlux 2025-05-09 04:43:15 +00:00
commit 7d1c046145
9 changed files with 384 additions and 0 deletions

33
Dockerfile Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}