Initial version of matrix logging bot.

master
Tomasz Polgrabia 2024-10-29 21:17:06 +01:00
parent b9016d8db5
commit fd49577ef8
4 changed files with 351 additions and 0 deletions

View File

@ -0,0 +1,2 @@
log/
state/

View File

@ -0,0 +1,23 @@
module matrix_demo1
go 1.22.0
require (
github.com/rs/zerolog v1.33.0
go.mau.fi/util v0.8.1
maunium.net/go/mautrix v0.21.1
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sys v0.26.0 // indirect
)

View File

@ -0,0 +1,45 @@
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 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/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.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.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 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/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.1 h1:Ga43cz6esQBYqcjZ/onRoVnYWoUwjWbsxVeJg2jOTSo=
go.mau.fi/util v0.8.1/go.mod h1:T1u/rD2rzidVrBLyaUdPpZiJdP/rsyi+aTzn0D+Q6wc=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
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.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.21.1 h1:Z+e448jtlY977iC1kokNJTH5kg2WmDpcQCqn+v9oZOA=
maunium.net/go/mautrix v0.21.1/go.mod h1:7F/S6XAdyc/6DW+Q7xyFXRSPb6IjfqMb1OMepQ8C8OE=

View File

@ -0,0 +1,281 @@
package main
import (
"bufio"
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"go.mau.fi/util/exzerolog"
"io"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"os"
"os/signal"
"regexp"
"sync"
"time"
)
const StateDataDir = "data"
const StateLogDir = "log"
const StateFilePath = StateDataDir + "/state.json"
const StateFiltersPath = StateDataDir + "/filters.json"
func syncFunc(client *mautrix.Client, syncCtx context.Context, syncStopWait *sync.WaitGroup) {
log.Info().Msg(fmt.Sprintf("Syncing try"))
err := client.SyncWithContext(syncCtx)
if err != nil && !errors.Is(err, context.Canceled) {
log.Error().Err(err).
Msg("Failed to sync")
}
defer syncStopWait.Done()
log.Info().Msg(fmt.Sprintf("Syncing try finished"))
}
type FileStore struct {
filterIds map[id.UserID]string
loadedFilterIds bool
nextBatchTokens map[id.UserID]string
loadedBatchTokens bool
}
func (f FileStore) SaveFilterID(_ context.Context, userID id.UserID, filterID string) error {
f.filterIds[userID] = filterID
err := saveValueToFile(StateFiltersPath, f.filterIds)
if err == nil {
f.loadedFilterIds = true
} else {
f.loadedFilterIds = false
}
return err
}
func saveValueToFile(fileName string, f map[id.UserID]string) error {
file, err := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
return err
}
writer := io.Writer(file)
encoder := json.NewEncoder(writer)
return encoder.Encode(f)
}
func (f FileStore) LoadFilterID(_ context.Context, userID id.UserID) (string, error) {
if !f.loadedFilterIds {
_, err := os.Stat(StateFiltersPath)
if err == nil {
err = loadStateFromFile(StateFiltersPath, &f.filterIds)
if err != nil {
log.Error().
Err(err).
Msg("Got an eror while reading filters")
}
}
f.loadedBatchTokens = true
}
s := f.filterIds[userID]
return s, nil
}
func (f FileStore) SaveNextBatch(ctx context.Context, userID id.UserID, nextBatchToken string) error {
f.nextBatchTokens[userID] = nextBatchToken
err := saveValueToFile(StateFilePath, f.nextBatchTokens)
if err == nil {
f.loadedBatchTokens = true
} else {
f.loadedBatchTokens = false
}
return err
}
func (f FileStore) LoadNextBatch(ctx context.Context, userID id.UserID) (string, error) {
if !f.loadedBatchTokens {
_, err := os.Stat(StateFilePath)
if err == nil {
err := loadStateFromFile(StateFilePath, &f.nextBatchTokens)
if err != nil {
log.Error().
Err(err).
Msg("Got an eror while reading state")
}
}
f.loadedBatchTokens = true
}
s := f.nextBatchTokens[userID]
return s, nil
}
func loadStateFromFile(path string, ref *map[id.UserID]string) error {
file, err := os.OpenFile(path, os.O_RDONLY, 0644)
if err != nil {
return err
}
decoder := json.NewDecoder(bufio.NewReader(file))
err = decoder.Decode(ref)
if err != nil {
return err
}
defer func(f2 *os.File) {
_ = f2.Close()
}(file)
return nil
}
func main() {
ce, cerr := regexp.Compile("[^a-zA-Z0-9 -_]+")
if cerr != nil {
panic("Error in compiling regexp " + cerr.Error())
}
var homeserver = flag.String("homeserver", "", "homeserver")
var userId = flag.String("userId", "", "userId")
var token = flag.String("token", "", "token")
var debug = flag.Bool("debug", false, "debug")
flag.Parse()
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
signal.Notify(c, os.Kill)
var user = id.NewUserID(*userId, *homeserver)
var client, err = mautrix.NewClient(*homeserver, user, *token)
if err != nil {
panic("Couldn't connect to homeserver " + err.Error())
}
log := zerolog.New(zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
w.Out = os.Stdout
w.TimeFormat = time.Stamp
})).With().Timestamp().Logger()
if !*debug {
log = log.Level(zerolog.DebugLevel)
}
exzerolog.SetupDefaults(&log)
client.Log = log
// var lastRoomId id.RoomID
var lastRoomName string
syncer := client.Syncer.(*mautrix.DefaultSyncer)
client.Store = FileStore{
loadedFilterIds: false,
filterIds: make(map[id.UserID]string),
loadedBatchTokens: false,
nextBatchTokens: make(map[id.UserID]string)}
roomNames := make(map[string]string)
filesNames := make(map[string]*os.File)
syncer.OnEventType(event.StateRoomName, func(ctx context.Context, evt *event.Event) {
roomNames[evt.RoomID.String()] = evt.Content.AsRoomName().Name
log.Info().
Str("sender", evt.Sender.String()).
Str("type", evt.Type.String()).
Str("id", evt.ID.String()).
Msg(fmt.Sprintf("Received sync state room name mapping %s -> %s",
evt.RoomID.String(),
evt.Content.AsRoomName().Name))
})
var lastRoomId id.RoomID
_ = os.MkdirAll(StateLogDir, 0644)
_ = os.MkdirAll(StateDataDir, 0644)
syncer.OnEventType(
event.EventMessage,
func(ctx context.Context, evt *event.Event) {
lastRoomId = evt.RoomID
lastRoomName = roomNames[lastRoomId.String()]
if lastRoomName == "" {
lastRoomName = lastRoomId.String()
}
var file *os.File
file = filesNames[lastRoomName]
if file == nil {
lastRoomName = string(ce.ReplaceAll([]byte(lastRoomName), []byte("_")))
channelLogPath := StateLogDir + "/" + lastRoomName + ".log"
file, err := os.OpenFile(channelLogPath, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
panic("Failed to open file " + lastRoomName)
}
_, err = file.Seek(0, io.SeekEnd)
if err != nil {
panic("Failed to seek file " + lastRoomName)
}
filesNames[lastRoomName] = file
if err != nil {
panic("Failed to create file " + lastRoomName)
}
}
file = filesNames[lastRoomName]
timestamp := evt.Timestamp
t := time.Unix(timestamp/1000, timestamp%1000*1000)
msg := evt.Content.AsMessage()
log.Info().
Str("sender", evt.Sender.String()).
Str("type", evt.Type.String()).
Str("id", evt.ID.String()).
Str("body", msg.Body).
Msg(fmt.Sprintf("Received sync message on channel %s (%s)",
lastRoomName, lastRoomId))
_, err := file.WriteString(t.String() + " " + "[" + evt.Sender.String() + "] " + msg.Body + "\n")
if err != nil {
panic("Failed to write to the file " + lastRoomName)
}
err = file.Sync()
if err != nil {
return
}
},
)
syncer.OnEventType(event.StateMember, func(ctx context.Context, evt *event.Event) {
if evt.GetStateKey() == client.UserID.String() && evt.Content.AsMember().Membership == event.MembershipInvite {
_, err := client.JoinRoomByID(ctx, evt.RoomID)
if err != nil {
log.Error().Err(err).
Str("room_id", evt.RoomID.String()).
Str("inviter", evt.Sender.String()).
Msg("Failed to join " + evt.RoomID.String() +
" + room after invite by " + evt.Sender.String())
} else {
log.Info().
Str("room_id", evt.RoomID.String()).
Str("inviter", evt.Sender.String()).
Msg("Succeeded to join " + evt.RoomID.String() +
" + room after invite by " + evt.Sender.String())
}
}
})
log.Info().Msg("Now running")
syncCtx, cancelSync := context.WithCancel(context.Background())
var syncStopWait sync.WaitGroup
syncStopWait.Add(1)
go syncFunc(client, syncCtx, &syncStopWait)
for sig := range c {
if sig == os.Interrupt {
break
}
}
cancelSync()
syncStopWait.Wait()
}