Compare commits
10 Commits
5bb6427b1d
...
988ca34644
Author | SHA1 | Date |
---|---|---|
Erik Winter | 988ca34644 | |
Erik Winter | ee638a6fc8 | |
Erik Winter | b241fc6961 | |
Erik Winter | fbc6aef783 | |
Erik Winter | bfcba9a464 | |
Erik Winter | 815b7e0808 | |
Erik Winter | 988951970b | |
Erik Winter | ae6d0532bd | |
Erik Winter | 681ad81358 | |
Erik Winter | 4fb1ec915b |
|
@ -1,2 +1,3 @@
|
|||
.idea
|
||||
*.db
|
||||
*.db
|
||||
*.toml
|
6
Makefile
6
Makefile
|
@ -1,4 +1,4 @@
|
|||
docker-push:
|
||||
docker build . -t matrix-bots
|
||||
docker tag matrix-bots registry.ewintr.nl/matrix-gptzoo
|
||||
docker push registry.ewintr.nl/matrix-gptzoo
|
||||
docker build . -t matrix-gptzoo:latest
|
||||
docker tag matrix-gptzoo:latest registry.ewintr.nl/matrix-gptzoo:latest
|
||||
docker push registry.ewintr.nl/matrix-gptzoo:latest
|
|
@ -0,0 +1,53 @@
|
|||
# Matrix-GPTZoo
|
||||
|
||||
A Matrix bot version of ChatGPT, with multiple configurable prompts.
|
||||
|
||||
The bot will log in with a different user for each prompt, making it possible to create a chat room full of AI assistants you can ask questions.
|
||||
|
||||
Bots will only answer questions specifically addressed to them, but it is also possible to configure one to answer questions that are not addressed to a specific bot. Continuing a conversation can be done by replying to an answer.
|
||||
|
||||
![screenshot](gptzoo-screenshot.png)
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration consists of two parts: a toml file that defines the prompt and environment variables for the passwords and keys. Matching environment variables to the bots in the toml is done on the Matrix user ID.
|
||||
|
||||
This probably best explained with an example:
|
||||
|
||||
```toml
|
||||
[[Bot]]
|
||||
DBPath = "gpt4-bot.db"
|
||||
Pickle = "hi-there"
|
||||
Homeserver = "https://ewintr.nl"
|
||||
UserID = "@chatgpt4:ewintr.nl"
|
||||
UserDisplayName = "ChatGPT4"
|
||||
SystemPrompt = "You are an encouraging chatbot that likes to help people by providing answers in a creative and fun way."
|
||||
AnswerUnaddressed = true
|
||||
|
||||
[[Bot]]
|
||||
DBPath = "go-bot.db"
|
||||
Pickle = "hi-there"
|
||||
Homeserver = "https://ewintr.nl"
|
||||
UserID = "@gogpt:ewintr.nl"
|
||||
UserDisplayName = "GoGPT"
|
||||
SystemPrompt = "You are an export on the programming language Go, you assume that any programming question is about Go and your answers are aimed at senior level developers."
|
||||
AnswerUnaddressed = false
|
||||
```
|
||||
|
||||
Together with:
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=secret
|
||||
|
||||
CONFIG_PATH=/config.toml
|
||||
|
||||
MATRIX_BOT0_ID=@chatgpt4:ewintr.nl
|
||||
MATRIX_BOT0_PASSWORD=secret
|
||||
MATRIX_BOT0_ACCESS_KEY=secret
|
||||
|
||||
MATRIX_BOT1_ID=@gogpt:ewintr.nl
|
||||
MATRIX_BOT1_PASSWORD=secret
|
||||
MATRIX_BOT1_ACCESS_KEY=secret
|
||||
|
||||
MATRIX_ACCEPT_INVITES=false
|
||||
```
|
|
@ -0,0 +1,234 @@
|
|||
package bot
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"golang.org/x/exp/slog"
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/crypto/cryptohelper"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/format"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
var (
|
||||
botNames = []string{}
|
||||
mu = &sync.Mutex{}
|
||||
)
|
||||
|
||||
func BotNameAppend(name string) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
botNames = append(botNames, name)
|
||||
}
|
||||
|
||||
type ConfigOpenAI struct {
|
||||
APIKey string
|
||||
}
|
||||
|
||||
type ConfigBot struct {
|
||||
DBPath string
|
||||
Pickle string
|
||||
Homeserver string
|
||||
UserID string
|
||||
UserAccessKey string
|
||||
UserPassword string
|
||||
UserDisplayName string
|
||||
SystemPrompt string
|
||||
AnswerUnaddressed bool
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
OpenAI ConfigOpenAI `toml:"openai"`
|
||||
Bots []ConfigBot `toml:"bot"`
|
||||
}
|
||||
|
||||
type Bot struct {
|
||||
openaiKey string
|
||||
config ConfigBot
|
||||
client *mautrix.Client
|
||||
cryptoHelper *cryptohelper.CryptoHelper
|
||||
characters []Character
|
||||
conversations Conversations
|
||||
gptClient *GPT
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func New(openaiKey string, cfg ConfigBot, logger *slog.Logger) *Bot {
|
||||
return &Bot{
|
||||
openaiKey: openaiKey,
|
||||
config: cfg,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Bot) Init(acceptInvites bool) error {
|
||||
client, err := mautrix.NewClient(m.config.Homeserver, id.UserID(m.config.UserID), m.config.UserAccessKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var oei mautrix.OldEventIgnorer
|
||||
oei.Register(client.Syncer.(mautrix.ExtensibleSyncer))
|
||||
m.client = client
|
||||
m.cryptoHelper, err = cryptohelper.NewCryptoHelper(client, []byte(m.config.Pickle), m.config.DBPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.cryptoHelper.LoginAs = &mautrix.ReqLogin{
|
||||
Type: mautrix.AuthTypePassword,
|
||||
Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: m.config.UserID},
|
||||
Password: m.config.UserPassword,
|
||||
}
|
||||
if err := m.cryptoHelper.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
m.client.Crypto = m.cryptoHelper
|
||||
m.gptClient = NewGPT(m.openaiKey)
|
||||
m.conversations = make(Conversations, 0)
|
||||
if acceptInvites {
|
||||
m.AddEventHandler(m.InviteHandler())
|
||||
}
|
||||
m.AddEventHandler(m.ResponseHandler())
|
||||
|
||||
m.config.UserDisplayName = strings.ToLower(m.config.UserDisplayName)
|
||||
BotNameAppend(m.config.UserDisplayName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Bot) Run() error {
|
||||
if err := m.client.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Bot) Close() error {
|
||||
if err := m.client.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.cryptoHelper.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Bot) AddEventHandler(eventType event.Type, handler mautrix.EventHandler) {
|
||||
syncer := m.client.Syncer.(*mautrix.DefaultSyncer)
|
||||
syncer.OnEventType(eventType, handler)
|
||||
}
|
||||
|
||||
func (m *Bot) InviteHandler() (event.Type, mautrix.EventHandler) {
|
||||
return event.StateMember, func(source mautrix.EventSource, evt *event.Event) {
|
||||
if evt.GetStateKey() == m.client.UserID.String() && evt.Content.AsMember().Membership == event.MembershipInvite {
|
||||
_, err := m.client.JoinRoomByID(evt.RoomID)
|
||||
if err != nil {
|
||||
m.logger.Error("failed to join room after invite", slog.String("err", err.Error()), slog.String("room_id", evt.RoomID.String()), slog.String("inviter", evt.Sender.String()), slog.String("bot", m.config.UserDisplayName))
|
||||
return
|
||||
}
|
||||
|
||||
m.logger.Info("joined room after invite", slog.String("room_id", evt.RoomID.String()), slog.String("inviter", evt.Sender.String()), slog.String("bot", m.config.UserDisplayName))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Bot) ResponseHandler() (event.Type, mautrix.EventHandler) {
|
||||
return event.EventMessage, func(source mautrix.EventSource, evt *event.Event) {
|
||||
content := evt.Content.AsMessage()
|
||||
eventID := evt.ID
|
||||
m.logger.Info("received message", slog.String("content", content.Body))
|
||||
|
||||
// ignore if the message is already recorded
|
||||
if conv := m.conversations.FindByEventID(eventID); conv != nil {
|
||||
m.logger.Info("known message, ignoring", slog.String("event_id", eventID.String()), slog.String("bot", m.config.UserDisplayName))
|
||||
return
|
||||
}
|
||||
|
||||
// ignore if the message is sent by the bot itself
|
||||
if evt.Sender == id.UserID(m.config.UserID) {
|
||||
m.logger.Info("message sent by bot itself, ignoring", slog.String("event_id", eventID.String()), slog.String("bot", m.config.UserDisplayName))
|
||||
return
|
||||
}
|
||||
|
||||
var conv *Conversation
|
||||
// find out if it is a reply to a known conversation
|
||||
parentID := id.EventID("")
|
||||
var hasParent bool
|
||||
if relatesTo := content.GetRelatesTo(); relatesTo != nil {
|
||||
if parentID = relatesTo.GetReplyTo(); parentID != "" {
|
||||
hasParent = true
|
||||
m.logger.Info("message is a reply", slog.String("parent_id", parentID.String()))
|
||||
if c := m.conversations.FindByEventID(parentID); c != nil {
|
||||
m.logger.Info("found parent, appending message to conversation", slog.String("event_id", eventID.String()), slog.String("bot", m.config.UserDisplayName))
|
||||
c.Add(Message{
|
||||
EventID: eventID,
|
||||
ParentID: parentID,
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: content.Body,
|
||||
})
|
||||
conv = c
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.logger.Info(content.Body)
|
||||
addressedTo, _, isAddressed := strings.Cut(content.Body, ": ")
|
||||
addressedTo = strings.TrimSpace(strings.ToLower(addressedTo))
|
||||
if strings.Contains(addressedTo, " ") {
|
||||
isAddressed = false // only display names without spaces, otherwise no way to know if it's a name or not
|
||||
}
|
||||
|
||||
// find out if message is a new question addressed to the bot
|
||||
if conv == nil && isAddressed && addressedTo == m.config.UserDisplayName {
|
||||
m.logger.Info("message is addressed to bot", slog.String("event_id", eventID.String()), slog.String("bot", m.config.UserDisplayName))
|
||||
conv = NewConversation(eventID, m.config.SystemPrompt, content.Body)
|
||||
m.conversations = append(m.conversations, conv)
|
||||
}
|
||||
// find out if the message is addressed to no-one and this bot answers those
|
||||
if conv == nil && !isAddressed && !hasParent && m.config.AnswerUnaddressed {
|
||||
m.logger.Info("message is addressed to no-one", slog.String("event_id", eventID.String()), slog.String("bot", m.config.UserDisplayName))
|
||||
conv = NewConversation(eventID, m.config.SystemPrompt, content.Body)
|
||||
m.conversations = append(m.conversations, conv)
|
||||
}
|
||||
|
||||
if conv == nil {
|
||||
m.logger.Info("apparently not for us, ignoring", slog.String("event_id", eventID.String()), slog.String("bot", m.config.UserDisplayName))
|
||||
return
|
||||
}
|
||||
|
||||
// get reply from GPT
|
||||
reply, err := m.gptClient.Complete(conv)
|
||||
if err != nil {
|
||||
m.logger.Error("failed to get reply from openai", slog.String("err", err.Error()), slog.String("bot", m.config.UserDisplayName))
|
||||
return
|
||||
}
|
||||
|
||||
formattedReply := format.RenderMarkdown(reply, true, false)
|
||||
formattedReply.RelatesTo = &event.RelatesTo{
|
||||
InReplyTo: &event.InReplyTo{
|
||||
EventID: eventID,
|
||||
},
|
||||
}
|
||||
res, err := m.client.SendMessageEvent(evt.RoomID, event.EventMessage, &formattedReply)
|
||||
if err != nil {
|
||||
m.logger.Error("failed to send message", slog.String("err", err.Error()), slog.String("bot", m.config.UserDisplayName))
|
||||
return
|
||||
}
|
||||
conv.Add(Message{
|
||||
EventID: res.EventID,
|
||||
ParentID: eventID,
|
||||
Role: openai.ChatMessageRoleAssistant,
|
||||
Content: reply,
|
||||
})
|
||||
|
||||
if len(reply) > 30 {
|
||||
reply = reply[:30] + "..."
|
||||
}
|
||||
m.logger.Info("sent reply", slog.String("parent_id", eventID.String()), slog.String("content", reply), slog.String("bot", m.config.UserDisplayName))
|
||||
}
|
||||
}
|
|
@ -2,10 +2,17 @@ package bot
|
|||
|
||||
import (
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
const systemPrompt = "You are a chatbot that helps people by responding to their questions with short messages."
|
||||
type Character struct {
|
||||
UserID string
|
||||
Password string
|
||||
AccessKey string
|
||||
Prompt string
|
||||
client *mautrix.Client
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
EventID id.EventID
|
||||
|
@ -18,7 +25,7 @@ type Conversation struct {
|
|||
Messages []Message
|
||||
}
|
||||
|
||||
func NewConversation(id id.EventID, question string) *Conversation {
|
||||
func NewConversation(id id.EventID, systemPrompt, question string) *Conversation {
|
||||
return &Conversation{
|
||||
Messages: []Message{
|
||||
{
|
||||
|
|
179
bot/matrix.go
179
bot/matrix.go
|
@ -1,179 +0,0 @@
|
|||
package bot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/chzyer/readline"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/crypto/cryptohelper"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/format"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Homeserver string
|
||||
UserID string
|
||||
UserAccessKey string
|
||||
UserPassword string
|
||||
DBPath string
|
||||
Pickle string
|
||||
OpenAIKey string
|
||||
}
|
||||
|
||||
type Matrix struct {
|
||||
config Config
|
||||
readline *readline.Instance
|
||||
client *mautrix.Client
|
||||
cryptoHelper *cryptohelper.CryptoHelper
|
||||
conversations Conversations
|
||||
gptClient *GPT
|
||||
}
|
||||
|
||||
func New(cfg Config) *Matrix {
|
||||
return &Matrix{
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Matrix) Init() error {
|
||||
client, err := mautrix.NewClient(m.config.Homeserver, id.UserID(m.config.UserID), m.config.UserAccessKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var oei mautrix.OldEventIgnorer
|
||||
oei.Register(client.Syncer.(mautrix.ExtensibleSyncer))
|
||||
m.client = client
|
||||
|
||||
m.client.Log = zerolog.New(zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
|
||||
w.TimeFormat = time.Stamp
|
||||
})).With().Timestamp().Logger().Level(zerolog.InfoLevel)
|
||||
|
||||
m.cryptoHelper, err = cryptohelper.NewCryptoHelper(client, []byte(m.config.Pickle), m.config.DBPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.cryptoHelper.LoginAs = &mautrix.ReqLogin{
|
||||
Type: mautrix.AuthTypePassword,
|
||||
Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: m.config.UserID},
|
||||
Password: m.config.UserPassword,
|
||||
}
|
||||
if err := m.cryptoHelper.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
m.client.Crypto = m.cryptoHelper
|
||||
|
||||
m.gptClient = NewGPT(m.config.OpenAIKey)
|
||||
|
||||
m.conversations = make(Conversations, 0)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Matrix) Run() error {
|
||||
if err := m.client.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Matrix) Close() error {
|
||||
if err := m.client.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.cryptoHelper.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Matrix) AddEventHandler(eventType event.Type, handler mautrix.EventHandler) {
|
||||
syncer := m.client.Syncer.(*mautrix.DefaultSyncer)
|
||||
syncer.OnEventType(eventType, handler)
|
||||
}
|
||||
|
||||
func (m *Matrix) InviteHandler() (event.Type, mautrix.EventHandler) {
|
||||
return event.StateMember, func(source mautrix.EventSource, evt *event.Event) {
|
||||
if evt.GetStateKey() == m.client.UserID.String() && evt.Content.AsMember().Membership == event.MembershipInvite {
|
||||
_, err := m.client.JoinRoomByID(evt.RoomID)
|
||||
if err == nil {
|
||||
m.client.Log.Info().
|
||||
Str("room_id", evt.RoomID.String()).
|
||||
Str("inviter", evt.Sender.String()).
|
||||
Msg("Joined room after invite")
|
||||
} else {
|
||||
m.client.Log.Error().Err(err).
|
||||
Str("room_id", evt.RoomID.String()).
|
||||
Str("inviter", evt.Sender.String()).
|
||||
Msg("Failed to join room after invite")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Matrix) ResponseHandler() (event.Type, mautrix.EventHandler) {
|
||||
return event.EventMessage, func(source mautrix.EventSource, evt *event.Event) {
|
||||
content := evt.Content.AsMessage()
|
||||
eventID := evt.ID
|
||||
m.client.Log.Info().
|
||||
Str("content", content.Body).
|
||||
Msg("received message")
|
||||
|
||||
conv := m.conversations.FindByEventID(eventID)
|
||||
if conv != nil {
|
||||
m.client.Log.Info().
|
||||
Str("event_id", eventID.String()).
|
||||
Msg("already known message, ignoring")
|
||||
return
|
||||
}
|
||||
|
||||
parentID := id.EventID("")
|
||||
if relatesTo := content.GetRelatesTo(); relatesTo != nil {
|
||||
parentID = relatesTo.GetReplyTo()
|
||||
}
|
||||
if parentID != "" {
|
||||
m.client.Log.Info().Msg("parent found, looking for conversation")
|
||||
conv = m.conversations.FindByEventID(parentID)
|
||||
}
|
||||
if conv != nil {
|
||||
conv.Add(Message{
|
||||
EventID: eventID,
|
||||
ParentID: parentID,
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: content.Body,
|
||||
})
|
||||
m.client.Log.Info().Msg("found parent, appending message to conversation")
|
||||
} else {
|
||||
conv = NewConversation(eventID, content.Body)
|
||||
m.conversations = append(m.conversations, conv)
|
||||
m.client.Log.Info().Msg("no parent found, starting new conversation")
|
||||
}
|
||||
|
||||
if evt.Sender != id.UserID(m.config.UserID) {
|
||||
// get reply from GPT
|
||||
reply, err := m.gptClient.Complete(conv)
|
||||
if err != nil {
|
||||
m.client.Log.Error().Err(err).Msg("OpenAI API returned with ")
|
||||
return
|
||||
}
|
||||
|
||||
formattedReply := format.RenderMarkdown(reply, true, false)
|
||||
formattedReply.RelatesTo = &event.RelatesTo{
|
||||
InReplyTo: &event.InReplyTo{
|
||||
EventID: eventID,
|
||||
},
|
||||
}
|
||||
if _, err := m.client.SendMessageEvent(evt.RoomID, event.EventMessage, &formattedReply); err != nil {
|
||||
m.client.Log.Err(err).Msg("failed to send message")
|
||||
return
|
||||
}
|
||||
|
||||
m.client.Log.Info().Str("message", fmt.Sprintf("%+v", formattedReply.Body)).Msg("Sent reply")
|
||||
}
|
||||
}
|
||||
}
|
1
go.mod
1
go.mod
|
@ -12,6 +12,7 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/tidwall/gjson v1.14.4 // indirect
|
||||
|
|
2
go.sum
2
go.sum
|
@ -1,3 +1,5 @@
|
|||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 216 KiB |
64
main.go
64
main.go
|
@ -1,10 +1,12 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"ewintr.nl/matrix-bots/bot"
|
||||
"github.com/BurntSushi/toml"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
@ -12,29 +14,63 @@ import (
|
|||
func main() {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
|
||||
|
||||
matrixClient := bot.New(bot.Config{
|
||||
Homeserver: getParam("MATRIX_HOMESERVER", "http://localhost"),
|
||||
UserID: getParam("MATRIX_USER_ID", "@bot:localhost"),
|
||||
UserPassword: getParam("MATRIX_PASSWORD", "secret"),
|
||||
UserAccessKey: getParam("MATRIX_ACCESS_KEY", "secret"),
|
||||
DBPath: getParam("BOT_DB_PATH", "bot.db"),
|
||||
Pickle: getParam("BOT_PICKLE", "scrambled"),
|
||||
OpenAIKey: getParam("OPENAI_API_KEY", "no key"),
|
||||
})
|
||||
|
||||
if err := matrixClient.Init(); err != nil {
|
||||
var config bot.Config
|
||||
if _, err := toml.DecodeFile(getParam("CONFIG_PATH", "conf.toml"), &config); err != nil {
|
||||
logger.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
go matrixClient.Run()
|
||||
type Credentials struct {
|
||||
Password string
|
||||
AccessKey string
|
||||
}
|
||||
credentials := make(map[string]Credentials)
|
||||
for i := 0; i < len(config.Bots); i++ {
|
||||
user := getParam(fmt.Sprintf("MATRIX_BOT%d_ID", i), "")
|
||||
if user == "" {
|
||||
logger.Error("missing user id", slog.Int("user", i))
|
||||
os.Exit(1)
|
||||
}
|
||||
credentials[user] = Credentials{
|
||||
Password: getParam(fmt.Sprintf("MATRIX_BOT%d_PASSWORD", i), ""),
|
||||
AccessKey: getParam(fmt.Sprintf("MATRIX_BOT%d_ACCESSKEY", i), ""),
|
||||
}
|
||||
}
|
||||
for i, bc := range config.Bots {
|
||||
creds, ok := credentials[bc.UserID]
|
||||
if !ok {
|
||||
logger.Error("missing credentials", slog.Int("user", i))
|
||||
os.Exit(1)
|
||||
}
|
||||
config.Bots[i].UserPassword = creds.Password
|
||||
config.Bots[i].UserAccessKey = creds.AccessKey
|
||||
}
|
||||
|
||||
matrixClient.AddEventHandler(matrixClient.InviteHandler())
|
||||
matrixClient.AddEventHandler(matrixClient.ResponseHandler())
|
||||
config.OpenAI = bot.ConfigOpenAI{
|
||||
APIKey: getParam("OPENAI_API_KEY", ""),
|
||||
}
|
||||
|
||||
var acceptInvites bool
|
||||
if getParam("MATRIX_ACCEPT_INVITES", "false") == "true" {
|
||||
acceptInvites = true
|
||||
}
|
||||
|
||||
logger.Info("loaded config", slog.Int("bots", len(config.Bots)))
|
||||
|
||||
for _, bc := range config.Bots {
|
||||
b := bot.New(config.OpenAI.APIKey, bc, logger)
|
||||
if err := b.Init(acceptInvites); err != nil {
|
||||
logger.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
go b.Run()
|
||||
logger.Info("started bot", slog.String("name", bc.UserDisplayName))
|
||||
}
|
||||
|
||||
done := make(chan os.Signal)
|
||||
signal.Notify(done, os.Interrupt)
|
||||
<-done
|
||||
|
||||
logger.Info("service stopped")
|
||||
}
|
||||
|
||||
func getParam(name, def string) string {
|
||||
|
|
Loading…
Reference in New Issue