2023-06-25 15:53:40 +02:00
package bot
import (
2023-07-11 15:34:03 +02:00
"regexp"
2023-06-25 15:53:40 +02:00
_ "github.com/mattn/go-sqlite3"
"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"
)
type MatrixConfig struct {
Homeserver string
UserID string
UserAccessKey string
UserPassword string
RoomID string
DBPath string
Pickle string
AcceptInvites bool
}
type Bot struct {
config MatrixConfig
client * mautrix . Client
cryptoHelper * cryptohelper . CryptoHelper
kagiCient * Kagi
logger * slog . Logger
2023-07-11 15:34:03 +02:00
messages map [ string ] string
2023-06-25 15:53:40 +02:00
}
func NewBot ( cfg MatrixConfig , kagi * Kagi , logger * slog . Logger ) * Bot {
return & Bot {
config : cfg ,
kagiCient : kagi ,
logger : logger ,
2023-07-11 15:34:03 +02:00
messages : make ( map [ string ] string ) ,
2023-06-25 15:53:40 +02:00
}
}
func ( m * Bot ) 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 . 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
if m . config . AcceptInvites {
m . AddEventHandler ( m . InviteHandler ( ) )
}
2023-07-11 15:34:03 +02:00
m . AddEventHandler ( m . ReactionHandler ( ) )
m . AddEventHandler ( m . MessageHandler ( ) )
2023-06-25 15:53:40 +02:00
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 && evt . RoomID . String ( ) == m . config . RoomID {
_ , 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 ( ) ) )
return
}
m . logger . Info ( "joined room after invite" , slog . String ( "room_id" , evt . RoomID . String ( ) ) , slog . String ( "inviter" , evt . Sender . String ( ) ) )
}
}
}
2023-07-11 15:34:03 +02:00
func ( m * Bot ) MessageHandler ( ) ( event . Type , mautrix . EventHandler ) {
return event . EventMessage , func ( source mautrix . EventSource , evt * event . Event ) {
2023-06-25 15:53:40 +02:00
content := evt . Content . AsMessage ( )
eventID := evt . ID
m . logger . Info ( "received message" , slog . String ( "content" , content . Body ) )
2023-07-03 10:35:07 +02:00
// 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 ( ) ) )
return
}
2023-07-11 15:34:03 +02:00
url , ok := findFirstURL ( content . Body )
if ! ok {
m . logger . Info ( "message does not contain url, ignoring" , slog . String ( "event_id" , eventID . String ( ) ) )
return
}
m . messages [ eventID . String ( ) ] = url
m . logger . Info ( "saved url" , slog . String ( "event_id" , eventID . String ( ) ) , slog . String ( "url" , url ) )
}
}
func ( m * Bot ) ReactionHandler ( ) ( event . Type , mautrix . EventHandler ) {
return event . EventReaction , func ( source mautrix . EventSource , evt * event . Event ) {
eventID := evt . ID
// 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 ( ) ) )
return
}
rel := evt . Content . AsReaction ( ) . GetRelatesTo ( )
relID := rel . GetAnnotationID ( )
relKey := rel . GetAnnotationKey ( )
m . logger . Info ( "received reaction" , slog . String ( "event_id" , eventID . String ( ) ) , slog . String ( "rel_id" , relID . String ( ) ) , slog . String ( "rel_key" , relKey ) )
2023-07-11 16:45:25 +02:00
if relKey != ` 🗒️ ` && relKey != ` 🗒 ` { // different clients have different emoji
m . logger . Info ( "reaction is not 🗒️ or 🗒, ignoring" , slog . String ( "event_id" , eventID . String ( ) ) )
2023-07-11 15:34:03 +02:00
return
}
2023-06-25 15:53:40 +02:00
2023-07-11 15:34:03 +02:00
wantedURL , ok := m . messages [ relID . String ( ) ]
2023-07-11 16:45:25 +02:00
var summary string
2023-07-11 15:34:03 +02:00
if ! ok {
2023-07-11 16:45:25 +02:00
summary = "could not find referenced message in ternal storage, or referenced message does not contain url"
2023-07-11 15:34:03 +02:00
m . logger . Info ( "referenced message is not known or does not contain url, ignoring" , slog . String ( "event_id" , eventID . String ( ) ) )
2023-07-11 16:45:25 +02:00
} else {
var err error
summary , err = m . kagiCient . Summarize ( wantedURL )
if err != nil {
m . logger . Error ( "failed to summarize" , slog . String ( "err" , err . Error ( ) ) )
return
}
2023-07-11 15:34:03 +02:00
}
reply := summary
2023-06-25 15:53:40 +02:00
formattedReply := format . RenderMarkdown ( reply , true , false )
formattedReply . RelatesTo = & event . RelatesTo {
InReplyTo : & event . InReplyTo {
2023-07-11 15:34:03 +02:00
EventID : relID ,
2023-06-25 15:53:40 +02:00
} ,
}
2023-07-11 15:34:03 +02:00
if _ , err := m . client . SendMessageEvent ( evt . RoomID , event . EventMessage , & formattedReply ) ; err != nil {
2023-06-25 15:53:40 +02:00
m . logger . Error ( "failed to send message" , slog . String ( "err" , err . Error ( ) ) )
return
}
if len ( reply ) > 30 {
reply = reply [ : 30 ] + "..."
}
2023-07-11 15:34:03 +02:00
m . logger . Info ( "sent reply" , slog . String ( "parent_id" , eventID . String ( ) ) , slog . String ( "msg" , reply ) )
}
}
func findFirstURL ( s string ) ( string , bool ) {
re := regexp . MustCompile ( ` https?\:\/\/[^ \)]* ` )
match := re . FindString ( s )
if match == "" {
return "" , false
2023-06-25 15:53:40 +02:00
}
2023-07-11 15:34:03 +02:00
return match , true
2023-06-25 15:53:40 +02:00
}