diff options
Diffstat (limited to 'internal/processing')
-rw-r--r-- | internal/processing/account/account.go | 2 | ||||
-rw-r--r-- | internal/processing/account/themes.go | 151 | ||||
-rw-r--r-- | internal/processing/account/themes_test.go | 52 | ||||
-rw-r--r-- | internal/processing/account/update.go | 16 |
4 files changed, 221 insertions, 0 deletions
diff --git a/internal/processing/account/account.go b/internal/processing/account/account.go index 79f6ecfc1..dbcecdb0a 100644 --- a/internal/processing/account/account.go +++ b/internal/processing/account/account.go @@ -44,6 +44,7 @@ type Processor struct { formatter *text.Formatter federator *federation.Federator parseMention gtsmodel.ParseMentionFunc + themes *Themes } // New returns a new account processor. @@ -67,5 +68,6 @@ func New( formatter: text.NewFormatter(state.DB), federator: federator, parseMention: parseMention, + themes: PopulateThemes(), } } diff --git a/internal/processing/account/themes.go b/internal/processing/account/themes.go new file mode 100644 index 000000000..4f8cc49a1 --- /dev/null +++ b/internal/processing/account/themes.go @@ -0,0 +1,151 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package account + +import ( + "cmp" + "os" + "path/filepath" + "regexp" + "slices" + "strings" + + "codeberg.org/gruf/go-bytesize" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" +) + +var ( + themeTitleRegex = regexp.MustCompile(`(?m)^\ *theme-title:(.*)$`) + themeDescriptionRegex = regexp.MustCompile(`(?m)^\ *theme-description:(.*)$`) +) + +// GetThemes returns available account css themes. +func (p *Processor) ThemesGet() []apimodel.Theme { + return p.converter.ThemesToAPIThemes(p.themes.SortedByTitle) +} + +// Themes represents an in-memory +// storage structure for themes. +type Themes struct { + // Themes sorted alphabetically + // by title (case insensitive). + SortedByTitle []*gtsmodel.Theme + + // ByFileName contains themes retrievable + // by their filename eg., `light-blurple.css`. + ByFileName map[string]*gtsmodel.Theme +} + +// PopulateThemes parses available account CSS +// themes from the web assets themes directory. +func PopulateThemes() *Themes { + webAssetsAbsFilePath, err := filepath.Abs(config.GetWebAssetBaseDir()) + if err != nil { + log.Panicf(nil, "error getting abs path for web assets: %v", err) + } + + themesAbsFilePath := filepath.Join(webAssetsAbsFilePath, "themes") + themesFiles, err := os.ReadDir(themesAbsFilePath) + if err != nil { + log.Warnf(nil, "error reading themes at %s: %v", themesAbsFilePath, err) + return nil + } + + themes := &Themes{ + ByFileName: make(map[string]*gtsmodel.Theme), + } + + for _, f := range themesFiles { + // Ignore nested directories. + if f.IsDir() { + continue + } + + // Ignore weird files. + info, err := f.Info() + if err != nil { + continue + } + + // Ignore really big files. + if info.Size() > int64(bytesize.MiB) { + continue + } + + // Get just the name of the + // file, eg `blurple-light.css`. + fileName := f.Name() + + // Get just the `.css` part. + extensionWithDot := filepath.Ext(fileName) + + // Remove any leading `.` + extension := strings.TrimPrefix(extensionWithDot, ".") + + // Ignore non-css files. + if extension != "css" { + continue + } + + // Load the file contents. + path := filepath.Join(themesAbsFilePath, fileName) + contents, err := os.ReadFile(path) + if err != nil { + log.Warnf(nil, "error reading css theme at %s: %v", path, err) + continue + } + + // Try to parse a title and description + // for this theme from the file itself. + var themeTitle string + titleMatches := themeTitleRegex.FindSubmatch(contents) + if len(titleMatches) == 2 { + themeTitle = strings.TrimSpace(string(titleMatches[1])) + } else { + // Fall back to file name + // without `.css` suffix. + themeTitle = strings.TrimSuffix(fileName, ".css") + } + + var themeDescription string + descMatches := themeDescriptionRegex.FindSubmatch(contents) + if len(descMatches) == 2 { + themeDescription = strings.TrimSpace(string(descMatches[1])) + } + + theme := >smodel.Theme{ + Title: themeTitle, + Description: themeDescription, + FileName: fileName, + } + + themes.SortedByTitle = append(themes.SortedByTitle, theme) + themes.ByFileName[fileName] = theme + } + + // Sort themes alphabetically + // by title (case insensitive). + slices.SortFunc(themes.SortedByTitle, func(a, b *gtsmodel.Theme) int { + return cmp.Compare(strings.ToLower(a.Title), strings.ToLower(b.Title)) + }) + + return themes +} diff --git a/internal/processing/account/themes_test.go b/internal/processing/account/themes_test.go new file mode 100644 index 000000000..9506aee50 --- /dev/null +++ b/internal/processing/account/themes_test.go @@ -0,0 +1,52 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package account_test + +import ( + "testing" + + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/processing/account" +) + +type ThemesTestSuite struct { + AccountStandardTestSuite +} + +func (suite *ThemesTestSuite) TestPopulateThemes() { + config.SetWebAssetBaseDir("../../../web/assets") + + themes := account.PopulateThemes() + if themes == nil { + suite.FailNow("themes was nil") + } + + suite.NotEmpty(themes.SortedByTitle) + theme := themes.ByFileName["blurple-light.css"] + if theme == nil { + suite.FailNow("theme was nil") + } + suite.Equal("Blurple (light)", theme.Title) + suite.Equal("Official light blurple theme", theme.Description) + suite.Equal("blurple-light.css", theme.FileName) +} + +func TestThemesTestSuite(t *testing.T) { + suite.Run(t, new(ThemesTestSuite)) +} diff --git a/internal/processing/account/update.go b/internal/processing/account/update.go index 7b5561138..076b6d7f4 100644 --- a/internal/processing/account/update.go +++ b/internal/processing/account/update.go @@ -256,6 +256,22 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form } } + if form.Theme != nil { + theme := *form.Theme + if theme == "" { + // Empty is easy, just clear this. + account.Settings.Theme = "" + } else { + // Theme was provided, check + // against known available themes. + if _, ok := p.themes.ByFileName[theme]; !ok { + err := fmt.Errorf("theme %s not available on this instance, see /api/v1/accounts/themes for available themes", theme) + return nil, gtserror.NewErrorBadRequest(err, err.Error()) + } + account.Settings.Theme = theme + } + } + if form.CustomCSS != nil { customCSS := *form.CustomCSS if err := validate.CustomCSS(customCSS); err != nil { |