feat(config): add attachment MIME allowlist and keybinds
This commit is contained in:
parent
52e7d6ed0e
commit
86c1fd6a77
4 changed files with 101 additions and 6 deletions
|
|
@ -4,7 +4,9 @@ import (
|
|||
_ "embed"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"mime"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
|
|
@ -106,6 +108,7 @@ type (
|
|||
Status discord.Status `toml:"status"`
|
||||
HideBlockedUsers bool `toml:"hide_blocked_users"`
|
||||
ShowAttachmentLinks bool `toml:"show_attachment_links"`
|
||||
AllowedMIMETypes MIMETypes `toml:"allowed_mime_types"`
|
||||
|
||||
// Use 0 to disable
|
||||
AutocompleteLimit uint8 `toml:"autocomplete_limit"`
|
||||
|
|
@ -170,6 +173,45 @@ func Load(path string) (*Config, error) {
|
|||
return &cfg, nil
|
||||
}
|
||||
|
||||
type MIMETypes []string
|
||||
|
||||
func (types *MIMETypes) UnmarshalTOML(value any) error {
|
||||
values, ok := value.([]any)
|
||||
if !ok {
|
||||
return errInvalidType
|
||||
}
|
||||
|
||||
parsed := make(MIMETypes, 0, len(values))
|
||||
for _, value := range values {
|
||||
mediaType, ok := value.(string)
|
||||
if !ok {
|
||||
return errInvalidType
|
||||
}
|
||||
mediaType, _, err := mime.ParseMediaType(mediaType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parsed = append(parsed, mediaType)
|
||||
}
|
||||
|
||||
*types = parsed
|
||||
return nil
|
||||
}
|
||||
|
||||
func (types MIMETypes) Allows(mediaType string) bool {
|
||||
mediaType, _, err := mime.ParseMediaType(mediaType)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, pattern := range types {
|
||||
if matches, _ := path.Match(pattern, mediaType); matches {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func applyDefaults(cfg *Config) {
|
||||
if cfg.Editor == "default" {
|
||||
cfg.Editor = os.Getenv("EDITOR")
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ status = "default"
|
|||
|
||||
hide_blocked_users = true
|
||||
show_attachment_links = true
|
||||
# Attachment MIME types that can be opened or downloaded without confirmation.
|
||||
# Exact types and wildcards such as "image/*" are supported.
|
||||
allowed_mime_types = ["image/*", "video/*"]
|
||||
|
||||
# Max members to be in the mention autocomplete suggestions list
|
||||
# Note: Use autocomplete_limit = 0 to disable.
|
||||
|
|
@ -160,11 +163,16 @@ reply = "R"
|
|||
reply_mention = "r"
|
||||
cancel = "esc"
|
||||
edit = "e"
|
||||
delete = "D"
|
||||
# Delete without confirmation.
|
||||
# delete = "D"
|
||||
delete_confirm = "d"
|
||||
# Open the selected message's attachments or hyperlinks in the message
|
||||
# using the default browser application.
|
||||
# Open attachments with their associated application. MIME types outside the
|
||||
# allowlist require confirmation. Hyperlinks still use the browser.
|
||||
open = "o"
|
||||
# Open attachments or hyperlinks in the browser.
|
||||
open_in_browser = "O"
|
||||
# Download one of the selected message's attachments.
|
||||
download = "D"
|
||||
# Yank (copy) the selected message's content/url/id.
|
||||
yank_content = "y"
|
||||
yank_url = "u"
|
||||
|
|
|
|||
|
|
@ -70,6 +70,17 @@ func TestLoad(t *testing.T) {
|
|||
}
|
||||
})
|
||||
|
||||
t.Run("invalid allowed MIME type returns error", func(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "bad-mime.toml")
|
||||
if err := os.WriteFile(path, []byte(`allowed_mime_types = ["image/"]`), os.ModePerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := Load(path); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
})
|
||||
|
||||
// Keys live in config.toml, descriptions in defaultKeybinds(); fail if either is missing.
|
||||
t.Run("default keybinds are fully populated", func(t *testing.T) {
|
||||
cfg, err := Load(filepath.Join(t.TempDir(), "missing.toml"))
|
||||
|
|
@ -84,7 +95,10 @@ func TestLoad(t *testing.T) {
|
|||
fv, name := v.Field(i), path+rt.Field(i).Name
|
||||
if kb, ok := fv.Interface().(Keybind); ok {
|
||||
if len(kb.Keys()) == 0 {
|
||||
t.Errorf("%s: no keys (missing from config.toml?)", name)
|
||||
if rt.Field(i).Tag.Get("toml") != "delete" {
|
||||
t.Errorf("%s: no keys (missing from config.toml?)", name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if kb.Help().Desc == "" {
|
||||
t.Errorf("%s: no help description (missing from defaultKeybinds?)", name)
|
||||
|
|
@ -131,3 +145,28 @@ func TestLoad(t *testing.T) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMIMETypesAllows(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
allowed MIMETypes
|
||||
mediaType string
|
||||
wantAllowed bool
|
||||
}{
|
||||
{"exact", []string{"application/pdf"}, "application/pdf", true},
|
||||
{"exact mismatch", []string{"application/pdf"}, "application/zip", false},
|
||||
{"subtype wildcard", []string{"image/*"}, "image/png", true},
|
||||
{"wildcard mismatch", []string{"image/*"}, "video/mp4", false},
|
||||
{"all wildcard", []string{"*/*"}, "video/mp4", true},
|
||||
{"case and parameters", []string{"image/*"}, "Image/PNG; charset=utf-8", true},
|
||||
{"invalid content type", []string{"*/*"}, "not-a-mime-type", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.allowed.Allows(tt.mediaType); got != tt.wantAllowed {
|
||||
t.Fatalf("Allows(%q) = %v, want %v", tt.mediaType, got, tt.wantAllowed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,11 +76,15 @@ type MessagesListKeybinds struct {
|
|||
Reply Keybind `toml:"reply"`
|
||||
ReplyMention Keybind `toml:"reply_mention"`
|
||||
|
||||
Cancel Keybind `toml:"cancel"`
|
||||
Edit Keybind `toml:"edit"`
|
||||
Cancel Keybind `toml:"cancel"`
|
||||
Edit Keybind `toml:"edit"`
|
||||
|
||||
Delete Keybind `toml:"delete"`
|
||||
DeleteConfirm Keybind `toml:"delete_confirm"`
|
||||
|
||||
Open Keybind `toml:"open"`
|
||||
OpenInBrowser Keybind `toml:"open_in_browser"`
|
||||
Download Keybind `toml:"download"`
|
||||
|
||||
YankContent Keybind `toml:"yank_content"`
|
||||
YankURL Keybind `toml:"yank_url"`
|
||||
|
|
@ -171,6 +175,8 @@ func defaultMessagesListKeybinds() MessagesListKeybinds {
|
|||
Delete: desc("force delete"),
|
||||
DeleteConfirm: desc("delete"),
|
||||
Open: desc("open"),
|
||||
OpenInBrowser: desc("open browser"),
|
||||
Download: desc("download"),
|
||||
YankContent: desc("copy text"),
|
||||
YankURL: desc("copy url"),
|
||||
YankID: desc("copy id"),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue