// 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 . 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 }