summaryrefslogtreecommitdiff
path: root/internal/processing/list/updateentries.go
blob: 6dcb951a7e98ab00529aa77eebfa88d8244326ed (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
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, &gtsmodel.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
}