diff options
Diffstat (limited to 'internal/processing/list')
-rw-r--r-- | internal/processing/list/create.go | 50 | ||||
-rw-r--r-- | internal/processing/list/delete.go | 46 | ||||
-rw-r--r-- | internal/processing/list/get.go | 155 | ||||
-rw-r--r-- | internal/processing/list/list.go | 35 | ||||
-rw-r--r-- | internal/processing/list/update.go | 73 | ||||
-rw-r--r-- | internal/processing/list/updateentries.go | 151 | ||||
-rw-r--r-- | internal/processing/list/util.go | 85 |
7 files changed, 595 insertions, 0 deletions
diff --git a/internal/processing/list/create.go b/internal/processing/list/create.go new file mode 100644 index 000000000..10dec1050 --- /dev/null +++ b/internal/processing/list/create.go @@ -0,0 +1,50 @@ +// 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 list + +import ( + "context" + "errors" + + 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/id" +) + +// Create creates one a new list for the given account, using the provided parameters. +// These params should have already been validated by the time they reach this function. +func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, title string, repliesPolicy gtsmodel.RepliesPolicy) (*apimodel.List, gtserror.WithCode) { + list := >smodel.List{ + ID: id.NewULID(), + Title: title, + AccountID: account.ID, + RepliesPolicy: repliesPolicy, + } + + if err := p.state.DB.PutList(ctx, list); err != nil { + if errors.Is(err, db.ErrAlreadyExists) { + err = errors.New("you already have a list with this title") + return nil, gtserror.NewErrorConflict(err, err.Error()) + } + return nil, gtserror.NewErrorInternalError(err) + } + + return p.apiList(ctx, list) +} diff --git a/internal/processing/list/delete.go b/internal/processing/list/delete.go new file mode 100644 index 000000000..1c8ee5700 --- /dev/null +++ b/internal/processing/list/delete.go @@ -0,0 +1,46 @@ +// 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 list + +import ( + "context" + + "github.com/superseriousbusiness/gotosocial/internal/gtscontext" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +// Delete deletes one list for the given account. +func (p *Processor) Delete(ctx context.Context, account *gtsmodel.Account, id string) gtserror.WithCode { + list, errWithCode := p.getList( + // Use barebones ctx; no embedded + // structs necessary for this call. + gtscontext.SetBarebones(ctx), + account.ID, + id, + ) + if errWithCode != nil { + return errWithCode + } + + if err := p.state.DB.DeleteListByID(ctx, list.ID); err != nil { + return gtserror.NewErrorInternalError(err) + } + + return nil +} diff --git a/internal/processing/list/get.go b/internal/processing/list/get.go new file mode 100644 index 000000000..3f124fe7c --- /dev/null +++ b/internal/processing/list/get.go @@ -0,0 +1,155 @@ +// 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 list + +import ( + "context" + "errors" + "fmt" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtscontext" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" + "github.com/superseriousbusiness/gotosocial/internal/util" +) + +// Get returns the api model of one list with the given ID. +func (p *Processor) Get(ctx context.Context, account *gtsmodel.Account, id string) (*apimodel.List, gtserror.WithCode) { + list, errWithCode := p.getList( + // Use barebones ctx; no embedded + // structs necessary for this call. + gtscontext.SetBarebones(ctx), + account.ID, + id, + ) + if errWithCode != nil { + return nil, errWithCode + } + + return p.apiList(ctx, list) +} + +// GetMultiple returns multiple lists created by the given account, sorted by list ID DESC (newest first). +func (p *Processor) GetAll(ctx context.Context, account *gtsmodel.Account) ([]*apimodel.List, gtserror.WithCode) { + lists, err := p.state.DB.GetListsForAccountID( + // Use barebones ctx; no embedded + // structs necessary for simple GET. + gtscontext.SetBarebones(ctx), + account.ID, + ) + if err != nil { + if errors.Is(err, db.ErrNoEntries) { + return nil, nil + } + return nil, gtserror.NewErrorInternalError(err) + } + + apiLists := make([]*apimodel.List, 0, len(lists)) + for _, list := range lists { + apiList, errWithCode := p.apiList(ctx, list) + if errWithCode != nil { + return nil, errWithCode + } + + apiLists = append(apiLists, apiList) + } + + return apiLists, nil +} + +// GetListAccounts returns accounts that are in the given list, owned by the given account. +// The additional parameters can be used for paging. +func (p *Processor) GetListAccounts( + ctx context.Context, + account *gtsmodel.Account, + listID string, + maxID string, + sinceID string, + minID string, + limit int, +) (*apimodel.PageableResponse, gtserror.WithCode) { + // Ensure list exists + is owned by requesting account. + if _, errWithCode := p.getList(ctx, account.ID, listID); errWithCode != nil { + return nil, errWithCode + } + + // To know which accounts are in the list, + // we need to first get requested list entries. + listEntries, err := p.state.DB.GetListEntries(ctx, listID, maxID, sinceID, minID, limit) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err = fmt.Errorf("GetListAccounts: error getting list entries: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + + count := len(listEntries) + if count == 0 { + // No list entries means no accounts. + return util.EmptyPageableResponse(), nil + } + + var ( + items = make([]interface{}, count) + nextMaxIDValue string + prevMinIDValue string + ) + + // For each list entry, we want the account it points to. + // To get this, we need to first get the follow that the + // list entry pertains to, then extract the target account + // from that follow. + // + // We do paging not by account ID, but by list entry ID. + for i, listEntry := range listEntries { + if i == count-1 { + nextMaxIDValue = listEntry.ID + } + + if i == 0 { + prevMinIDValue = listEntry.ID + } + + if err := p.state.DB.PopulateListEntry(ctx, listEntry); err != nil { + log.Debugf(ctx, "skipping list entry because of error populating it: %q", err) + continue + } + + if err := p.state.DB.PopulateFollow(ctx, listEntry.Follow); err != nil { + log.Debugf(ctx, "skipping list entry because of error populating follow: %q", err) + continue + } + + apiAccount, err := p.tc.AccountToAPIAccountPublic(ctx, listEntry.Follow.TargetAccount) + if err != nil { + log.Debugf(ctx, "skipping list entry because of error converting follow target account: %q", err) + continue + } + + items[i] = apiAccount + } + + return util.PackagePageableResponse(util.PageableResponseParams{ + Items: items, + Path: "api/v1/lists/" + listID + "/accounts", + NextMaxIDValue: nextMaxIDValue, + PrevMinIDValue: prevMinIDValue, + Limit: limit, + }) +} diff --git a/internal/processing/list/list.go b/internal/processing/list/list.go new file mode 100644 index 000000000..f192beb60 --- /dev/null +++ b/internal/processing/list/list.go @@ -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 list + +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, + } +} diff --git a/internal/processing/list/update.go b/internal/processing/list/update.go new file mode 100644 index 000000000..656af1f78 --- /dev/null +++ b/internal/processing/list/update.go @@ -0,0 +1,73 @@ +// 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 list + +import ( + "context" + "errors" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtscontext" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +// Update updates one list for the given account, using the provided parameters. +// These params should have already been validated by the time they reach this function. +func (p *Processor) Update( + ctx context.Context, + account *gtsmodel.Account, + id string, + title *string, + repliesPolicy *gtsmodel.RepliesPolicy, +) (*apimodel.List, gtserror.WithCode) { + list, errWithCode := p.getList( + // Use barebones ctx; no embedded + // structs necessary for this call. + gtscontext.SetBarebones(ctx), + account.ID, + id, + ) + if errWithCode != nil { + return nil, errWithCode + } + + // Only update columns we're told to update. + columns := make([]string, 0, 2) + + if title != nil { + list.Title = *title + columns = append(columns, "title") + } + + if repliesPolicy != nil { + list.RepliesPolicy = *repliesPolicy + columns = append(columns, "replies_policy") + } + + if err := p.state.DB.UpdateList(ctx, list, columns...); err != nil { + if errors.Is(err, db.ErrAlreadyExists) { + err = errors.New("you already have a list with this title") + return nil, gtserror.NewErrorConflict(err, err.Error()) + } + return nil, gtserror.NewErrorInternalError(err) + } + + return p.apiList(ctx, list) +} diff --git a/internal/processing/list/updateentries.go b/internal/processing/list/updateentries.go new file mode 100644 index 000000000..6dcb951a7 --- /dev/null +++ b/internal/processing/list/updateentries.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 list + +import ( + "context" + "errors" + "fmt" + + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" +) + +// AddToList adds targetAccountIDs to the given list, if valid. +func (p *Processor) AddToList(ctx context.Context, account *gtsmodel.Account, listID string, targetAccountIDs []string) gtserror.WithCode { + // Ensure this list exists + account owns it. + list, errWithCode := p.getList(ctx, account.ID, listID) + if errWithCode != nil { + return errWithCode + } + + // Pre-assemble list of entries to add. We *could* add these + // one by one as we iterate through accountIDs, but according + // to the Mastodon API we should only add them all once we know + // they're all valid, no partial updates. + listEntries := make([]*gtsmodel.ListEntry, 0, len(targetAccountIDs)) + + // Check each targetAccountID is valid. + // - Follow must exist. + // - Follow must not already be in the given list. + for _, targetAccountID := range targetAccountIDs { + // Ensure follow exists. + follow, err := p.state.DB.GetFollow(ctx, account.ID, targetAccountID) + if err != nil { + if errors.Is(err, db.ErrNoEntries) { + err = fmt.Errorf("you do not follow account %s", targetAccountID) + return gtserror.NewErrorNotFound(err, err.Error()) + } + return gtserror.NewErrorInternalError(err) + } + + // Ensure followID not already in list. + // This particular call to isInList will + // never error, so just check entryID. + entryID, _ := isInList( + list, + follow.ID, + func(listEntry *gtsmodel.ListEntry) (string, error) { + // Looking for the listEntry follow ID. + return listEntry.FollowID, nil + }, + ) + + // Empty entryID means entry with given + // followID wasn't found in the list. + if entryID != "" { + err = fmt.Errorf("account with id %s is already in list %s with entryID %s", targetAccountID, listID, entryID) + return gtserror.NewErrorUnprocessableEntity(err, err.Error()) + } + + // Entry wasn't in the list, we can add it. + listEntries = append(listEntries, >smodel.ListEntry{ + ID: id.NewULID(), + ListID: listID, + FollowID: follow.ID, + }) + } + + // If we get to here we can assume all + // entries are valid, so try to add them. + if err := p.state.DB.PutListEntries(ctx, listEntries); err != nil { + if errors.Is(err, db.ErrAlreadyExists) { + err = fmt.Errorf("one or more errors inserting list entries: %w", err) + return gtserror.NewErrorUnprocessableEntity(err, err.Error()) + } + return gtserror.NewErrorInternalError(err) + } + + return nil +} + +// RemoveFromList removes targetAccountIDs from the given list, if valid. +func (p *Processor) RemoveFromList(ctx context.Context, account *gtsmodel.Account, listID string, targetAccountIDs []string) gtserror.WithCode { + // Ensure this list exists + account owns it. + list, errWithCode := p.getList(ctx, account.ID, listID) + if errWithCode != nil { + return errWithCode + } + + // For each targetAccountID, we want to check if + // a follow with that targetAccountID is in the + // given list. If it is in there, we want to remove + // it from the list. + for _, targetAccountID := range targetAccountIDs { + // Check if targetAccountID is + // on a follow in the list. + entryID, err := isInList( + list, + targetAccountID, + func(listEntry *gtsmodel.ListEntry) (string, error) { + // We need the follow so populate this + // entry, if it's not already populated. + if err := p.state.DB.PopulateListEntry(ctx, listEntry); err != nil { + return "", err + } + + // Looking for the list entry targetAccountID. + return listEntry.Follow.TargetAccountID, nil + }, + ) + + // Error may be returned here if there was an issue + // populating the list entry. We only return on proper + // DB errors, we can just skip no entry errors. + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err = fmt.Errorf("error checking if targetAccountID %s was in list %s: %w", targetAccountID, listID, err) + return gtserror.NewErrorInternalError(err) + } + + if entryID == "" { + // There was an errNoEntries or targetAccount + // wasn't in this list anyway, so we can skip it. + continue + } + + // TargetAccount was in the list, remove the entry. + if err := p.state.DB.DeleteListEntry(ctx, entryID); err != nil && !errors.Is(err, db.ErrNoEntries) { + err = fmt.Errorf("error removing list entry %s from list %s: %w", entryID, listID, err) + return gtserror.NewErrorInternalError(err) + } + } + + return nil +} diff --git a/internal/processing/list/util.go b/internal/processing/list/util.go new file mode 100644 index 000000000..6186f58c7 --- /dev/null +++ b/internal/processing/list/util.go @@ -0,0 +1,85 @@ +// 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 list + +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" +) + +// getList is a shortcut to get one list from the database and +// check that it's owned by the given accountID. Will return +// appropriate errors so caller doesn't need to bother. +func (p *Processor) getList(ctx context.Context, accountID string, listID string) (*gtsmodel.List, gtserror.WithCode) { + list, err := p.state.DB.GetListByID(ctx, listID) + if err != nil { + if errors.Is(err, db.ErrNoEntries) { + // List doesn't seem to exist. + return nil, gtserror.NewErrorNotFound(err) + } + // Real database error. + return nil, gtserror.NewErrorInternalError(err) + } + + if list.AccountID != accountID { + err = fmt.Errorf("list with id %s does not belong to account %s", list.ID, accountID) + return nil, gtserror.NewErrorNotFound(err) + } + + return list, nil +} + +// apiList is a shortcut to return the API version of the given +// list, or return an appropriate error if conversion fails. +func (p *Processor) apiList(ctx context.Context, list *gtsmodel.List) (*apimodel.List, gtserror.WithCode) { + apiList, err := p.tc.ListToAPIList(ctx, list) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting list to api: %w", err)) + } + + return apiList, nil +} + +// isInList check if thisID is equal to the result of thatID +// for any entry in the given list. +// +// Will return the id of the listEntry if true, empty if false, +// or an error if the result of thatID returns an error. +func isInList( + list *gtsmodel.List, + thisID string, + getThatID func(listEntry *gtsmodel.ListEntry) (string, error), +) (string, error) { + for _, listEntry := range list.ListEntries { + thatID, err := getThatID(listEntry) + if err != nil { + return "", err + } + + if thisID == thatID { + return listEntry.ID, nil + } + } + return "", nil +} |