diff options
author | 2023-07-29 03:49:14 -0700 | |
---|---|---|
committer | 2023-07-29 12:49:14 +0200 | |
commit | b874e9251e00961f295e4c409e1b34da89fab4ed (patch) | |
tree | cb528816250f322707a90c0954c963886ea96c19 /internal/api | |
parent | [chore] Update activity dependency (#2031) (diff) | |
download | gotosocial-b874e9251e00961f295e4c409e1b34da89fab4ed.tar.xz |
[feature] Implement markers API (#1989)
* Implement markers API
Fixes #1856
* Correct import grouping in markers files
* Regenerate Swagger for markers API
* Shorten names for readability
* Cache markers for 6 hours
* Update DB ref
* Update envparsing.sh
Diffstat (limited to 'internal/api')
-rw-r--r-- | internal/api/client.go | 4 | ||||
-rw-r--r-- | internal/api/client/markers/markers.go | 45 | ||||
-rw-r--r-- | internal/api/client/markers/markersget.go | 108 | ||||
-rw-r--r-- | internal/api/client/markers/markerspost.go | 110 | ||||
-rw-r--r-- | internal/api/model/marker.go | 47 |
5 files changed, 311 insertions, 3 deletions
diff --git a/internal/api/client.go b/internal/api/client.go index d9f131e3b..e91ee2d2c 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -33,6 +33,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/api/client/followrequests" "github.com/superseriousbusiness/gotosocial/internal/api/client/instance" "github.com/superseriousbusiness/gotosocial/internal/api/client/lists" + "github.com/superseriousbusiness/gotosocial/internal/api/client/markers" "github.com/superseriousbusiness/gotosocial/internal/api/client/media" "github.com/superseriousbusiness/gotosocial/internal/api/client/notifications" "github.com/superseriousbusiness/gotosocial/internal/api/client/preferences" @@ -64,6 +65,7 @@ type Client struct { followRequests *followrequests.Module // api/v1/follow_requests instance *instance.Module // api/v1/instance lists *lists.Module // api/v1/lists + markers *markers.Module // api/v1/markers media *media.Module // api/v1/media, api/v2/media notifications *notifications.Module // api/v1/notifications preferences *preferences.Module // api/v1/preferences @@ -104,6 +106,7 @@ func (c *Client) Route(r router.Router, m ...gin.HandlerFunc) { c.followRequests.Route(h) c.instance.Route(h) c.lists.Route(h) + c.markers.Route(h) c.media.Route(h) c.notifications.Route(h) c.preferences.Route(h) @@ -132,6 +135,7 @@ func NewClient(db db.DB, p *processing.Processor) *Client { followRequests: followrequests.New(p), instance: instance.New(p), lists: lists.New(p), + markers: markers.New(p), media: media.New(p), notifications: notifications.New(p), preferences: preferences.New(p), diff --git a/internal/api/client/markers/markers.go b/internal/api/client/markers/markers.go new file mode 100644 index 000000000..fcf584d12 --- /dev/null +++ b/internal/api/client/markers/markers.go @@ -0,0 +1,45 @@ +// 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 markers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/processing" +) + +const ( + // BasePath is the base path for serving the markers API, minus the 'api' prefix + BasePath = "/v1/markers" +) + +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.MarkersGETHandler) + attachHandler(http.MethodPost, BasePath, m.MarkersPOSTHandler) +} diff --git a/internal/api/client/markers/markersget.go b/internal/api/client/markers/markersget.go new file mode 100644 index 000000000..eb403dcc6 --- /dev/null +++ b/internal/api/client/markers/markersget.go @@ -0,0 +1,108 @@ +// 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 markers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/oauth" + "github.com/superseriousbusiness/gotosocial/internal/validate" +) + +// MarkersGETHandler swagger:operation GET /api/v1/markers markersGet +// +// Get timeline markers by name +// +// --- +// tags: +// - markers +// +// produces: +// - application/json +// +// parameters: +// - +// name: timeline +// type: array +// items: +// type: string +// enum: +// - home +// - notifications +// description: Timelines to retrieve. +// in: query +// +// security: +// - OAuth2 Bearer: +// - read:statuses +// +// responses: +// '200': +// description: Requested markers +// schema: +// "$ref": "#/definitions/markers" +// '400': +// description: bad request +// '401': +// description: unauthorized +// '500': +// description: internal server error +func (m *Module) MarkersGETHandler(c *gin.Context) { + authed, err := oauth.Authed(c, true, true, true, true) + if err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1) + return + } + + names, errWithCode := parseMarkerNames(c.QueryArray("timeline[]")) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + } + + marker, errWithCode := m.processor.Markers().Get(c.Request.Context(), authed.Account, names) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + c.JSON(http.StatusOK, marker) +} + +// parseMarkerNames turns a list of strings into a set of valid marker timeline names, or returns an error. +func parseMarkerNames(nameStrings []string) ([]apimodel.MarkerName, gtserror.WithCode) { + nameSet := make(map[apimodel.MarkerName]struct{}, apimodel.MarkerNameNumValues) + for _, timelineString := range nameStrings { + if err := validate.MarkerName(timelineString); err != nil { + return nil, gtserror.NewErrorBadRequest(err, err.Error()) + } + nameSet[apimodel.MarkerName(timelineString)] = struct{}{} + } + + i := 0 + names := make([]apimodel.MarkerName, len(nameSet)) + for name := range nameSet { + names[i] = name + i++ + } + + return names, nil +} diff --git a/internal/api/client/markers/markerspost.go b/internal/api/client/markers/markerspost.go new file mode 100644 index 000000000..3167becac --- /dev/null +++ b/internal/api/client/markers/markerspost.go @@ -0,0 +1,110 @@ +// 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 markers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +// MarkersPOSTHandler swagger:operation POST /api/v1/markers markersPost +// +// Update timeline markers by name +// +// --- +// tags: +// - markers +// +// consumes: +// - multipart/form-data +// +// produces: +// - application/json +// +// parameters: +// - +// name: home[last_read_id] +// type: string +// description: Last status ID read on the home timeline. +// in: formData +// - +// name: notifications[last_read_id] +// type: string +// description: Last notification ID read on the notifications timeline. +// in: formData +// +// security: +// - OAuth2 Bearer: +// - write:statuses +// +// responses: +// '200': +// description: Requested markers +// schema: +// "$ref": "#/definitions/markers" +// '400': +// description: bad request +// '401': +// description: unauthorized +// '409': +// description: conflict (when two clients try to update the same timeline at the same time) +// '500': +// description: internal server error +func (m *Module) MarkersPOSTHandler(c *gin.Context) { + authed, err := oauth.Authed(c, true, true, true, true) + if err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1) + return + } + + form := &apimodel.MarkerPostRequest{} + if err := c.ShouldBind(form); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) + return + } + + markers := make([]*gtsmodel.Marker, 0, apimodel.MarkerNameNumValues) + if homeLastReadID := form.HomeLastReadID(); homeLastReadID != "" { + markers = append(markers, >smodel.Marker{ + AccountID: authed.Account.ID, + Name: gtsmodel.MarkerNameHome, + LastReadID: homeLastReadID, + }) + } + if notificationsLastReadID := form.NotificationsLastReadID(); notificationsLastReadID != "" { + markers = append(markers, >smodel.Marker{ + AccountID: authed.Account.ID, + Name: gtsmodel.MarkerNameNotifications, + LastReadID: notificationsLastReadID, + }) + } + + marker, errWithCode := m.processor.Markers().Update(c.Request.Context(), markers) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + c.JSON(http.StatusOK, marker) +} diff --git a/internal/api/model/marker.go b/internal/api/model/marker.go index f64b75ab4..f2d5cb296 100644 --- a/internal/api/model/marker.go +++ b/internal/api/model/marker.go @@ -20,9 +20,9 @@ package model // Marker represents the last read position within a user's timelines. type Marker struct { // Information about the user's position in the home timeline. - Home *TimelineMarker `json:"home"` + Home *TimelineMarker `json:"home,omitempty"` // Information about the user's position in their notifications. - Notifications *TimelineMarker `json:"notifications"` + Notifications *TimelineMarker `json:"notifications,omitempty"` } // TimelineMarker contains information about a user's progress through a specific timeline. @@ -32,5 +32,46 @@ type TimelineMarker struct { // The timestamp of when the marker was set (ISO 8601 Datetime) UpdatedAt string `json:"updated_at"` // Used for locking to prevent write conflicts. - Version string `json:"version"` + Version int `json:"version"` +} + +// MarkerName is the name of one of the timelines we can store markers for. +type MarkerName string + +const ( + MarkerNameHome MarkerName = "home" + MarkerNameNotifications MarkerName = "notifications" + MarkerNameNumValues = 2 +) + +// MarkerPostRequest models a request to update one or more markers. +// This has two sets of fields to support a goofy nested map structure in both form data and JSON bodies. +// +// swagger:ignore +type MarkerPostRequest struct { + Home *MarkerPostRequestMarker `json:"home"` + FormHomeLastReadID string `form:"home[last_read_id]"` + Notifications *MarkerPostRequestMarker `json:"notifications"` + FormNotificationsLastReadID string `form:"notifications[last_read_id]"` +} + +type MarkerPostRequestMarker struct { + // The ID of the most recently viewed entity. + LastReadID string `json:"last_read_id"` +} + +// HomeLastReadID should be used instead of Home or FormHomeLastReadID. +func (r *MarkerPostRequest) HomeLastReadID() string { + if r.Home != nil { + return r.Home.LastReadID + } + return r.FormHomeLastReadID +} + +// NotificationsLastReadID should be used instead of Notifications or FormNotificationsLastReadID. +func (r *MarkerPostRequest) NotificationsLastReadID() string { + if r.Notifications != nil { + return r.Notifications.LastReadID + } + return r.FormNotificationsLastReadID } |