gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 6d138588d8fc450121a3230612e8cf2d9b4e9908
parent 9b4b4d4eb81e16783eb3f21bea6c997d560c0cbe
Author: Daenney <daenney@users.noreply.github.com>
Date:   Sat,  6 May 2023 17:42:58 +0200

[feature] Implement the preferences client API (#1740)

This adds the preferences endpoint to our Mastodon Client API
implementation. It's a read-only endpoint that returns a number of
user preferences. Applications can query these settings when logging in
a user (for the first time) to configure themselves.
Diffstat:
Mdocs/api/swagger.yaml | 41+++++++++++++++++++++++++++++++++++++++++
Minternal/api/client.go | 4++++
Ainternal/api/client/preferences/preferences.go | 44++++++++++++++++++++++++++++++++++++++++++++
Ainternal/api/client/preferences/preferencesget.go | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minternal/api/model/preferences.go | 2++
Ainternal/processing/preferences.go | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/processing/preferences_test.go | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtestrig/testmodels.go | 4++--
8 files changed, 315 insertions(+), 2 deletions(-)

diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml @@ -4651,6 +4651,47 @@ paths: summary: Clear/delete all notifications for currently authorized user. tags: - notifications + /api/v1/preferences: + get: + description: |- + Example: + + ``` + + { + "posting:default:visibility": "public", + "posting:default:sensitive": false, + "posting:default:language": "en", + "reading:expand:media": "default", + "reading:expand:spoilers": false, + "reading:autoplay:gifs": false + } + + ```` + operationId: preferencesGet + produces: + - application/json + responses: + "200": + description: "" + schema: + type: object + "400": + description: bad request + "401": + description: unauthorized + "404": + description: not found + "406": + description: not acceptable + "500": + description: internal server error + security: + - OAuth2 Bearer: + - read:accounts + summary: Return an object of user preferences. + tags: + - preferences /api/v1/reports: get: description: |- diff --git a/internal/api/client.go b/internal/api/client.go @@ -35,6 +35,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/api/client/lists" "github.com/superseriousbusiness/gotosocial/internal/api/client/media" "github.com/superseriousbusiness/gotosocial/internal/api/client/notifications" + "github.com/superseriousbusiness/gotosocial/internal/api/client/preferences" "github.com/superseriousbusiness/gotosocial/internal/api/client/reports" "github.com/superseriousbusiness/gotosocial/internal/api/client/search" "github.com/superseriousbusiness/gotosocial/internal/api/client/statuses" @@ -65,6 +66,7 @@ type Client struct { lists *lists.Module // api/v1/lists media *media.Module // api/v1/media, api/v2/media notifications *notifications.Module // api/v1/notifications + preferences *preferences.Module // api/v1/preferences reports *reports.Module // api/v1/reports search *search.Module // api/v1/search, api/v2/search statuses *statuses.Module // api/v1/statuses @@ -101,6 +103,7 @@ func (c *Client) Route(r router.Router, m ...gin.HandlerFunc) { c.lists.Route(h) c.media.Route(h) c.notifications.Route(h) + c.preferences.Route(h) c.reports.Route(h) c.search.Route(h) c.statuses.Route(h) @@ -128,6 +131,7 @@ func NewClient(db db.DB, p *processing.Processor) *Client { lists: lists.New(p), media: media.New(p), notifications: notifications.New(p), + preferences: preferences.New(p), reports: reports.New(p), search: search.New(p), statuses: statuses.New(p), diff --git a/internal/api/client/preferences/preferences.go b/internal/api/client/preferences/preferences.go @@ -0,0 +1,44 @@ +// 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 preferences + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/processing" +) + +const ( + // BasePath is the base URI path for serving preferences, minus the api prefix. + BasePath = "/v1/preferences" +) + +type Module struct { + processor *processing.Processor +} + +func New(processor *processing.Processor) *Module { + return &Module{ + processor: processor, + } +} + +func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { + attachHandler(http.MethodGet, BasePath, m.PreferencesGETHandler) +} diff --git a/internal/api/client/preferences/preferencesget.go b/internal/api/client/preferences/preferencesget.go @@ -0,0 +1,91 @@ +// 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 preferences + +import ( + "net/http" + + "github.com/gin-gonic/gin" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +// PreferencesGETHandler swagger:operation GET /api/v1/preferences preferencesGet +// +// Return an object of user preferences. +// +// Example: +// +// ``` +// +// { +// "posting:default:visibility": "public", +// "posting:default:sensitive": false, +// "posting:default:language": "en", +// "reading:expand:media": "default", +// "reading:expand:spoilers": false, +// "reading:autoplay:gifs": false +// } +// +// ```` +// +// --- +// tags: +// - preferences +// +// produces: +// - application/json +// +// security: +// - OAuth2 Bearer: +// - read:accounts +// +// responses: +// '200': +// schema: +// type: object +// '400': +// description: bad request +// '401': +// description: unauthorized +// '404': +// description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error +func (m *Module) PreferencesGETHandler(c *gin.Context) { + authed, err := oauth.Authed(c, false, false, false, true) + if err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1) + return + } + + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1) + return + } + + resp, errWithCode := m.processor.PreferencesGet(c.Request.Context(), authed.Account.ID) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + c.JSON(http.StatusOK, resp) +} diff --git a/internal/api/model/preferences.go b/internal/api/model/preferences.go @@ -36,4 +36,6 @@ type Preferences struct { ReadingExpandMedia string `json:"reading:expand:media"` // Whether CWs should be expanded by default. ReadingExpandSpoilers bool `json:"reading:expand:spoilers"` + // Whether gifs should automatically play. + ReadingAutoPlayGifs bool `json:"reading:autoplay:gifs"` } diff --git a/internal/processing/preferences.go b/internal/processing/preferences.go @@ -0,0 +1,57 @@ +// 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 processing + +import ( + "context" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +func (p *Processor) PreferencesGet(ctx context.Context, accountID string) (*apimodel.Preferences, gtserror.WithCode) { + act, err := p.state.DB.GetAccountByID(ctx, accountID) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + return &apimodel.Preferences{ + PostingDefaultVisibility: mastoPrefVisibility(act.Privacy), + PostingDefaultSensitive: *act.Sensitive, + PostingDefaultLanguage: act.Language, + // The Reading* preferences don't appear to actually be settable by the + // client, so forcing some sensible defaults here + ReadingExpandMedia: "default", + ReadingExpandSpoilers: false, + ReadingAutoPlayGifs: false, + }, nil +} + +func mastoPrefVisibility(vis gtsmodel.Visibility) string { + switch vis { + case gtsmodel.VisibilityPublic, gtsmodel.VisibilityDirect: + return string(vis) + case gtsmodel.VisibilityUnlocked: + return "unlisted" + default: + // this will catch gtsmodel.VisibilityMutualsOnly and other types Mastodon doesn't + // have and map them to the most restrictive state + return "private" + } +} diff --git a/internal/processing/preferences_test.go b/internal/processing/preferences_test.go @@ -0,0 +1,74 @@ +// 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 processing_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +type PreferencesTestSuite struct { + ProcessingStandardTestSuite +} + +func (suite *PreferencesTestSuite) TestPreferencesGet() { + ctx := context.Background() + tests := []struct { + act *gtsmodel.Account + prefs *model.Preferences + }{ + { + act: suite.testAccounts["local_account_1"], + prefs: &model.Preferences{ + PostingDefaultVisibility: "public", + PostingDefaultSensitive: false, + PostingDefaultLanguage: "en", + ReadingExpandMedia: "default", + ReadingExpandSpoilers: false, + ReadingAutoPlayGifs: false, + }, + }, + { + act: suite.testAccounts["local_account_2"], + prefs: &model.Preferences{ + PostingDefaultVisibility: "private", + PostingDefaultSensitive: true, + PostingDefaultLanguage: "fr", + ReadingExpandMedia: "default", + ReadingExpandSpoilers: false, + ReadingAutoPlayGifs: false, + }, + }, + } + + for _, tt := range tests { + suite.Run(tt.act.ID, func() { + prefs, err := suite.processor.PreferencesGet(ctx, tt.act.ID) + suite.NoError(err) + suite.Equal(tt.prefs, prefs) + }) + } +} + +func TestPreferencesTestSuite(t *testing.T) { + suite.Run(t, &PreferencesTestSuite{}) +} diff --git a/testrig/testmodels.go b/testrig/testmodels.go @@ -487,8 +487,8 @@ func NewTestAccounts() map[string]*gtsmodel.Account { Locked: TrueBool(), Discoverable: FalseBool(), Privacy: gtsmodel.VisibilityFollowersOnly, - Sensitive: FalseBool(), - Language: "en", + Sensitive: TrueBool(), + Language: "fr", URI: "http://localhost:8080/users/1happyturtle", URL: "http://localhost:8080/@1happyturtle", FetchedAt: time.Time{},