updateentries.go (5362B)
1 // GoToSocial 2 // Copyright (C) GoToSocial Authors admin@gotosocial.org 3 // SPDX-License-Identifier: AGPL-3.0-or-later 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package list 19 20 import ( 21 "context" 22 "errors" 23 "fmt" 24 25 "github.com/superseriousbusiness/gotosocial/internal/db" 26 "github.com/superseriousbusiness/gotosocial/internal/gtserror" 27 "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" 28 "github.com/superseriousbusiness/gotosocial/internal/id" 29 ) 30 31 // AddToList adds targetAccountIDs to the given list, if valid. 32 func (p *Processor) AddToList(ctx context.Context, account *gtsmodel.Account, listID string, targetAccountIDs []string) gtserror.WithCode { 33 // Ensure this list exists + account owns it. 34 list, errWithCode := p.getList(ctx, account.ID, listID) 35 if errWithCode != nil { 36 return errWithCode 37 } 38 39 // Pre-assemble list of entries to add. We *could* add these 40 // one by one as we iterate through accountIDs, but according 41 // to the Mastodon API we should only add them all once we know 42 // they're all valid, no partial updates. 43 listEntries := make([]*gtsmodel.ListEntry, 0, len(targetAccountIDs)) 44 45 // Check each targetAccountID is valid. 46 // - Follow must exist. 47 // - Follow must not already be in the given list. 48 for _, targetAccountID := range targetAccountIDs { 49 // Ensure follow exists. 50 follow, err := p.state.DB.GetFollow(ctx, account.ID, targetAccountID) 51 if err != nil { 52 if errors.Is(err, db.ErrNoEntries) { 53 err = fmt.Errorf("you do not follow account %s", targetAccountID) 54 return gtserror.NewErrorNotFound(err, err.Error()) 55 } 56 return gtserror.NewErrorInternalError(err) 57 } 58 59 // Ensure followID not already in list. 60 // This particular call to isInList will 61 // never error, so just check entryID. 62 entryID, _ := isInList( 63 list, 64 follow.ID, 65 func(listEntry *gtsmodel.ListEntry) (string, error) { 66 // Looking for the listEntry follow ID. 67 return listEntry.FollowID, nil 68 }, 69 ) 70 71 // Empty entryID means entry with given 72 // followID wasn't found in the list. 73 if entryID != "" { 74 err = fmt.Errorf("account with id %s is already in list %s with entryID %s", targetAccountID, listID, entryID) 75 return gtserror.NewErrorUnprocessableEntity(err, err.Error()) 76 } 77 78 // Entry wasn't in the list, we can add it. 79 listEntries = append(listEntries, >smodel.ListEntry{ 80 ID: id.NewULID(), 81 ListID: listID, 82 FollowID: follow.ID, 83 }) 84 } 85 86 // If we get to here we can assume all 87 // entries are valid, so try to add them. 88 if err := p.state.DB.PutListEntries(ctx, listEntries); err != nil { 89 if errors.Is(err, db.ErrAlreadyExists) { 90 err = fmt.Errorf("one or more errors inserting list entries: %w", err) 91 return gtserror.NewErrorUnprocessableEntity(err, err.Error()) 92 } 93 return gtserror.NewErrorInternalError(err) 94 } 95 96 return nil 97 } 98 99 // RemoveFromList removes targetAccountIDs from the given list, if valid. 100 func (p *Processor) RemoveFromList(ctx context.Context, account *gtsmodel.Account, listID string, targetAccountIDs []string) gtserror.WithCode { 101 // Ensure this list exists + account owns it. 102 list, errWithCode := p.getList(ctx, account.ID, listID) 103 if errWithCode != nil { 104 return errWithCode 105 } 106 107 // For each targetAccountID, we want to check if 108 // a follow with that targetAccountID is in the 109 // given list. If it is in there, we want to remove 110 // it from the list. 111 for _, targetAccountID := range targetAccountIDs { 112 // Check if targetAccountID is 113 // on a follow in the list. 114 entryID, err := isInList( 115 list, 116 targetAccountID, 117 func(listEntry *gtsmodel.ListEntry) (string, error) { 118 // We need the follow so populate this 119 // entry, if it's not already populated. 120 if err := p.state.DB.PopulateListEntry(ctx, listEntry); err != nil { 121 return "", err 122 } 123 124 // Looking for the list entry targetAccountID. 125 return listEntry.Follow.TargetAccountID, nil 126 }, 127 ) 128 129 // Error may be returned here if there was an issue 130 // populating the list entry. We only return on proper 131 // DB errors, we can just skip no entry errors. 132 if err != nil && !errors.Is(err, db.ErrNoEntries) { 133 err = fmt.Errorf("error checking if targetAccountID %s was in list %s: %w", targetAccountID, listID, err) 134 return gtserror.NewErrorInternalError(err) 135 } 136 137 if entryID == "" { 138 // There was an errNoEntries or targetAccount 139 // wasn't in this list anyway, so we can skip it. 140 continue 141 } 142 143 // TargetAccount was in the list, remove the entry. 144 if err := p.state.DB.DeleteListEntry(ctx, entryID); err != nil && !errors.Is(err, db.ErrNoEntries) { 145 err = fmt.Errorf("error removing list entry %s from list %s: %w", entryID, listID, err) 146 return gtserror.NewErrorInternalError(err) 147 } 148 } 149 150 return nil 151 }