summaryrefslogtreecommitdiff
path: root/internal/processing/account
diff options
context:
space:
mode:
Diffstat (limited to 'internal/processing/account')
-rw-r--r--internal/processing/account/account.go2
-rw-r--r--internal/processing/account/themes.go151
-rw-r--r--internal/processing/account/themes_test.go52
-rw-r--r--internal/processing/account/update.go16
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 := &gtsmodel.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 {