gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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, &gtsmodel.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 }