Merge branch 'admin-support' into 'main'
Initial work on admin commands support See merge request etke.cc/postmoogle!22
This commit is contained in:
@@ -44,6 +44,7 @@ env vars
|
||||
* **POSTMOOGLE_DB_DIALECT** - database dialect (postgres, sqlite3)
|
||||
* **POSTMOOGLE_MAXSIZE** - max email size (including attachments) in megabytes
|
||||
* **POSTMOOGLE_USERS** - a space-separated list of whitelisted users allowed to use the bridge. If not defined, everyone is allowed. Example rule: `@someone:example.com @another:example.com @bot.*:example.com @*:another.com`
|
||||
* **POSTMOOGLE_ADMINS** - a space-separated list of admin users. See `POSTMOOGLE_USERS` for syntax examples
|
||||
|
||||
You can find default values in [config/defaults.go](config/defaults.go)
|
||||
|
||||
|
||||
42
bot/access.go
Normal file
42
bot/access.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"gitlab.com/etke.cc/postmoogle/utils"
|
||||
)
|
||||
|
||||
func (b *Bot) allowAnyone(actorID id.UserID, targetRoomID id.RoomID) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *Bot) allowOwner(actorID id.UserID, targetRoomID id.RoomID) bool {
|
||||
if len(b.allowedUsers) != 0 {
|
||||
if !utils.Match(actorID.String(), b.allowedUsers) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if b.noowner {
|
||||
return true
|
||||
}
|
||||
|
||||
cfg, err := b.getSettings(targetRoomID)
|
||||
if err != nil {
|
||||
b.Error(context.Background(), targetRoomID, "failed to retrieve settings: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
owner := cfg.Owner()
|
||||
if owner == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return owner == actorID.String()
|
||||
}
|
||||
|
||||
func (b *Bot) allowAdmin(actorID id.UserID, targetRoomID id.RoomID) bool {
|
||||
return utils.Match(actorID.String(), b.allowedAdmins)
|
||||
}
|
||||
57
bot/bot.go
57
bot/bot.go
@@ -6,6 +6,7 @@ import (
|
||||
"regexp"
|
||||
"sync"
|
||||
|
||||
"git.sr.ht/~xn/cache/v2"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"gitlab.com/etke.cc/go/logger"
|
||||
"gitlab.com/etke.cc/linkpearl"
|
||||
@@ -21,7 +22,10 @@ type Bot struct {
|
||||
prefix string
|
||||
domain string
|
||||
allowedUsers []*regexp.Regexp
|
||||
allowedAdmins []*regexp.Regexp
|
||||
commands commandList
|
||||
rooms sync.Map
|
||||
cfg cache.Cache[settings]
|
||||
log *logger.Logger
|
||||
lp *linkpearl.Linkpearl
|
||||
mu map[id.RoomID]*sync.Mutex
|
||||
@@ -29,36 +33,51 @@ type Bot struct {
|
||||
}
|
||||
|
||||
// New creates a new matrix bot
|
||||
func New(lp *linkpearl.Linkpearl, log *logger.Logger, prefix, domain string, noowner, federation bool, allowedUsers []*regexp.Regexp) *Bot {
|
||||
return &Bot{
|
||||
noowner: noowner,
|
||||
federation: federation,
|
||||
prefix: prefix,
|
||||
domain: domain,
|
||||
allowedUsers: allowedUsers,
|
||||
rooms: sync.Map{},
|
||||
log: log,
|
||||
lp: lp,
|
||||
mu: map[id.RoomID]*sync.Mutex{},
|
||||
func New(
|
||||
lp *linkpearl.Linkpearl,
|
||||
log *logger.Logger,
|
||||
prefix, domain string,
|
||||
noowner, federation bool,
|
||||
allowedUsers []*regexp.Regexp,
|
||||
allowedAdmins []*regexp.Regexp,
|
||||
) *Bot {
|
||||
b := &Bot{
|
||||
noowner: noowner,
|
||||
federation: federation,
|
||||
prefix: prefix,
|
||||
domain: domain,
|
||||
allowedUsers: allowedUsers,
|
||||
allowedAdmins: allowedAdmins,
|
||||
rooms: sync.Map{},
|
||||
cfg: cache.NewLRU[settings](1000),
|
||||
log: log,
|
||||
lp: lp,
|
||||
mu: map[id.RoomID]*sync.Mutex{},
|
||||
}
|
||||
|
||||
b.commands = b.buildCommandList()
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// 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...)
|
||||
err := fmt.Errorf(message, args...)
|
||||
|
||||
sentry.GetHubFromContext(ctx).CaptureException(fmt.Errorf(message, args...))
|
||||
sentry.GetHubFromContext(ctx).CaptureException(err)
|
||||
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...),
|
||||
})
|
||||
b.SendError(ctx, roomID, message)
|
||||
}
|
||||
}
|
||||
|
||||
// Notice sends a notice message to the matrix room
|
||||
func (b *Bot) Notice(ctx context.Context, roomID id.RoomID, message string) {
|
||||
// SendError sends an error message to the matrix room
|
||||
func (b *Bot) SendError(ctx context.Context, roomID id.RoomID, message string) {
|
||||
b.SendNotice(ctx, roomID, "ERROR: "+message)
|
||||
}
|
||||
|
||||
// SendNotice sends a notice message to the matrix room
|
||||
func (b *Bot) SendNotice(ctx context.Context, roomID id.RoomID, message string) {
|
||||
content := format.RenderMarkdown(message, true, true)
|
||||
content.MsgType = event.MsgNotice
|
||||
_, err := b.lp.Send(roomID, &content)
|
||||
|
||||
285
bot/command.go
285
bot/command.go
@@ -11,11 +11,18 @@ import (
|
||||
"gitlab.com/etke.cc/postmoogle/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
commandHelp = "help"
|
||||
commandStop = "stop"
|
||||
commandMailboxes = "mailboxes"
|
||||
)
|
||||
|
||||
type (
|
||||
command struct {
|
||||
key string
|
||||
description string
|
||||
sanitizer func(string) string
|
||||
allowed func(id.UserID, id.RoomID) bool
|
||||
}
|
||||
commandList []command
|
||||
)
|
||||
@@ -29,73 +36,91 @@ func (c commandList) get(key string) *command {
|
||||
return nil
|
||||
}
|
||||
|
||||
var commands = commandList{
|
||||
// special commands
|
||||
{
|
||||
key: "help",
|
||||
description: "Show this help message",
|
||||
},
|
||||
{
|
||||
key: "stop",
|
||||
description: "Disable bridge for the room and clear all configuration",
|
||||
},
|
||||
{}, // delimiter
|
||||
// options commands
|
||||
{
|
||||
key: optionMailbox,
|
||||
description: "Get or set mailbox of the room",
|
||||
sanitizer: utils.Mailbox,
|
||||
},
|
||||
{
|
||||
key: optionOwner,
|
||||
description: "Get or set owner of the room",
|
||||
sanitizer: func(s string) string { return s },
|
||||
},
|
||||
{}, // delimiter
|
||||
{
|
||||
key: optionNoSender,
|
||||
description: fmt.Sprintf(
|
||||
"Get or set `%s` of the room (`true` - hide email sender; `false` - show email sender)",
|
||||
optionNoSender,
|
||||
),
|
||||
sanitizer: utils.SanitizeBoolString,
|
||||
},
|
||||
{
|
||||
key: optionNoSubject,
|
||||
description: fmt.Sprintf(
|
||||
"Get or set `%s` of the room (`true` - hide email subject; `false` - show email subject)",
|
||||
optionNoSubject,
|
||||
),
|
||||
sanitizer: utils.SanitizeBoolString,
|
||||
},
|
||||
{
|
||||
key: optionNoHTML,
|
||||
description: fmt.Sprintf(
|
||||
"Get or set `%s` of the room (`true` - ignore HTML in email; `false` - parse HTML in emails)",
|
||||
optionNoHTML,
|
||||
),
|
||||
sanitizer: utils.SanitizeBoolString,
|
||||
},
|
||||
{
|
||||
key: optionNoThreads,
|
||||
description: fmt.Sprintf(
|
||||
"Get or set `%s` of the room (`true` - ignore email threads; `false` - convert email threads into matrix threads)",
|
||||
optionNoThreads,
|
||||
),
|
||||
sanitizer: utils.SanitizeBoolString,
|
||||
},
|
||||
{
|
||||
key: optionNoFiles,
|
||||
description: fmt.Sprintf(
|
||||
"Get or set `%s` of the room (`true` - ignore email attachments; `false` - upload email attachments)",
|
||||
optionNoFiles,
|
||||
),
|
||||
sanitizer: utils.SanitizeBoolString,
|
||||
},
|
||||
func (b *Bot) buildCommandList() commandList {
|
||||
return commandList{
|
||||
// special commands
|
||||
{
|
||||
key: commandHelp,
|
||||
description: "Show this help message",
|
||||
allowed: b.allowAnyone,
|
||||
},
|
||||
{
|
||||
key: commandStop,
|
||||
description: "Disable bridge for the room and clear all configuration",
|
||||
allowed: b.allowOwner,
|
||||
},
|
||||
{allowed: b.allowOwner}, // delimiter
|
||||
// options commands
|
||||
{
|
||||
key: optionMailbox,
|
||||
description: "Get or set mailbox of the room",
|
||||
sanitizer: utils.Mailbox,
|
||||
allowed: b.allowOwner,
|
||||
},
|
||||
{
|
||||
key: optionOwner,
|
||||
description: "Get or set owner of the room",
|
||||
sanitizer: func(s string) string { return s },
|
||||
allowed: b.allowOwner,
|
||||
},
|
||||
{allowed: b.allowOwner}, // delimiter
|
||||
{
|
||||
key: optionNoSender,
|
||||
description: fmt.Sprintf(
|
||||
"Get or set `%s` of the room (`true` - hide email sender; `false` - show email sender)",
|
||||
optionNoSender,
|
||||
),
|
||||
sanitizer: utils.SanitizeBoolString,
|
||||
allowed: b.allowOwner,
|
||||
},
|
||||
{
|
||||
key: optionNoSubject,
|
||||
description: fmt.Sprintf(
|
||||
"Get or set `%s` of the room (`true` - hide email subject; `false` - show email subject)",
|
||||
optionNoSubject,
|
||||
),
|
||||
sanitizer: utils.SanitizeBoolString,
|
||||
allowed: b.allowOwner,
|
||||
},
|
||||
{
|
||||
key: optionNoHTML,
|
||||
description: fmt.Sprintf(
|
||||
"Get or set `%s` of the room (`true` - ignore HTML in email; `false` - parse HTML in emails)",
|
||||
optionNoHTML,
|
||||
),
|
||||
sanitizer: utils.SanitizeBoolString,
|
||||
allowed: b.allowOwner,
|
||||
},
|
||||
{
|
||||
key: optionNoThreads,
|
||||
description: fmt.Sprintf(
|
||||
"Get or set `%s` of the room (`true` - ignore email threads; `false` - convert email threads into matrix threads)",
|
||||
optionNoThreads,
|
||||
),
|
||||
sanitizer: utils.SanitizeBoolString,
|
||||
allowed: b.allowOwner,
|
||||
},
|
||||
{
|
||||
key: optionNoFiles,
|
||||
description: fmt.Sprintf(
|
||||
"Get or set `%s` of the room (`true` - ignore email attachments; `false` - upload email attachments)",
|
||||
optionNoFiles,
|
||||
),
|
||||
sanitizer: utils.SanitizeBoolString,
|
||||
allowed: b.allowOwner,
|
||||
},
|
||||
{allowed: b.allowAdmin}, // delimiter
|
||||
{
|
||||
key: commandMailboxes,
|
||||
description: "Show the list of all mailboxes",
|
||||
allowed: b.allowAdmin,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) handleCommand(ctx context.Context, evt *event.Event, commandSlice []string) {
|
||||
if cmd := commands.get(commandSlice[0]); cmd == nil {
|
||||
cmd := b.commands.get(commandSlice[0])
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -104,11 +129,18 @@ func (b *Bot) handleCommand(ctx context.Context, evt *event.Event, commandSlice
|
||||
return
|
||||
}
|
||||
|
||||
if !cmd.allowed(evt.Sender, evt.RoomID) {
|
||||
b.SendNotice(ctx, evt.RoomID, "not allowed to do that, kupo")
|
||||
return
|
||||
}
|
||||
|
||||
switch commandSlice[0] {
|
||||
case "help":
|
||||
b.sendHelp(ctx, evt.RoomID)
|
||||
case "stop":
|
||||
b.runStop(ctx, true)
|
||||
case commandHelp:
|
||||
b.sendHelp(ctx)
|
||||
case commandStop:
|
||||
b.runStop(ctx)
|
||||
case commandMailboxes:
|
||||
b.sendMailboxes(ctx)
|
||||
default:
|
||||
b.handleOption(ctx, commandSlice)
|
||||
}
|
||||
@@ -144,10 +176,10 @@ func (b *Bot) sendIntroduction(ctx context.Context, roomID id.RoomID) {
|
||||
msg.WriteString(b.domain)
|
||||
msg.WriteString("` and have them appear in this room.")
|
||||
|
||||
b.Notice(ctx, roomID, msg.String())
|
||||
b.SendNotice(ctx, roomID, msg.String())
|
||||
}
|
||||
|
||||
func (b *Bot) sendHelp(ctx context.Context, roomID id.RoomID) {
|
||||
func (b *Bot) sendHelp(ctx context.Context) {
|
||||
evt := eventFromContext(ctx)
|
||||
|
||||
cfg, serr := b.getSettings(evt.RoomID)
|
||||
@@ -157,7 +189,10 @@ func (b *Bot) sendHelp(ctx context.Context, roomID id.RoomID) {
|
||||
|
||||
var msg strings.Builder
|
||||
msg.WriteString("The following commands are supported:\n\n")
|
||||
for _, cmd := range commands {
|
||||
for _, cmd := range b.commands {
|
||||
if !cmd.allowed(evt.Sender, evt.RoomID) {
|
||||
continue
|
||||
}
|
||||
if cmd.key == "" {
|
||||
msg.WriteString("\n---\n")
|
||||
continue
|
||||
@@ -189,111 +224,5 @@ func (b *Bot) sendHelp(ctx context.Context, roomID id.RoomID) {
|
||||
msg.WriteString("\n")
|
||||
}
|
||||
|
||||
b.Notice(ctx, roomID, msg.String())
|
||||
}
|
||||
|
||||
func (b *Bot) runStop(ctx context.Context, checkAllowed bool) {
|
||||
evt := eventFromContext(ctx)
|
||||
cfg, err := b.getSettings(evt.RoomID)
|
||||
if err != nil {
|
||||
b.Error(ctx, evt.RoomID, "failed to retrieve settings: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if checkAllowed && !b.Allowed(evt.Sender, cfg) {
|
||||
b.Notice(ctx, evt.RoomID, "you don't have permission to do that")
|
||||
return
|
||||
}
|
||||
|
||||
mailbox := cfg.Get(optionMailbox)
|
||||
if mailbox == "" {
|
||||
b.Notice(ctx, evt.RoomID, "that room is not configured yet")
|
||||
return
|
||||
}
|
||||
|
||||
b.rooms.Delete(mailbox)
|
||||
|
||||
err = b.setSettings(evt.RoomID, settings{})
|
||||
if err != nil {
|
||||
b.Error(ctx, evt.RoomID, "cannot update settings: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
b.Notice(ctx, evt.RoomID, "mailbox has been disabled")
|
||||
}
|
||||
|
||||
func (b *Bot) handleOption(ctx context.Context, cmd []string) {
|
||||
if len(cmd) == 1 {
|
||||
b.getOption(ctx, cmd[0])
|
||||
return
|
||||
}
|
||||
b.setOption(ctx, cmd[0], cmd[1])
|
||||
}
|
||||
|
||||
func (b *Bot) getOption(ctx context.Context, name string) {
|
||||
evt := eventFromContext(ctx)
|
||||
cfg, err := b.getSettings(evt.RoomID)
|
||||
if err != nil {
|
||||
b.Error(ctx, evt.RoomID, "failed to retrieve settings: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
value := cfg.Get(name)
|
||||
if value == "" {
|
||||
b.Notice(ctx, evt.RoomID, fmt.Sprintf("`%s` is not set, kupo.", name))
|
||||
return
|
||||
}
|
||||
|
||||
if name == optionMailbox {
|
||||
value = value + "@" + b.domain
|
||||
}
|
||||
|
||||
b.Notice(ctx, evt.RoomID, fmt.Sprintf("`%s` of this room is `%s`", name, value))
|
||||
}
|
||||
|
||||
func (b *Bot) setOption(ctx context.Context, name, value string) {
|
||||
cmd := commands.get(name)
|
||||
if cmd != nil {
|
||||
value = cmd.sanitizer(value)
|
||||
}
|
||||
|
||||
evt := eventFromContext(ctx)
|
||||
if name == optionMailbox {
|
||||
existingID, ok := b.GetMapping(value)
|
||||
if ok && existingID != "" && existingID != evt.RoomID {
|
||||
b.Notice(ctx, evt.RoomID, fmt.Sprintf("Mailbox `%s@%s` already taken, kupo", value, b.domain))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cfg, err := b.getSettings(evt.RoomID)
|
||||
if err != nil {
|
||||
b.Error(ctx, evt.RoomID, "failed to retrieve settings: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !b.Allowed(evt.Sender, cfg) {
|
||||
b.Notice(ctx, evt.RoomID, "you don't have permission to do that, kupo")
|
||||
return
|
||||
}
|
||||
|
||||
old := cfg.Get(name)
|
||||
cfg.Set(name, value)
|
||||
|
||||
if name == optionMailbox {
|
||||
cfg.Set(optionOwner, evt.Sender.String())
|
||||
if old != "" {
|
||||
b.rooms.Delete(old)
|
||||
}
|
||||
b.rooms.Store(value, evt.RoomID)
|
||||
value = fmt.Sprintf("%s@%s", value, b.domain)
|
||||
}
|
||||
|
||||
err = b.setSettings(evt.RoomID, cfg)
|
||||
if err != nil {
|
||||
b.Error(ctx, evt.RoomID, "cannot update settings: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
b.Notice(ctx, evt.RoomID, fmt.Sprintf("`%s` of this room set to `%s`", name, value))
|
||||
b.SendNotice(ctx, evt.RoomID, msg.String())
|
||||
}
|
||||
|
||||
53
bot/command_admin.go
Normal file
53
bot/command_admin.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
func (b *Bot) sendMailboxes(ctx context.Context) {
|
||||
evt := eventFromContext(ctx)
|
||||
mailboxes := map[string]id.RoomID{}
|
||||
b.rooms.Range(func(key any, value any) bool {
|
||||
if key == nil {
|
||||
return true
|
||||
}
|
||||
if value == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
mailbox, ok := key.(string)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
roomID, ok := value.(id.RoomID)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
mailboxes[mailbox] = roomID
|
||||
return true
|
||||
})
|
||||
|
||||
if len(mailboxes) == 0 {
|
||||
b.SendNotice(ctx, evt.RoomID, "No mailboxes are managed by the bot so far, kupo!")
|
||||
return
|
||||
}
|
||||
|
||||
var msg strings.Builder
|
||||
msg.WriteString("The following mailboxes are managed by the bot:\n")
|
||||
for mailbox, roomID := range mailboxes {
|
||||
msg.WriteString("* `")
|
||||
msg.WriteString(mailbox)
|
||||
msg.WriteString("@")
|
||||
msg.WriteString(b.domain)
|
||||
msg.WriteString("` - `")
|
||||
msg.WriteString(roomID.String())
|
||||
msg.WriteString("`")
|
||||
msg.WriteString("\n")
|
||||
}
|
||||
|
||||
b.SendNotice(ctx, evt.RoomID, msg.String())
|
||||
}
|
||||
106
bot/command_owner.go
Normal file
106
bot/command_owner.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func (b *Bot) runStop(ctx context.Context) {
|
||||
evt := eventFromContext(ctx)
|
||||
cfg, err := b.getSettings(evt.RoomID)
|
||||
if err != nil {
|
||||
b.Error(ctx, evt.RoomID, "failed to retrieve settings: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
mailbox := cfg.Get(optionMailbox)
|
||||
if mailbox == "" {
|
||||
b.SendNotice(ctx, evt.RoomID, "that room is not configured yet")
|
||||
return
|
||||
}
|
||||
|
||||
b.rooms.Delete(mailbox)
|
||||
|
||||
err = b.setSettings(evt.RoomID, settings{})
|
||||
if err != nil {
|
||||
b.Error(ctx, evt.RoomID, "cannot update settings: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
b.SendNotice(ctx, evt.RoomID, "mailbox has been disabled")
|
||||
}
|
||||
|
||||
func (b *Bot) handleOption(ctx context.Context, cmd []string) {
|
||||
if len(cmd) == 1 {
|
||||
b.getOption(ctx, cmd[0])
|
||||
return
|
||||
}
|
||||
b.setOption(ctx, cmd[0], cmd[1])
|
||||
}
|
||||
|
||||
func (b *Bot) getOption(ctx context.Context, name string) {
|
||||
evt := eventFromContext(ctx)
|
||||
cfg, err := b.getSettings(evt.RoomID)
|
||||
if err != nil {
|
||||
b.Error(ctx, evt.RoomID, "failed to retrieve settings: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
value := cfg.Get(name)
|
||||
if value == "" {
|
||||
b.SendNotice(ctx, evt.RoomID, fmt.Sprintf("`%s` is not set, kupo.", name))
|
||||
return
|
||||
}
|
||||
|
||||
if name == optionMailbox {
|
||||
value = value + "@" + b.domain
|
||||
}
|
||||
|
||||
b.SendNotice(ctx, evt.RoomID, fmt.Sprintf("`%s` of this room is `%s`", name, value))
|
||||
}
|
||||
|
||||
func (b *Bot) setOption(ctx context.Context, name, value string) {
|
||||
cmd := b.commands.get(name)
|
||||
if cmd != nil {
|
||||
value = cmd.sanitizer(value)
|
||||
}
|
||||
|
||||
evt := eventFromContext(ctx)
|
||||
if name == optionMailbox {
|
||||
existingID, ok := b.GetMapping(value)
|
||||
if ok && existingID != "" && existingID != evt.RoomID {
|
||||
b.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Mailbox `%s@%s` already taken, kupo", value, b.domain))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cfg, err := b.getSettings(evt.RoomID)
|
||||
if err != nil {
|
||||
b.Error(ctx, evt.RoomID, "failed to retrieve settings: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
old := cfg.Get(name)
|
||||
cfg.Set(name, value)
|
||||
|
||||
if name == optionMailbox {
|
||||
cfg.Set(optionOwner, evt.Sender.String())
|
||||
if old != "" {
|
||||
b.rooms.Delete(old)
|
||||
}
|
||||
b.rooms.Store(value, evt.RoomID)
|
||||
value = fmt.Sprintf("%s@%s", value, b.domain)
|
||||
}
|
||||
|
||||
err = b.setSettings(evt.RoomID, cfg)
|
||||
if err != nil {
|
||||
b.Error(ctx, evt.RoomID, "cannot update settings: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if name == optionMailbox {
|
||||
value = value + "@" + b.domain
|
||||
}
|
||||
|
||||
b.SendNotice(ctx, evt.RoomID, fmt.Sprintf("`%s` of this room set to `%s`", name, value))
|
||||
}
|
||||
@@ -81,6 +81,11 @@ func (b *Bot) migrateSettings(roomID id.RoomID) {
|
||||
}
|
||||
|
||||
func (b *Bot) getSettings(roomID id.RoomID) (settings, error) {
|
||||
cfg := b.cfg.Get(roomID.String())
|
||||
if cfg != nil {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
config := settings{}
|
||||
err := b.lp.GetClient().GetRoomAccountData(roomID, settingskey, &config)
|
||||
if err != nil {
|
||||
@@ -90,29 +95,14 @@ func (b *Bot) getSettings(roomID id.RoomID) (settings, error) {
|
||||
// In such cases, just return a default (empty) settings object.
|
||||
err = nil
|
||||
}
|
||||
} else {
|
||||
b.cfg.Set(roomID.String(), config)
|
||||
}
|
||||
|
||||
return config, utils.UnwrapError(err)
|
||||
}
|
||||
|
||||
func (b *Bot) setSettings(roomID id.RoomID, cfg settings) error {
|
||||
b.cfg.Set(roomID.String(), cfg)
|
||||
return utils.UnwrapError(b.lp.GetClient().SetRoomAccountData(roomID, settingskey, cfg))
|
||||
}
|
||||
|
||||
// Allowed checks if change is allowed
|
||||
func (b *Bot) Allowed(userID id.UserID, cfg settings) bool {
|
||||
if !utils.Match(userID.String(), b.allowedUsers) {
|
||||
return false
|
||||
}
|
||||
|
||||
if b.noowner {
|
||||
return true
|
||||
}
|
||||
|
||||
owner := cfg.Owner()
|
||||
if owner == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return owner == userID.String()
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func (b *Bot) onBotJoin(ctx context.Context) {
|
||||
}
|
||||
|
||||
b.sendIntroduction(ctx, evt.RoomID)
|
||||
b.sendHelp(ctx, evt.RoomID)
|
||||
b.sendHelp(ctx)
|
||||
}
|
||||
|
||||
func (b *Bot) onLeave(ctx context.Context) {
|
||||
@@ -94,7 +94,7 @@ func (b *Bot) onLeave(ctx context.Context) {
|
||||
count := len(members)
|
||||
if count == 1 && members[0] == b.lp.GetClient().UserID {
|
||||
b.log.Info("no more users left in the %s room", evt.RoomID)
|
||||
b.runStop(ctx, false)
|
||||
b.runStop(ctx)
|
||||
_, err := b.lp.GetClient().LeaveRoom(evt.RoomID)
|
||||
if err != nil {
|
||||
b.Error(ctx, evt.RoomID, "cannot leave empty room: %v", err)
|
||||
|
||||
@@ -87,7 +87,7 @@ func initBot(cfg *config.Config) {
|
||||
// nolint // Fatal = panic, not os.Exit()
|
||||
log.Fatal("cannot initialize matrix bot: %v", err)
|
||||
}
|
||||
mxb = bot.New(lp, mxlog, cfg.Prefix, cfg.Domain, cfg.NoOwner, cfg.Federation, cfg.Users)
|
||||
mxb = bot.New(lp, mxlog, cfg.Prefix, cfg.Domain, cfg.NoOwner, cfg.Federation, cfg.Users, cfg.Admins)
|
||||
log.Debug("bot has been created")
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"gitlab.com/etke.cc/go/env"
|
||||
|
||||
@@ -14,14 +15,14 @@ const prefix = "postmoogle"
|
||||
func New() (*Config, error) {
|
||||
env.SetPrefix(prefix)
|
||||
|
||||
mxidPatterns := env.Slice("users")
|
||||
regexPatterns, err := utils.WildcardMXIDsToRegexes(mxidPatterns)
|
||||
userPatterns, err := getUserRegexPatterns("users")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"failed to convert wildcard user patterns (`%s`) to regular expression: %s",
|
||||
mxidPatterns,
|
||||
err,
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
adminPatterns, err := getUserRegexPatterns("admins")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := &Config{
|
||||
@@ -36,7 +37,8 @@ func New() (*Config, error) {
|
||||
Federation: env.Bool("federation"),
|
||||
MaxSize: env.Int("maxsize", defaultConfig.MaxSize),
|
||||
StatusMsg: env.String("statusmsg", defaultConfig.StatusMsg),
|
||||
Users: *regexPatterns,
|
||||
Users: userPatterns,
|
||||
Admins: adminPatterns,
|
||||
Sentry: Sentry{
|
||||
DSN: env.String("sentry.dsn", defaultConfig.Sentry.DSN),
|
||||
},
|
||||
@@ -49,3 +51,17 @@ func New() (*Config, error) {
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func getUserRegexPatterns(key string) ([]*regexp.Regexp, error) {
|
||||
mxidPatterns := env.Slice(key)
|
||||
regexPatterns, err := utils.WildcardMXIDsToRegexes(mxidPatterns)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"failed to convert wildcard %s patterns (`%s`) to regular expression: %s",
|
||||
key,
|
||||
mxidPatterns,
|
||||
err,
|
||||
)
|
||||
}
|
||||
return regexPatterns, nil
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ type Config struct {
|
||||
StatusMsg string
|
||||
// Users holds list of allowed users (wildcards supported), e.g.: @*:example.com, @bot.*:example.com, @admin:*. Empty = *
|
||||
Users []*regexp.Regexp
|
||||
// Admins holds list of admin users (wildcards supported), e.g.: @*:example.com, @bot.*:example.com, @admin:*. Empty = *
|
||||
Admins []*regexp.Regexp
|
||||
|
||||
// DB config
|
||||
DB DB
|
||||
|
||||
1
go.mod
1
go.mod
@@ -3,6 +3,7 @@ module gitlab.com/etke.cc/postmoogle
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
git.sr.ht/~xn/cache/v2 v2.0.0
|
||||
github.com/emersion/go-smtp v0.15.0
|
||||
github.com/getsentry/sentry-go v0.13.0
|
||||
github.com/jhillyerd/enmime v0.10.0
|
||||
|
||||
2
go.sum
2
go.sum
@@ -1,3 +1,5 @@
|
||||
git.sr.ht/~xn/cache/v2 v2.0.0 h1:aYzwGDyVIzjCl2yqcxZjprnu++Q3BmUQeK2agqvcQt8=
|
||||
git.sr.ht/~xn/cache/v2 v2.0.0/go.mod h1:HIPSMiDudQ483tRDup586e0YZdwMySIZFWXMPwYMuV8=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
// WildcardMXIDsToRegexes converts a list of wildcard patterns to a list of regular expressions
|
||||
func WildcardMXIDsToRegexes(wildCardPatterns []string) (*[]*regexp.Regexp, error) {
|
||||
func WildcardMXIDsToRegexes(wildCardPatterns []string) ([]*regexp.Regexp, error) {
|
||||
regexPatterns := make([]*regexp.Regexp, len(wildCardPatterns))
|
||||
|
||||
for idx, wildCardPattern := range wildCardPatterns {
|
||||
@@ -18,16 +18,11 @@ func WildcardMXIDsToRegexes(wildCardPatterns []string) (*[]*regexp.Regexp, error
|
||||
regexPatterns[idx] = regex
|
||||
}
|
||||
|
||||
return ®exPatterns, nil
|
||||
return regexPatterns, nil
|
||||
}
|
||||
|
||||
// Match tells if the given user id is allowed to use the bot, according to the given whitelist
|
||||
func Match(userID string, allowed []*regexp.Regexp) bool {
|
||||
// No whitelisted users means everyone is whitelisted
|
||||
if len(allowed) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, regex := range allowed {
|
||||
if regex.MatchString(userID) {
|
||||
return true
|
||||
|
||||
@@ -127,10 +127,10 @@ func TestMatch(t *testing.T) {
|
||||
|
||||
tests := []testDataDefinition{
|
||||
{
|
||||
name: "Empty allowed users allows anyone",
|
||||
name: "Empty allowed users allows no one",
|
||||
checkedValue: "@someone:example.com",
|
||||
allowedUsers: []string{},
|
||||
expectedResult: true,
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
name: "Direct full mxid match is allowed",
|
||||
@@ -202,7 +202,7 @@ func TestMatch(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
actualResult := Match(testData.checkedValue, *allowedUserRegexes)
|
||||
actualResult := Match(testData.checkedValue, allowedUserRegexes)
|
||||
|
||||
if actualResult == testData.expectedResult {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user