2023-05-17 19:51:45 +02:00
package bot
2023-05-16 20:07:05 +02:00
import (
"time"
"github.com/chzyer/readline"
"github.com/rs/zerolog"
2023-05-18 09:59:01 +02:00
"github.com/sashabaranov/go-openai"
2023-05-23 16:15:53 +02:00
"golang.org/x/exp/slog"
2023-05-16 20:07:05 +02:00
"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
2023-05-17 19:51:45 +02:00
OpenAIKey string
2023-05-16 20:07:05 +02:00
}
type Matrix struct {
2023-05-17 19:51:45 +02:00
config Config
readline * readline . Instance
client * mautrix . Client
cryptoHelper * cryptohelper . CryptoHelper
conversations Conversations
gptClient * GPT
2023-05-16 20:07:05 +02:00
}
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
2023-05-18 09:29:23 +02:00
} ) ) . With ( ) . Timestamp ( ) . Logger ( ) . Level ( zerolog . InfoLevel )
2023-05-16 20:07:05 +02:00
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
2023-05-17 19:51:45 +02:00
m . gptClient = NewGPT ( m . config . OpenAIKey )
m . conversations = make ( Conversations , 0 )
2023-05-16 20:07:05 +02:00
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 )
}
2023-05-23 16:15:53 +02:00
func ( m * Matrix ) InviteHandler ( logger * slog . Logger ) ( event . Type , mautrix . EventHandler ) {
2023-05-16 20:07:05 +02:00
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 )
2023-05-23 16:15:53 +02:00
if err != nil {
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 ( ) ) )
return
2023-05-16 20:07:05 +02:00
}
2023-05-23 16:15:53 +02:00
logger . Info ( "Joined room after invite" , slog . String ( "room_id" , evt . RoomID . String ( ) ) , slog . String ( "inviter" , evt . Sender . String ( ) ) )
2023-05-16 20:07:05 +02:00
}
}
}
2023-05-23 16:15:53 +02:00
func ( m * Matrix ) ResponseHandler ( logger * slog . Logger ) ( event . Type , mautrix . EventHandler ) {
2023-05-16 20:07:05 +02:00
return event . EventMessage , func ( source mautrix . EventSource , evt * event . Event ) {
2023-05-18 09:29:23 +02:00
content := evt . Content . AsMessage ( )
2023-05-18 10:38:40 +02:00
eventID := evt . ID
2023-05-23 16:15:53 +02:00
logger . Info ( "received message" , slog . String ( "content" , content . Body ) )
2023-05-18 10:38:40 +02:00
conv := m . conversations . FindByEventID ( eventID )
if conv != nil {
2023-05-23 16:15:53 +02:00
logger . Info ( "known message, ignoring" , slog . String ( "event_id" , eventID . String ( ) ) )
2023-05-18 10:38:40 +02:00
return
}
2023-05-18 09:59:01 +02:00
2023-05-18 10:38:40 +02:00
parentID := id . EventID ( "" )
if relatesTo := content . GetRelatesTo ( ) ; relatesTo != nil {
parentID = relatesTo . GetReplyTo ( )
}
if parentID != "" {
2023-05-23 16:15:53 +02:00
logger . Info ( "parent found, looking for conversation" , slog . String ( "parent_id" , parentID . String ( ) ) )
2023-05-18 10:38:40 +02:00
conv = m . conversations . FindByEventID ( parentID )
}
if conv != nil {
conv . Add ( Message {
EventID : eventID ,
ParentID : parentID ,
Role : openai . ChatMessageRoleUser ,
Content : content . Body ,
} )
2023-05-23 16:15:53 +02:00
logger . Info ( "found parent, appending message to conversation" , slog . String ( "event_id" , eventID . String ( ) ) )
2023-05-18 10:38:40 +02:00
} else {
2023-05-23 15:40:16 +02:00
conv = NewConversation ( eventID , content . Body )
2023-05-18 10:38:40 +02:00
m . conversations = append ( m . conversations , conv )
2023-05-23 16:15:53 +02:00
logger . Info ( "no parent found, starting new conversation" , slog . String ( "event_id" , eventID . String ( ) ) )
2023-05-18 10:38:40 +02:00
}
2023-05-18 09:59:01 +02:00
2023-05-18 10:38:40 +02:00
if evt . Sender != id . UserID ( m . config . UserID ) {
2023-05-18 09:59:01 +02:00
// get reply from GPT
reply , err := m . gptClient . Complete ( conv )
2023-05-16 20:07:05 +02:00
if err != nil {
2023-05-23 16:15:53 +02:00
logger . Error ( "failed to get reply from openai" , slog . String ( "err" , err . Error ( ) ) )
2023-05-16 20:07:05 +02:00
return
}
2023-05-18 09:59:01 +02:00
formattedReply := format . RenderMarkdown ( reply , true , false )
formattedReply . RelatesTo = & event . RelatesTo {
2023-05-18 09:29:23 +02:00
InReplyTo : & event . InReplyTo {
EventID : eventID ,
} ,
}
2023-05-18 10:38:40 +02:00
if _ , err := m . client . SendMessageEvent ( evt . RoomID , event . EventMessage , & formattedReply ) ; err != nil {
2023-05-23 16:15:53 +02:00
logger . Error ( "failed to send message" , slog . String ( "err" , err . Error ( ) ) )
2023-05-18 09:29:23 +02:00
return
}
2023-05-23 16:15:53 +02:00
if len ( reply ) > 30 {
reply = reply [ : 30 ] + "..."
}
logger . Info ( "sent reply" , slog . String ( "parent_id" , eventID . String ( ) ) , slog . String ( "content" , reply ) )
2023-05-16 20:07:05 +02:00
}
}
}