Browse Source
* 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.shpull/2040/head
29 changed files with 1083 additions and 3 deletions
@ -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) |
||||
} |
||||
@ -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 |
||||
} |
||||
@ -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) |
||||
} |
||||
@ -0,0 +1,115 @@
|
||||
// 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 bundb |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"fmt" |
||||
"time" |
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db" |
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" |
||||
"github.com/superseriousbusiness/gotosocial/internal/state" |
||||
"github.com/uptrace/bun" |
||||
) |
||||
|
||||
type markerDB struct { |
||||
db *WrappedDB |
||||
state *state.State |
||||
} |
||||
|
||||
/* |
||||
MARKER FUNCTIONS |
||||
*/ |
||||
|
||||
func (m *markerDB) GetMarker(ctx context.Context, accountID string, name gtsmodel.MarkerName) (*gtsmodel.Marker, error) { |
||||
marker, err := m.state.Caches.GTS.Marker().Load( |
||||
"AccountID.Name", |
||||
func() (*gtsmodel.Marker, error) { |
||||
var marker gtsmodel.Marker |
||||
|
||||
if err := m.db.NewSelect(). |
||||
Model(&marker). |
||||
Where("? = ? AND ? = ?", bun.Ident("account_id"), accountID, bun.Ident("name"), name). |
||||
Scan(ctx); err != nil { |
||||
return nil, m.db.ProcessError(err) |
||||
} |
||||
|
||||
return &marker, nil |
||||
}, |
||||
accountID, |
||||
name, |
||||
) |
||||
if err != nil { |
||||
return nil, err // already processed
|
||||
} |
||||
|
||||
return marker, nil |
||||
} |
||||
|
||||
func (m *markerDB) UpdateMarker(ctx context.Context, marker *gtsmodel.Marker) error { |
||||
prevMarker, err := m.GetMarker(ctx, marker.AccountID, marker.Name) |
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) { |
||||
return fmt.Errorf("UpdateMarker: error fetching previous version of marker: %w", err) |
||||
} |
||||
|
||||
marker.UpdatedAt = time.Now() |
||||
if prevMarker != nil { |
||||
marker.Version = prevMarker.Version + 1 |
||||
} |
||||
|
||||
return m.state.Caches.GTS.Marker().Store(marker, func() error { |
||||
if prevMarker == nil { |
||||
if _, err := m.db.NewInsert(). |
||||
Model(marker). |
||||
Exec(ctx); err != nil { |
||||
return m.db.ProcessError(err) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// Optimistic concurrency control: start a transaction, try to update a row with a previously retrieved version.
|
||||
// If the update in the transaction fails to actually change anything, another update happened concurrently, and
|
||||
// this update should be retried by the caller, which in this case involves sending HTTP 409 to the API client.
|
||||
return m.db.RunInTx(ctx, func(tx bun.Tx) error { |
||||
result, err := tx.NewUpdate(). |
||||
Model(marker). |
||||
WherePK(). |
||||
Where("? = ?", bun.Ident("version"), prevMarker.Version). |
||||
Exec(ctx) |
||||
if err != nil { |
||||
return m.db.ProcessError(err) |
||||
} |
||||
|
||||
rowsAffected, err := result.RowsAffected() |
||||
if err != nil { |
||||
return m.db.ProcessError(err) |
||||
} |
||||
if rowsAffected == 0 { |
||||
// Will trigger a rollback, although there should be no changes to roll back.
|
||||
return db.ErrAlreadyExists |
||||
} else if rowsAffected > 1 { |
||||
// This shouldn't happen.
|
||||
return db.ErrNoEntries |
||||
} |
||||
|
||||
return nil |
||||
}) |
||||
}) |
||||
} |
||||
@ -0,0 +1,127 @@
|
||||
// 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 bundb_test |
||||
|
||||
import ( |
||||
"context" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/stretchr/testify/suite" |
||||
"github.com/superseriousbusiness/gotosocial/internal/db" |
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" |
||||
) |
||||
|
||||
// MarkersTestSuite uses home timelines for Get tests
|
||||
// and notifications timelines for Update tests
|
||||
// so that multiple tests running at once can't step on each other.
|
||||
type MarkersTestSuite struct { |
||||
BunDBStandardTestSuite |
||||
} |
||||
|
||||
func (suite *MarkersTestSuite) TestGetExisting() { |
||||
ctx := context.Background() |
||||
|
||||
// This account has home and notifications markers set.
|
||||
localAccount1 := suite.testAccounts["local_account_1"] |
||||
marker, err := suite.db.GetMarker(ctx, localAccount1.ID, gtsmodel.MarkerNameHome) |
||||
suite.NoError(err) |
||||
if err != nil { |
||||
suite.FailNow(err.Error()) |
||||
} |
||||
// Should match our fixture.
|
||||
suite.Equal("01F8MH82FYRXD2RC6108DAJ5HB", marker.LastReadID) |
||||
} |
||||
|
||||
func (suite *MarkersTestSuite) TestGetUnset() { |
||||
ctx := context.Background() |
||||
|
||||
// This account has no markers set.
|
||||
localAccount2 := suite.testAccounts["local_account_2"] |
||||
marker, err := suite.db.GetMarker(ctx, localAccount2.ID, gtsmodel.MarkerNameHome) |
||||
// Should not return anything.
|
||||
suite.Nil(marker) |
||||
suite.ErrorIs(err, db.ErrNoEntries) |
||||
} |
||||
|
||||
func (suite *MarkersTestSuite) TestUpdateExisting() { |
||||
ctx := context.Background() |
||||
|
||||
now := time.Now() |
||||
// This account has home and notifications markers set.
|
||||
localAccount1 := suite.testAccounts["local_account_1"] |
||||
prevMarker := suite.testMarkers["local_account_1_notification_marker"] |
||||
marker := >smodel.Marker{ |
||||
AccountID: localAccount1.ID, |
||||
Name: gtsmodel.MarkerNameNotifications, |
||||
LastReadID: "01H57YZECGJ2ZW39H8TJWAH0KY", |
||||
} |
||||
err := suite.db.UpdateMarker(ctx, marker) |
||||
suite.NoError(err) |
||||
if err != nil { |
||||
suite.FailNow(err.Error()) |
||||
} |
||||
// Modifies the update and version fields of the marker as an intentional side effect.
|
||||
suite.GreaterOrEqual(marker.UpdatedAt, now) |
||||
suite.Greater(marker.Version, prevMarker.Version) |
||||
|
||||
// Re-fetch it from the DB and confirm that we got the updated version.
|
||||
marker2, err := suite.db.GetMarker(ctx, localAccount1.ID, gtsmodel.MarkerNameNotifications) |
||||
suite.NoError(err) |
||||
if err != nil { |
||||
suite.FailNow(err.Error()) |
||||
} |
||||
suite.GreaterOrEqual(marker2.UpdatedAt, now) |
||||
suite.GreaterOrEqual(marker2.Version, prevMarker.Version) |
||||
suite.Equal("01H57YZECGJ2ZW39H8TJWAH0KY", marker2.LastReadID) |
||||
} |
||||
|
||||
func (suite *MarkersTestSuite) TestUpdateUnset() { |
||||
ctx := context.Background() |
||||
|
||||
now := time.Now() |
||||
// This account has no markers set.
|
||||
localAccount2 := suite.testAccounts["local_account_2"] |
||||
marker := >smodel.Marker{ |
||||
AccountID: localAccount2.ID, |
||||
Name: gtsmodel.MarkerNameNotifications, |
||||
LastReadID: "01H57ZVGMD348ZJD5WENDZDH9Z", |
||||
} |
||||
err := suite.db.UpdateMarker(ctx, marker) |
||||
suite.NoError(err) |
||||
if err != nil { |
||||
suite.FailNow(err.Error()) |
||||
} |
||||
// Modifies the update and version fields of the marker as an intentional side effect.
|
||||
suite.GreaterOrEqual(marker.UpdatedAt, now) |
||||
suite.GreaterOrEqual(marker.Version, 0) |
||||
|
||||
// Re-fetch it from the DB and confirm that we got the updated version.
|
||||
marker2, err := suite.db.GetMarker(ctx, localAccount2.ID, gtsmodel.MarkerNameNotifications) |
||||
suite.NoError(err) |
||||
if err != nil { |
||||
suite.FailNow(err.Error()) |
||||
} |
||||
suite.GreaterOrEqual(marker2.UpdatedAt, now) |
||||
suite.GreaterOrEqual(marker2.Version, 0) |
||||
suite.Equal("01H57ZVGMD348ZJD5WENDZDH9Z", marker2.LastReadID) |
||||
} |
||||
|
||||
func TestMarkersTestSuite(t *testing.T) { |
||||
suite.Run(t, new(MarkersTestSuite)) |
||||
} |
||||
@ -0,0 +1,66 @@
|
||||
// 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 migrations |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
gtsmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" |
||||
"github.com/uptrace/bun" |
||||
) |
||||
|
||||
func init() { |
||||
up := func(ctx context.Context, db *bun.DB) error { |
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { |
||||
// Marker table.
|
||||
if _, err := tx. |
||||
NewCreateTable(). |
||||
Model(>smodel.Marker{}). |
||||
IfNotExists(). |
||||
Exec(ctx); err != nil { |
||||
return err |
||||
} |
||||
|
||||
// Add indexes to the Marker table.
|
||||
for index, columns := range map[string][]string{ |
||||
"markers_account_id_name_idx": {"account_id", "name"}, |
||||
} { |
||||
if _, err := tx. |
||||
NewCreateIndex(). |
||||
Table("markers"). |
||||
Index(index). |
||||
Column(columns...). |
||||
Exec(ctx); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
}) |
||||
} |
||||
|
||||
down := func(ctx context.Context, db *bun.DB) error { |
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { |
||||
return nil |
||||
}) |
||||
} |
||||
|
||||
if err := Migrations.Register(up, down); err != nil { |
||||
panic(err) |
||||
} |
||||
} |
||||
@ -0,0 +1,32 @@
|
||||
// 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 db |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" |
||||
) |
||||
|
||||
type Marker interface { |
||||
// GetMarker gets one marker with the given timeline name.
|
||||
GetMarker(ctx context.Context, accountID string, name gtsmodel.MarkerName) (*gtsmodel.Marker, error) |
||||
|
||||
// UpdateMarker updates the given marker.
|
||||
UpdateMarker(ctx context.Context, marker *gtsmodel.Marker) error |
||||
} |
||||
@ -0,0 +1,37 @@
|
||||
// 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 gtsmodel |
||||
|
||||
import "time" |
||||
|
||||
// Marker stores a local account's read position on a given timeline.
|
||||
type Marker struct { |
||||
AccountID string `validate:"required,ulid" bun:"type:CHAR(26),pk,unique:markers_account_id_timeline_uniq,notnull,nullzero"` // ID of the local account that owns the marker
|
||||
Name MarkerName `validate:"oneof=home notifications" bun:",nullzero,notnull,pk,unique:markers_account_id_timeline_uniq"` // Name of the marked timeline
|
||||
UpdatedAt time.Time `validate:"required" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // When marker was last updated
|
||||
Version int `validate:"required,min=0" bun:",nullzero,notnull,default:0"` // For optimistic concurrency control
|
||||
LastReadID string `validate:"required,ulid" bun:"type:CHAR(26),notnull,nullzero"` // Last ID read on this timeline (status ID for home, notification ID for notifications)
|
||||
} |
||||
|
||||
// MarkerName is the name of one of the timelines we can store markers for.
|
||||
type MarkerName string |
||||
|
||||
const ( |
||||
MarkerNameHome MarkerName = "home" |
||||
MarkerNameNotifications MarkerName = "notifications" |
||||
) |
||||
@ -0,0 +1,54 @@
|
||||
// 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 ( |
||||
"context" |
||||
"errors" |
||||
"fmt" |
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" |
||||
"github.com/superseriousbusiness/gotosocial/internal/db" |
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror" |
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" |
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils" |
||||
) |
||||
|
||||
// Get returns an API model for the markers of the requested timelines.
|
||||
// If a timeline marker hasn't been set yet, it's not included in the response.
|
||||
func (p *Processor) Get(ctx context.Context, account *gtsmodel.Account, names []apimodel.MarkerName) (*apimodel.Marker, gtserror.WithCode) { |
||||
markers := make([]*gtsmodel.Marker, 0, len(names)) |
||||
for _, name := range names { |
||||
marker, err := p.state.DB.GetMarker(ctx, account.ID, typeutils.APIMarkerNameToMarkerName(name)) |
||||
if err != nil { |
||||
if errors.Is(err, db.ErrNoEntries) { |
||||
continue |
||||
} |
||||
// Real database error.
|
||||
return nil, gtserror.NewErrorInternalError(err) |
||||
} |
||||
markers = append(markers, marker) |
||||
} |
||||
|
||||
apiMarker, err := p.tc.MarkersToAPIMarker(ctx, markers) |
||||
if err != nil { |
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting marker to api: %w", err)) |
||||
} |
||||
|
||||
return apiMarker, nil |
||||
} |
||||
@ -0,0 +1,35 @@
|
||||
// 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 ( |
||||
"github.com/superseriousbusiness/gotosocial/internal/state" |
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils" |
||||
) |
||||
|
||||
type Processor struct { |
||||
state *state.State |
||||
tc typeutils.TypeConverter |
||||
} |
||||
|
||||
func New(state *state.State, tc typeutils.TypeConverter) Processor { |
||||
return Processor{ |
||||
state: state, |
||||
tc: tc, |
||||
} |
||||
} |
||||
@ -0,0 +1,48 @@
|
||||
// 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 ( |
||||
"context" |
||||
"errors" |
||||
"fmt" |
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" |
||||
"github.com/superseriousbusiness/gotosocial/internal/db" |
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror" |
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" |
||||
) |
||||
|
||||
// Update updates the given markers and returns an API model for them.
|
||||
func (p *Processor) Update(ctx context.Context, markers []*gtsmodel.Marker) (*apimodel.Marker, gtserror.WithCode) { |
||||
for _, marker := range markers { |
||||
if err := p.state.DB.UpdateMarker(ctx, marker); err != nil { |
||||
if errors.Is(err, db.ErrAlreadyExists) { |
||||
return nil, gtserror.NewErrorConflict(err, "marker updated by another client") |
||||
} |
||||
return nil, gtserror.NewErrorInternalError(err) |
||||
} |
||||
} |
||||
|
||||
apiMarker, err := p.tc.MarkersToAPIMarker(ctx, markers) |
||||
if err != nil { |
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting marker to api: %w", err)) |
||||
} |
||||
|
||||
return apiMarker, nil |
||||
} |
||||
Loading…
Reference in new issue