This commit is contained in:
Aine
2022-08-21 18:41:35 +03:00
commit c4b7a16e21
22 changed files with 1745 additions and 0 deletions

71
bot/bot.go Normal file
View File

@@ -0,0 +1,71 @@
package bot
import (
"context"
"fmt"
"github.com/getsentry/sentry-go"
"gitlab.com/etke.cc/go/logger"
"gitlab.com/etke.cc/linkpearl"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
// Bot represents matrix bot
type Bot struct {
prefix string
domain string
log *logger.Logger
lp *linkpearl.Linkpearl
}
// New creates a new matrix bot
func New(lp *linkpearl.Linkpearl, log *logger.Logger, prefix, domain string) *Bot {
return &Bot{
prefix: prefix,
domain: domain,
log: log,
lp: lp,
}
}
// Error message to the log and matrix room
func (b *Bot) Error(ctx context.Context, roomID id.RoomID, message string, args ...interface{}) {
b.log.Error(message, args...)
if sentry.HasHubOnContext(ctx) {
sentry.GetHubFromContext(ctx).CaptureException(fmt.Errorf(message, args...))
} else {
sentry.CaptureException(fmt.Errorf(message, args...))
}
if roomID != "" {
// nolint // if something goes wrong here nobody can help...
b.lp.Send(roomID, &event.MessageEventContent{
MsgType: event.MsgNotice,
Body: "ERROR: " + fmt.Sprintf(message, args...),
})
}
}
// Start performs matrix /sync
func (b *Bot) Start() error {
if err := b.migrate(); err != nil {
return err
}
if err := b.lp.GetClient().SetPresence(event.PresenceOnline); err != nil {
return err
}
b.initSync()
b.log.Info("Postmoogle has been started")
return b.lp.GetClient().Sync()
}
// Stop the bot
func (b *Bot) Stop() {
err := b.lp.GetClient().SetPresence(event.PresenceOffline)
if err != nil {
b.log.Error("cannot set presence = offline: %v", err)
}
b.lp.GetClient().StopSync()
}

60
bot/command.go Normal file
View File

@@ -0,0 +1,60 @@
package bot
import (
"context"
"strings"
"github.com/getsentry/sentry-go"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
)
var commands = map[string]string{
"mailbox": "Get or set mailbox of that room",
"help": "Get help",
}
func (b *Bot) handleCommand(ctx context.Context, evt *event.Event, command []string) {
if _, ok := commands[command[0]]; !ok {
return
}
switch command[0] {
case "help":
b.sendHelp(ctx, evt.RoomID)
case "mailbox":
b.handleMailbox(ctx, evt, command)
}
}
func (b *Bot) parseCommand(message string) []string {
if message == "" {
return nil
}
message = strings.Replace(message, b.prefix+" ", "", 1)
return strings.Split(message, " ")
}
func (b *Bot) sendHelp(ctx context.Context, roomID id.RoomID) {
span := sentry.StartSpan(ctx, "http.server", sentry.TransactionName("sendHelp"))
defer span.Finish()
var msg strings.Builder
msg.WriteString("the following commands are supported:\n\n")
for name, desc := range commands {
msg.WriteString("* **")
msg.WriteString(name)
msg.WriteString("** - ")
msg.WriteString(desc)
msg.WriteString("\n")
}
content := format.RenderMarkdown(msg.String(), true, true)
content.MsgType = event.MsgNotice
_, err := b.lp.Send(roomID, content)
if err != nil {
b.Error(span.Context(), roomID, "cannot send message: %v", err)
}
}

125
bot/data.go Normal file
View File

@@ -0,0 +1,125 @@
package bot
import (
"context"
"github.com/getsentry/sentry-go"
"maunium.net/go/mautrix/id"
)
const settingskey = "cc.etke.postmoogle.settings"
var migrations = []string{
`
CREATE TABLE IF NOT EXISTS settings (
room_id VARCHAR(255),
mailbox VARCHAR(255)
)
`,
}
// settings of a room
type settings struct {
Mailbox string
}
func (b *Bot) migrate() error {
b.log.Debug("migrating database...")
tx, beginErr := b.lp.GetDB().Begin()
if beginErr != nil {
b.log.Error("cannot begin transaction: %v", beginErr)
return beginErr
}
for _, query := range migrations {
_, execErr := tx.Exec(query)
if execErr != nil {
b.log.Error("cannot apply migration: %v", execErr)
// nolint // we already have the execErr to return
tx.Rollback()
return execErr
}
}
commitErr := tx.Commit()
if commitErr != nil {
b.log.Error("cannot commit transaction: %v", commitErr)
// nolint // we already have the commitErr to return
tx.Rollback()
return commitErr
}
return nil
}
func (b *Bot) getSettings(ctx context.Context, roomID id.RoomID) (*settings, error) {
span := sentry.StartSpan(ctx, "http.server", sentry.TransactionName("getSettings"))
defer span.Finish()
var config settings
query := "SELECT * FROM settings WHERE room_id = "
switch b.lp.GetStore().GetDialect() {
case "postgres":
query += "$1"
case "sqlite3":
query += "?"
}
row := b.lp.GetDB().QueryRow(query, roomID)
err := row.Scan(&config.Mailbox)
if err == nil {
return &config, nil
}
b.log.Error("cannot find settings in database: %v", err)
err = b.lp.GetClient().GetRoomAccountData(roomID, settingskey, &config)
return &config, err
}
func (b *Bot) setSettings(ctx context.Context, roomID id.RoomID, cfg *settings) error {
span := sentry.StartSpan(ctx, "http.server", sentry.TransactionName("setSettings"))
defer span.Finish()
tx, err := b.lp.GetDB().Begin()
if err == nil {
var insert string
switch b.lp.GetStore().GetDialect() {
case "postgres":
insert = "INSERT INTO settings VALUES ($1) ON CONFLICT (room_id) DO UPDATE SET mailbox = $1"
case "sqlite3":
insert = "INSERT INTO settings VALUES (?) ON CONFLICT (room_id) DO UPDATE SET mailbox = ?"
}
_, err = tx.Exec(insert, cfg.Mailbox)
if err != nil {
b.log.Error("cannot insert settigs: %v", err)
// nolint // no need to check error here
tx.Rollback()
}
if err != nil {
err = tx.Commit()
if err != nil {
b.log.Error("cannot commit transaction: %v", err)
}
}
}
return b.lp.GetClient().SetRoomAccountData(roomID, settingskey, cfg)
}
// FindRoomID by mailbox
func (b *Bot) FindRoomID(mailbox string) (id.RoomID, error) {
query := "SELECT room_id FROM settings WHERE mailbox = "
switch b.lp.GetStore().GetDialect() {
case "postgres":
query += "$1"
case "sqlite3":
query += "?"
}
var roomID string
row := b.lp.GetDB().QueryRow(query, mailbox)
err := row.Scan(&roomID)
return id.RoomID(roomID), err
}

66
bot/mailbox.go Normal file
View File

@@ -0,0 +1,66 @@
package bot
import (
"context"
"github.com/getsentry/sentry-go"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
)
func (b *Bot) handleMailbox(ctx context.Context, evt *event.Event, command []string) {
if len(command) == 1 {
b.getMailbox(ctx, evt)
return
}
b.setMailbox(ctx, evt, command[1])
}
func (b *Bot) getMailbox(ctx context.Context, evt *event.Event) {
span := sentry.StartSpan(ctx, "http.server", sentry.TransactionName("getMailbox"))
defer span.Finish()
cfg, err := b.getSettings(span.Context(), evt.RoomID)
if err != nil || cfg == nil {
b.Error(span.Context(), evt.RoomID, "cannot get settings: %v", err)
return
}
if cfg.Mailbox == "" {
b.Error(span.Context(), evt.RoomID, "mailbox name is not set")
return
}
content := format.RenderMarkdown("Mailbox of this room is **"+cfg.Mailbox+"@"+b.domain+"**", true, true)
content.MsgType = event.MsgNotice
_, err = b.lp.Send(evt.RoomID, content)
if err != nil {
b.Error(span.Context(), evt.RoomID, "cannot send message: %v", err)
}
}
func (b *Bot) setMailbox(ctx context.Context, evt *event.Event, mailbox string) {
span := sentry.StartSpan(ctx, "http.server", sentry.TransactionName("setMailbox"))
defer span.Finish()
cfg, err := b.getSettings(span.Context(), evt.RoomID)
if err != nil {
b.log.Warn("cannot get settings: %v", err)
}
if cfg == nil {
cfg = &settings{}
}
cfg.Mailbox = mailbox
err = b.setSettings(span.Context(), evt.RoomID, cfg)
if err != nil {
b.Error(span.Context(), evt.RoomID, "cannot update settings: %v", err)
return
}
content := format.RenderMarkdown("Mailbox of this room set to **"+cfg.Mailbox+"@"+b.domain+"**", true, true)
content.MsgType = event.MsgNotice
_, err = b.lp.Send(evt.RoomID, content)
if err != nil {
b.Error(span.Context(), evt.RoomID, "cannot send message: %v", err)
}
}

23
bot/message.go Normal file
View File

@@ -0,0 +1,23 @@
package bot
import (
"context"
"strings"
"maunium.net/go/mautrix/event"
)
func (b *Bot) handle(ctx context.Context, evt *event.Event) {
content := evt.Content.AsMessage()
if content == nil {
b.Error(ctx, evt.RoomID, "cannot read message")
return
}
message := strings.TrimSpace(content.Body)
command := b.parseCommand(message)
if command == nil {
return
}
b.handleCommand(ctx, evt, command)
}

72
bot/sync.go Normal file
View File

@@ -0,0 +1,72 @@
package bot
import (
"context"
"github.com/getsentry/sentry-go"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
)
func (b *Bot) initSync() {
b.lp.OnEventType(
event.EventMessage,
func(_ mautrix.EventSource, evt *event.Event) {
go b.onMessage(evt)
})
b.lp.OnEventType(
event.EventEncrypted,
func(_ mautrix.EventSource, evt *event.Event) {
go b.onEncryptedMessage(evt)
})
}
func (b *Bot) onMessage(evt *event.Event) {
// ignore own messages
if evt.Sender == b.lp.GetClient().UserID {
return
}
hub := sentry.CurrentHub().Clone()
hub.ConfigureScope(func(scope *sentry.Scope) {
scope.SetUser(sentry.User{ID: evt.Sender.String()})
scope.SetContext("event", map[string]string{
"id": evt.ID.String(),
"room": evt.RoomID.String(),
"sender": evt.Sender.String(),
})
})
ctx := sentry.SetHubOnContext(context.Background(), hub)
span := sentry.StartSpan(ctx, "http.server", sentry.TransactionName("onMessage"))
defer span.Finish()
b.handle(span.Context(), evt)
}
func (b *Bot) onEncryptedMessage(evt *event.Event) {
// ignore own messages
if evt.Sender == b.lp.GetClient().UserID {
return
}
hub := sentry.CurrentHub().Clone()
hub.ConfigureScope(func(scope *sentry.Scope) {
scope.SetUser(sentry.User{ID: evt.Sender.String()})
scope.SetContext("event", map[string]string{
"id": evt.ID.String(),
"room": evt.RoomID.String(),
"sender": evt.Sender.String(),
})
})
ctx := sentry.SetHubOnContext(context.Background(), hub)
span := sentry.StartSpan(ctx, "http.server", sentry.TransactionName("onMessage"))
defer span.Finish()
decrypted, err := b.lp.GetMachine().DecryptMegolmEvent(evt)
if err != nil {
b.Error(span.Context(), evt.RoomID, "cannot decrypt a message: %v", err)
return
}
b.handle(span.Context(), decrypted)
}