Files
postmoogle/bot/command.go
Slavi Pantaleev a057654962 Put command access checks on the command level
Checking using `settings.Allowed` is odd. Not all commands are related
to setting configuration settings. Admin commands are coming in the
future, for which this is certainly not the case.

We now do access checks early on (during command processing), so command
handlers can be clean of access checks. If we're inside of a command
handler, the user is privileged to run it.
2022-08-29 10:27:53 +03:00

323 lines
7.4 KiB
Go

package bot
import (
"context"
"fmt"
"strings"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"gitlab.com/etke.cc/postmoogle/utils"
)
const (
commandHelp = "help"
commandStop = "stop"
commandMailboxes = "mailboxes"
)
type (
command struct {
key string
description string
sanitizer func(string) string
accessChecker accessCheckerFunc
}
commandList []command
)
func (c commandList) get(key string) *command {
for _, cmd := range c {
if cmd.key == key {
return &cmd
}
}
return nil
}
func (b *Bot) buildCommandList() commandList {
return commandList{
// special commands
{
key: commandHelp,
description: "Show this help message",
accessChecker: b.allowAnyone,
},
{
key: commandStop,
description: "Disable bridge for the room and clear all configuration",
accessChecker: b.allowOwner,
},
{}, // delimiter
// options commands
{
key: optionMailbox,
description: "Get or set mailbox of the room",
sanitizer: utils.Mailbox,
accessChecker: b.allowOwner,
},
{
key: optionOwner,
description: "Get or set owner of the room",
sanitizer: func(s string) string { return s },
accessChecker: 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,
accessChecker: 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,
accessChecker: 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,
accessChecker: 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,
accessChecker: 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,
accessChecker: b.allowOwner,
},
}
}
func (b *Bot) handleCommand(ctx context.Context, evt *event.Event, commandSlice []string) {
cmd := b.commands.get(commandSlice[0])
if cmd == nil {
return
}
// ignore requests over federation if disabled
if !b.federation && evt.Sender.Homeserver() != b.lp.GetClient().UserID.Homeserver() {
return
}
allowed, err := cmd.accessChecker(evt.Sender, evt.RoomID)
if err != nil {
b.Error(ctx, evt.RoomID, err.Error())
return
}
if !allowed {
b.Notice(ctx, evt.RoomID, "not allowed to do that, kupo")
return
}
switch commandSlice[0] {
case commandHelp:
b.sendHelp(ctx, evt.RoomID)
case commandStop:
b.runStop(ctx)
default:
b.handleOption(ctx, commandSlice)
}
}
func (b *Bot) parseCommand(message string) []string {
if message == "" {
return nil
}
index := strings.LastIndex(message, b.prefix)
if index == -1 {
return nil
}
message = strings.ToLower(strings.TrimSpace(strings.Replace(message, b.prefix, "", 1)))
return strings.Split(message, " ")
}
func (b *Bot) sendIntroduction(ctx context.Context, roomID id.RoomID) {
var msg strings.Builder
msg.WriteString("Hello, kupo!\n\n")
msg.WriteString("This is Postmoogle - a bot that bridges Email to Matrix.\n\n")
msg.WriteString("To get started, assign an email address to this room by sending a `")
msg.WriteString(b.prefix)
msg.WriteString(" ")
msg.WriteString(optionMailbox)
msg.WriteString("` command.\n")
msg.WriteString("You will then be able to send emails to `SOME_INBOX@")
msg.WriteString(b.domain)
msg.WriteString("` and have them appear in this room.")
b.Notice(ctx, roomID, msg.String())
}
func (b *Bot) sendHelp(ctx context.Context, roomID id.RoomID) {
evt := eventFromContext(ctx)
cfg, serr := b.getSettings(evt.RoomID)
if serr != nil {
b.log.Error("cannot retrieve settings: %v", serr)
}
var msg strings.Builder
msg.WriteString("The following commands are supported:\n\n")
for _, cmd := range b.commands {
if cmd.key == "" {
msg.WriteString("\n---\n")
continue
}
msg.WriteString("* **`")
msg.WriteString(b.prefix)
msg.WriteString(" ")
msg.WriteString(cmd.key)
msg.WriteString("`**")
value := cfg.Get(cmd.key)
if cmd.sanitizer != nil {
switch value != "" {
case false:
msg.WriteString("(currently not set)")
case true:
msg.WriteString("(currently `")
msg.WriteString(value)
if cmd.key == optionMailbox {
msg.WriteString("@")
msg.WriteString(b.domain)
}
msg.WriteString("`)")
}
}
msg.WriteString(" - ")
msg.WriteString(cmd.description)
msg.WriteString("\n")
}
b.Notice(ctx, roomID, msg.String())
}
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.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 := 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.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
}
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.Notice(ctx, evt.RoomID, fmt.Sprintf("`%s` of this room set to `%s`", name, value))
}