summaryrefslogtreecommitdiff
path: root/internal/processing/workers/util.go
blob: 0aa0febf08f68147ff03cc4cf043863f3afe9eda (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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
// 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 workers

import (
	"context"
	"errors"

	apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
	"code.superseriousbusiness.org/gotosocial/internal/db"
	"code.superseriousbusiness.org/gotosocial/internal/gtscontext"
	"code.superseriousbusiness.org/gotosocial/internal/gtserror"
	"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
	"code.superseriousbusiness.org/gotosocial/internal/log"
	"code.superseriousbusiness.org/gotosocial/internal/processing/account"
	"code.superseriousbusiness.org/gotosocial/internal/processing/media"
	"code.superseriousbusiness.org/gotosocial/internal/state"
	"code.superseriousbusiness.org/gotosocial/internal/typeutils"
)

// util provides util functions used by both
// the fromClientAPI and fromFediAPI functions.
type utils struct {
	state     *state.State
	media     *media.Processor
	account   *account.Processor
	surface   *Surface
	converter *typeutils.Converter
}

// wipeStatus encapsulates common logic used to
// totally delete a status + all its attachments,
// notifications, boosts, and timeline entries.
//
// If deleteAttachments is true, then any status
// attachments will also be deleted, else they
// will just be detached.
//
// If copyToSinBin is true, then a version of the
// status will be put in the `sin_bin_statuses`
// table prior to deletion.
func (u *utils) wipeStatus(
	ctx context.Context,
	status *gtsmodel.Status,
	deleteAttachments bool,
	copyToSinBin bool,
) error {
	var errs gtserror.MultiError

	if copyToSinBin {
		// Copy this status to the sin bin before we delete it.
		sbStatus, err := u.converter.StatusToSinBinStatus(ctx, status)
		if err != nil {
			errs.Appendf("error converting status to sinBinStatus: %w", err)
		} else {
			if err := u.state.DB.PutSinBinStatus(ctx, sbStatus); err != nil {
				errs.Appendf("db error storing sinBinStatus: %w", err)
			}
		}
	}

	// Before handling media, ensure
	// historic edits are populated.
	if !status.EditsPopulated() {
		var err error

		// Fetch all historical edits of status from database.
		status.Edits, err = u.state.DB.GetStatusEditsByIDs(
			gtscontext.SetBarebones(ctx),
			status.EditIDs,
		)
		if err != nil {
			errs.Appendf("error getting status edits from database: %w", err)
		}
	}

	// Either delete all attachments for this status,
	// or simply detach + clean them separately later.
	//
	// Reason to detach rather than delete is that
	// the author might want to reattach them to another
	// status immediately (in case of delete + redraft).
	if deleteAttachments {
		// todo:u.state.DB.DeleteAttachmentsForStatus
		for _, id := range status.AllAttachmentIDs() {
			if err := u.media.Delete(ctx, id); err != nil {
				errs.Appendf("error deleting media: %w", err)
			}
		}
	} else {
		// todo:u.state.DB.UnattachAttachmentsForStatus
		for _, id := range status.AllAttachmentIDs() {
			if _, err := u.media.Unattach(ctx, status.Account, id); err != nil {
				errs.Appendf("error unattaching media: %w", err)
			}
		}
	}

	// Delete all historical edits of status.
	if ids := status.EditIDs; len(ids) > 0 {
		if err := u.state.DB.DeleteStatusEdits(ctx, ids); err != nil {
			errs.Appendf("error deleting status edits: %w", err)
		}
	}

	// Delete all mentions generated by this status.
	// todo:u.state.DB.DeleteMentionsForStatus
	for _, id := range status.MentionIDs {
		if err := u.state.DB.DeleteMentionByID(ctx, id); err != nil {
			errs.Appendf("error deleting status mention: %w", err)
		}
	}

	// Delete all notifications generated by this status.
	if err := u.state.DB.DeleteNotificationsForStatus(ctx, status.ID); err != nil {
		errs.Appendf("error deleting status notifications: %w", err)
	}

	// Delete all bookmarks of this status.
	if err := u.state.DB.DeleteStatusBookmarksForStatus(ctx, status.ID); err != nil {
		errs.Appendf("error deleting status bookmarks: %w", err)
	}

	// Delete all faves of this status.
	if err := u.state.DB.DeleteStatusFavesForStatus(ctx, status.ID); err != nil {
		errs.Appendf("error deleting status faves: %w", err)
	}

	if id := status.PollID; id != "" {
		// Delete this poll by ID from the database.
		if err := u.state.DB.DeletePollByID(ctx, id); err != nil {
			errs.Appendf("error deleting status poll: %w", err)
		}

		// Cancel any scheduled expiry task for poll.
		_ = u.state.Workers.Scheduler.Cancel(id)
	}

	// Get all boost of this status so that we can
	// delete those boosts + remove them from timelines.
	boosts, err := u.state.DB.GetStatusBoosts(

		// We MUST set a barebones context here,
		// as depending on where it came from the
		// original BoostOf may already be gone.
		gtscontext.SetBarebones(ctx),
		status.ID)
	if err != nil {
		errs.Appendf("error fetching status boosts: %w", err)
	}

	for _, boost := range boosts {
		// Delete the boost itself.
		if err := u.state.DB.DeleteStatusByID(ctx, boost.ID); err != nil {
			errs.Appendf("error deleting boost: %w", err)
		}

		// Remove the boost from any and all timelines.
		u.surface.deleteStatusFromTimelines(ctx, boost.ID)
	}

	// Delete the status itself from any and all timelines.
	u.surface.deleteStatusFromTimelines(ctx, status.ID)

	// Delete this status from any conversations it's part of.
	if err := u.state.DB.DeleteStatusFromConversations(ctx, status.ID); err != nil {
		errs.Appendf("error deleting status from conversations: %w", err)
	}

	// Finally delete the status itself.
	if err := u.state.DB.DeleteStatusByID(ctx, status.ID); err != nil {
		errs.Appendf("error deleting status: %w", err)
	}

	return errs.Combine()
}

// redirectFollowers redirects all local
// followers of originAcct to targetAcct.
//
// Both accounts must be fully dereferenced
// already, and the Move must be valid.
//
// Return bool will be true if all goes OK.
func (u *utils) redirectFollowers(
	ctx context.Context,
	originAcct *gtsmodel.Account,
	targetAcct *gtsmodel.Account,
) bool {
	// Any local followers of originAcct should
	// send follow requests to targetAcct instead,
	// and have followers of originAcct removed.
	//
	// Select local followers with barebones, since
	// we only need follow.Account and we can get
	// that ourselves.
	followers, err := u.state.DB.GetAccountLocalFollowers(
		gtscontext.SetBarebones(ctx),
		originAcct.ID,
	)
	if err != nil && !errors.Is(err, db.ErrNoEntries) {
		log.Errorf(ctx,
			"db error getting follows targeting originAcct: %v",
			err,
		)
		return false
	}

	for _, follow := range followers {
		// Fetch the local account that
		// owns the follow targeting originAcct.
		if follow.Account, err = u.state.DB.GetAccountByID(
			gtscontext.SetBarebones(ctx),
			follow.AccountID,
		); err != nil {
			log.Errorf(ctx,
				"db error getting follow account %s: %v",
				follow.AccountID, err,
			)
			return false
		}

		// Use the account processor FollowCreate
		// function to send off the new follow,
		// carrying over the Reblogs and Notify
		// values from the old follow to the new.
		//
		// This will also handle cases where our
		// account has already followed the target
		// account, by just updating the existing
		// follow of target account.
		//
		// Also, ensure new follow wouldn't be a
		// self follow, since that will error.
		if follow.AccountID != targetAcct.ID {
			if _, err := u.account.FollowCreate(
				ctx,
				follow.Account,
				&apimodel.AccountFollowRequest{
					ID:      targetAcct.ID,
					Reblogs: follow.ShowReblogs,
					Notify:  follow.Notify,
				},
			); err != nil {
				log.Errorf(ctx,
					"error creating new follow for account %s: %v",
					follow.AccountID, err,
				)
				return false
			}
		}

		// New follow is in the process of
		// sending, remove the existing follow.
		// This will send out an Undo Activity for each Follow.
		if _, err := u.account.FollowRemove(
			ctx,
			follow.Account,
			follow.TargetAccountID,
		); err != nil {
			log.Errorf(ctx,
				"error removing old follow for account %s: %v",
				follow.AccountID, err,
			)
			return false
		}
	}

	return true
}

// impoliteFaveRequest stores an interaction request
// for the given fave, and notifies the interactee.
//
// It should be used only when an actor has sent a Like
// directly in response to a post that requires approval
// for it, instead of sending a LikeRequest.
func (u *utils) impoliteFaveRequest(
	ctx context.Context,
	fave *gtsmodel.StatusFave,
) error {
	// Only create interaction request
	// if fave targets a local status.
	if fave.Status == nil ||
		!fave.Status.IsLocal() {
		return nil
	}

	// Lock on the interaction URI.
	unlock := u.state.ProcessingLocks.Lock(fave.URI)
	defer unlock()

	// Ensure no req with this URI exists already.
	req, err := u.state.DB.GetInteractionRequestByInteractionURI(ctx, fave.URI)
	if err != nil && !errors.Is(err, db.ErrNoEntries) {
		return gtserror.Newf("db error checking for existing interaction request: %w", err)
	}

	if req != nil {
		// Interaction req already exists,
		// no need to do anything else.
		return nil
	}

	// Create + store new impolite interaction request.
	req = typeutils.StatusFaveToImpoliteInteractionRequest(fave)
	if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil {
		return gtserror.Newf("db error storing interaction request: %w", err)
	}

	// Notify *local* account of pending fave.
	if err := u.surface.notifyPendingFave(ctx, fave); err != nil {
		return gtserror.Newf("error notifying pending fave: %w", err)
	}

	return nil
}

// impoliteReplyRequest stores an interaction request
// for the given reply, and notifies the interactee.
//
// It should be used only when an actor has sent a reply
// directly in response to a post that requires approval
// for it, instead of sending a ReplyRequest.
func (u *utils) impoliteReplyRequest(
	ctx context.Context,
	reply *gtsmodel.Status,
) error {
	// Only create interaction request if
	// status replies to a local status.
	if reply.InReplyTo == nil ||
		!reply.InReplyTo.IsLocal() {
		return nil
	}

	// Lock on the interaction URI.
	unlock := u.state.ProcessingLocks.Lock(reply.URI)
	defer unlock()

	// Ensure no req with this URI exists already.
	req, err := u.state.DB.GetInteractionRequestByInteractionURI(ctx, reply.URI)
	if err != nil && !errors.Is(err, db.ErrNoEntries) {
		return gtserror.Newf("db error checking for existing interaction request: %w", err)
	}

	if req != nil {
		// Interaction req already exists,
		// no need to do anything else.
		return nil
	}

	// Create + store impolite interaction request.
	req = typeutils.StatusToImpoliteInteractionRequest(reply)
	if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil {
		return gtserror.Newf("db error storing interaction request: %w", err)
	}

	// Notify *local* account of pending reply.
	if err := u.surface.notifyPendingReply(ctx, reply); err != nil {
		return gtserror.Newf("error notifying pending reply: %w", err)
	}

	return nil
}

// impoliteAnnounceRequest stores an interaction request
// for the given announce, and notifies the interactee.
//
// It should be used only when an actor has sent an Announce
// directly in response to a post that requires approval
// for it, instead of sending an AnnounceRequest.
func (u *utils) impoliteAnnounceRequest(
	ctx context.Context,
	boost *gtsmodel.Status,
) error {
	// Only create interaction request if
	// status announces a local status.
	if boost.BoostOf == nil ||
		!boost.BoostOf.IsLocal() {
		return nil
	}

	// Lock on the interaction URI.
	unlock := u.state.ProcessingLocks.Lock(boost.URI)
	defer unlock()

	// Ensure no req with this URI exists already.
	req, err := u.state.DB.GetInteractionRequestByInteractionURI(ctx, boost.URI)
	if err != nil && !errors.Is(err, db.ErrNoEntries) {
		return gtserror.Newf("db error checking for existing interaction request: %w", err)
	}

	if req != nil {
		// Interaction req already exists,
		// no need to do anything else.
		return nil
	}

	// Create + store impolite interaction request.
	req = typeutils.StatusToImpoliteInteractionRequest(boost)
	if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil {
		return gtserror.Newf("db error storing interaction request: %w", err)
	}

	// Notify *local* account of pending announce.
	if err := u.surface.notifyPendingAnnounce(ctx, boost); err != nil {
		return gtserror.Newf("error notifying pending announce: %w", err)
	}

	return nil
}