mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-11-24 04:03:33 -06:00
[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
This commit is contained in:
parent
cf4bd700fb
commit
b874e9251e
29 changed files with 1083 additions and 3 deletions
|
|
@ -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),
|
||||
|
|
|
|||
45
internal/api/client/markers/markers.go
Normal file
45
internal/api/client/markers/markers.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
108
internal/api/client/markers/markersget.go
Normal file
108
internal/api/client/markers/markersget.go
Normal file
|
|
@ -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
|
||||
}
|
||||
110
internal/api/client/markers/markerspost.go
Normal file
110
internal/api/client/markers/markerspost.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue