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_DB_DIALECT** - database dialect (postgres, sqlite3)
|
||||||
* **POSTMOOGLE_MAXSIZE** - max email size (including attachments) in megabytes
|
* **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_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)
|
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"
|
"regexp"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"git.sr.ht/~xn/cache/v2"
|
||||||
"github.com/getsentry/sentry-go"
|
"github.com/getsentry/sentry-go"
|
||||||
"gitlab.com/etke.cc/go/logger"
|
"gitlab.com/etke.cc/go/logger"
|
||||||
"gitlab.com/etke.cc/linkpearl"
|
"gitlab.com/etke.cc/linkpearl"
|
||||||
@@ -21,7 +22,10 @@ type Bot struct {
|
|||||||
prefix string
|
prefix string
|
||||||
domain string
|
domain string
|
||||||
allowedUsers []*regexp.Regexp
|
allowedUsers []*regexp.Regexp
|
||||||
|
allowedAdmins []*regexp.Regexp
|
||||||
|
commands commandList
|
||||||
rooms sync.Map
|
rooms sync.Map
|
||||||
|
cfg cache.Cache[settings]
|
||||||
log *logger.Logger
|
log *logger.Logger
|
||||||
lp *linkpearl.Linkpearl
|
lp *linkpearl.Linkpearl
|
||||||
mu map[id.RoomID]*sync.Mutex
|
mu map[id.RoomID]*sync.Mutex
|
||||||
@@ -29,36 +33,51 @@ type Bot struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new matrix bot
|
// New creates a new matrix bot
|
||||||
func New(lp *linkpearl.Linkpearl, log *logger.Logger, prefix, domain string, noowner, federation bool, allowedUsers []*regexp.Regexp) *Bot {
|
func New(
|
||||||
return &Bot{
|
lp *linkpearl.Linkpearl,
|
||||||
noowner: noowner,
|
log *logger.Logger,
|
||||||
federation: federation,
|
prefix, domain string,
|
||||||
prefix: prefix,
|
noowner, federation bool,
|
||||||
domain: domain,
|
allowedUsers []*regexp.Regexp,
|
||||||
allowedUsers: allowedUsers,
|
allowedAdmins []*regexp.Regexp,
|
||||||
rooms: sync.Map{},
|
) *Bot {
|
||||||
log: log,
|
b := &Bot{
|
||||||
lp: lp,
|
noowner: noowner,
|
||||||
mu: map[id.RoomID]*sync.Mutex{},
|
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
|
// Error message to the log and matrix room
|
||||||
func (b *Bot) Error(ctx context.Context, roomID id.RoomID, message string, args ...interface{}) {
|
func (b *Bot) Error(ctx context.Context, roomID id.RoomID, message string, args ...interface{}) {
|
||||||
b.log.Error(message, args...)
|
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 != "" {
|
if roomID != "" {
|
||||||
// nolint // if something goes wrong here nobody can help...
|
b.SendError(ctx, roomID, message)
|
||||||
b.lp.Send(roomID, &event.MessageEventContent{
|
|
||||||
MsgType: event.MsgNotice,
|
|
||||||
Body: "ERROR: " + fmt.Sprintf(message, args...),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notice sends a notice message to the matrix room
|
// SendError sends an error message to the matrix room
|
||||||
func (b *Bot) Notice(ctx context.Context, roomID id.RoomID, message string) {
|
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 := format.RenderMarkdown(message, true, true)
|
||||||
content.MsgType = event.MsgNotice
|
content.MsgType = event.MsgNotice
|
||||||
_, err := b.lp.Send(roomID, &content)
|
_, 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"
|
"gitlab.com/etke.cc/postmoogle/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
commandHelp = "help"
|
||||||
|
commandStop = "stop"
|
||||||
|
commandMailboxes = "mailboxes"
|
||||||
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
command struct {
|
command struct {
|
||||||
key string
|
key string
|
||||||
description string
|
description string
|
||||||
sanitizer func(string) string
|
sanitizer func(string) string
|
||||||
|
allowed func(id.UserID, id.RoomID) bool
|
||||||
}
|
}
|
||||||
commandList []command
|
commandList []command
|
||||||
)
|
)
|
||||||
@@ -29,73 +36,91 @@ func (c commandList) get(key string) *command {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var commands = commandList{
|
func (b *Bot) buildCommandList() commandList {
|
||||||
// special commands
|
return commandList{
|
||||||
{
|
// special commands
|
||||||
key: "help",
|
{
|
||||||
description: "Show this help message",
|
key: commandHelp,
|
||||||
},
|
description: "Show this help message",
|
||||||
{
|
allowed: b.allowAnyone,
|
||||||
key: "stop",
|
},
|
||||||
description: "Disable bridge for the room and clear all configuration",
|
{
|
||||||
},
|
key: commandStop,
|
||||||
{}, // delimiter
|
description: "Disable bridge for the room and clear all configuration",
|
||||||
// options commands
|
allowed: b.allowOwner,
|
||||||
{
|
},
|
||||||
key: optionMailbox,
|
{allowed: b.allowOwner}, // delimiter
|
||||||
description: "Get or set mailbox of the room",
|
// options commands
|
||||||
sanitizer: utils.Mailbox,
|
{
|
||||||
},
|
key: optionMailbox,
|
||||||
{
|
description: "Get or set mailbox of the room",
|
||||||
key: optionOwner,
|
sanitizer: utils.Mailbox,
|
||||||
description: "Get or set owner of the room",
|
allowed: b.allowOwner,
|
||||||
sanitizer: func(s string) string { return s },
|
},
|
||||||
},
|
{
|
||||||
{}, // delimiter
|
key: optionOwner,
|
||||||
{
|
description: "Get or set owner of the room",
|
||||||
key: optionNoSender,
|
sanitizer: func(s string) string { return s },
|
||||||
description: fmt.Sprintf(
|
allowed: b.allowOwner,
|
||||||
"Get or set `%s` of the room (`true` - hide email sender; `false` - show email sender)",
|
},
|
||||||
optionNoSender,
|
{allowed: b.allowOwner}, // delimiter
|
||||||
),
|
{
|
||||||
sanitizer: utils.SanitizeBoolString,
|
key: optionNoSender,
|
||||||
},
|
description: fmt.Sprintf(
|
||||||
{
|
"Get or set `%s` of the room (`true` - hide email sender; `false` - show email sender)",
|
||||||
key: optionNoSubject,
|
optionNoSender,
|
||||||
description: fmt.Sprintf(
|
),
|
||||||
"Get or set `%s` of the room (`true` - hide email subject; `false` - show email subject)",
|
sanitizer: utils.SanitizeBoolString,
|
||||||
optionNoSubject,
|
allowed: b.allowOwner,
|
||||||
),
|
},
|
||||||
sanitizer: utils.SanitizeBoolString,
|
{
|
||||||
},
|
key: optionNoSubject,
|
||||||
{
|
description: fmt.Sprintf(
|
||||||
key: optionNoHTML,
|
"Get or set `%s` of the room (`true` - hide email subject; `false` - show email subject)",
|
||||||
description: fmt.Sprintf(
|
optionNoSubject,
|
||||||
"Get or set `%s` of the room (`true` - ignore HTML in email; `false` - parse HTML in emails)",
|
),
|
||||||
optionNoHTML,
|
sanitizer: utils.SanitizeBoolString,
|
||||||
),
|
allowed: b.allowOwner,
|
||||||
sanitizer: utils.SanitizeBoolString,
|
},
|
||||||
},
|
{
|
||||||
{
|
key: optionNoHTML,
|
||||||
key: optionNoThreads,
|
description: fmt.Sprintf(
|
||||||
description: fmt.Sprintf(
|
"Get or set `%s` of the room (`true` - ignore HTML in email; `false` - parse HTML in emails)",
|
||||||
"Get or set `%s` of the room (`true` - ignore email threads; `false` - convert email threads into matrix threads)",
|
optionNoHTML,
|
||||||
optionNoThreads,
|
),
|
||||||
),
|
sanitizer: utils.SanitizeBoolString,
|
||||||
sanitizer: utils.SanitizeBoolString,
|
allowed: b.allowOwner,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: optionNoFiles,
|
key: optionNoThreads,
|
||||||
description: fmt.Sprintf(
|
description: fmt.Sprintf(
|
||||||
"Get or set `%s` of the room (`true` - ignore email attachments; `false` - upload email attachments)",
|
"Get or set `%s` of the room (`true` - ignore email threads; `false` - convert email threads into matrix threads)",
|
||||||
optionNoFiles,
|
optionNoThreads,
|
||||||
),
|
),
|
||||||
sanitizer: utils.SanitizeBoolString,
|
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) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,11 +129,18 @@ func (b *Bot) handleCommand(ctx context.Context, evt *event.Event, commandSlice
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !cmd.allowed(evt.Sender, evt.RoomID) {
|
||||||
|
b.SendNotice(ctx, evt.RoomID, "not allowed to do that, kupo")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
switch commandSlice[0] {
|
switch commandSlice[0] {
|
||||||
case "help":
|
case commandHelp:
|
||||||
b.sendHelp(ctx, evt.RoomID)
|
b.sendHelp(ctx)
|
||||||
case "stop":
|
case commandStop:
|
||||||
b.runStop(ctx, true)
|
b.runStop(ctx)
|
||||||
|
case commandMailboxes:
|
||||||
|
b.sendMailboxes(ctx)
|
||||||
default:
|
default:
|
||||||
b.handleOption(ctx, commandSlice)
|
b.handleOption(ctx, commandSlice)
|
||||||
}
|
}
|
||||||
@@ -144,10 +176,10 @@ func (b *Bot) sendIntroduction(ctx context.Context, roomID id.RoomID) {
|
|||||||
msg.WriteString(b.domain)
|
msg.WriteString(b.domain)
|
||||||
msg.WriteString("` and have them appear in this room.")
|
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)
|
evt := eventFromContext(ctx)
|
||||||
|
|
||||||
cfg, serr := b.getSettings(evt.RoomID)
|
cfg, serr := b.getSettings(evt.RoomID)
|
||||||
@@ -157,7 +189,10 @@ func (b *Bot) sendHelp(ctx context.Context, roomID id.RoomID) {
|
|||||||
|
|
||||||
var msg strings.Builder
|
var msg strings.Builder
|
||||||
msg.WriteString("The following commands are supported:\n\n")
|
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 == "" {
|
if cmd.key == "" {
|
||||||
msg.WriteString("\n---\n")
|
msg.WriteString("\n---\n")
|
||||||
continue
|
continue
|
||||||
@@ -189,111 +224,5 @@ func (b *Bot) sendHelp(ctx context.Context, roomID id.RoomID) {
|
|||||||
msg.WriteString("\n")
|
msg.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
b.Notice(ctx, roomID, msg.String())
|
b.SendNotice(ctx, evt.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))
|
|
||||||
}
|
}
|
||||||
|
|||||||
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) {
|
func (b *Bot) getSettings(roomID id.RoomID) (settings, error) {
|
||||||
|
cfg := b.cfg.Get(roomID.String())
|
||||||
|
if cfg != nil {
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
config := settings{}
|
config := settings{}
|
||||||
err := b.lp.GetClient().GetRoomAccountData(roomID, settingskey, &config)
|
err := b.lp.GetClient().GetRoomAccountData(roomID, settingskey, &config)
|
||||||
if err != nil {
|
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.
|
// In such cases, just return a default (empty) settings object.
|
||||||
err = nil
|
err = nil
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
b.cfg.Set(roomID.String(), config)
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, utils.UnwrapError(err)
|
return config, utils.UnwrapError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) setSettings(roomID id.RoomID, cfg settings) error {
|
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))
|
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.sendIntroduction(ctx, evt.RoomID)
|
||||||
b.sendHelp(ctx, evt.RoomID)
|
b.sendHelp(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) onLeave(ctx context.Context) {
|
func (b *Bot) onLeave(ctx context.Context) {
|
||||||
@@ -94,7 +94,7 @@ func (b *Bot) onLeave(ctx context.Context) {
|
|||||||
count := len(members)
|
count := len(members)
|
||||||
if count == 1 && members[0] == b.lp.GetClient().UserID {
|
if count == 1 && members[0] == b.lp.GetClient().UserID {
|
||||||
b.log.Info("no more users left in the %s room", evt.RoomID)
|
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)
|
_, err := b.lp.GetClient().LeaveRoom(evt.RoomID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(ctx, evt.RoomID, "cannot leave empty room: %v", err)
|
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()
|
// nolint // Fatal = panic, not os.Exit()
|
||||||
log.Fatal("cannot initialize matrix bot: %v", err)
|
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")
|
log.Debug("bot has been created")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
"gitlab.com/etke.cc/go/env"
|
"gitlab.com/etke.cc/go/env"
|
||||||
|
|
||||||
@@ -14,14 +15,14 @@ const prefix = "postmoogle"
|
|||||||
func New() (*Config, error) {
|
func New() (*Config, error) {
|
||||||
env.SetPrefix(prefix)
|
env.SetPrefix(prefix)
|
||||||
|
|
||||||
mxidPatterns := env.Slice("users")
|
userPatterns, err := getUserRegexPatterns("users")
|
||||||
regexPatterns, err := utils.WildcardMXIDsToRegexes(mxidPatterns)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil, err
|
||||||
"failed to convert wildcard user patterns (`%s`) to regular expression: %s",
|
}
|
||||||
mxidPatterns,
|
|
||||||
err,
|
adminPatterns, err := getUserRegexPatterns("admins")
|
||||||
)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
@@ -36,7 +37,8 @@ func New() (*Config, error) {
|
|||||||
Federation: env.Bool("federation"),
|
Federation: env.Bool("federation"),
|
||||||
MaxSize: env.Int("maxsize", defaultConfig.MaxSize),
|
MaxSize: env.Int("maxsize", defaultConfig.MaxSize),
|
||||||
StatusMsg: env.String("statusmsg", defaultConfig.StatusMsg),
|
StatusMsg: env.String("statusmsg", defaultConfig.StatusMsg),
|
||||||
Users: *regexPatterns,
|
Users: userPatterns,
|
||||||
|
Admins: adminPatterns,
|
||||||
Sentry: Sentry{
|
Sentry: Sentry{
|
||||||
DSN: env.String("sentry.dsn", defaultConfig.Sentry.DSN),
|
DSN: env.String("sentry.dsn", defaultConfig.Sentry.DSN),
|
||||||
},
|
},
|
||||||
@@ -49,3 +51,17 @@ func New() (*Config, error) {
|
|||||||
|
|
||||||
return cfg, nil
|
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
|
StatusMsg string
|
||||||
// Users holds list of allowed users (wildcards supported), e.g.: @*:example.com, @bot.*:example.com, @admin:*. Empty = *
|
// Users holds list of allowed users (wildcards supported), e.g.: @*:example.com, @bot.*:example.com, @admin:*. Empty = *
|
||||||
Users []*regexp.Regexp
|
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 config
|
||||||
DB DB
|
DB DB
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -3,6 +3,7 @@ module gitlab.com/etke.cc/postmoogle
|
|||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
git.sr.ht/~xn/cache/v2 v2.0.0
|
||||||
github.com/emersion/go-smtp v0.15.0
|
github.com/emersion/go-smtp v0.15.0
|
||||||
github.com/getsentry/sentry-go v0.13.0
|
github.com/getsentry/sentry-go v0.13.0
|
||||||
github.com/jhillyerd/enmime v0.10.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/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 h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
|
||||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
|
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
|
// 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))
|
regexPatterns := make([]*regexp.Regexp, len(wildCardPatterns))
|
||||||
|
|
||||||
for idx, wildCardPattern := range wildCardPatterns {
|
for idx, wildCardPattern := range wildCardPatterns {
|
||||||
@@ -18,16 +18,11 @@ func WildcardMXIDsToRegexes(wildCardPatterns []string) (*[]*regexp.Regexp, error
|
|||||||
regexPatterns[idx] = regex
|
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
|
// 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 {
|
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 {
|
for _, regex := range allowed {
|
||||||
if regex.MatchString(userID) {
|
if regex.MatchString(userID) {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -127,10 +127,10 @@ func TestMatch(t *testing.T) {
|
|||||||
|
|
||||||
tests := []testDataDefinition{
|
tests := []testDataDefinition{
|
||||||
{
|
{
|
||||||
name: "Empty allowed users allows anyone",
|
name: "Empty allowed users allows no one",
|
||||||
checkedValue: "@someone:example.com",
|
checkedValue: "@someone:example.com",
|
||||||
allowedUsers: []string{},
|
allowedUsers: []string{},
|
||||||
expectedResult: true,
|
expectedResult: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Direct full mxid match is allowed",
|
name: "Direct full mxid match is allowed",
|
||||||
@@ -202,7 +202,7 @@ func TestMatch(t *testing.T) {
|
|||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
actualResult := Match(testData.checkedValue, *allowedUserRegexes)
|
actualResult := Match(testData.checkedValue, allowedUserRegexes)
|
||||||
|
|
||||||
if actualResult == testData.expectedResult {
|
if actualResult == testData.expectedResult {
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user