diff options
author | 2024-03-13 13:53:29 +0100 | |
---|---|---|
committer | 2024-03-13 13:53:29 +0100 | |
commit | ab2d063fcb04f241a3147c843a021491f5fc0a55 (patch) | |
tree | 3d2eff864e8b19d4d9a24f4f1fe92feda8ee4dac /internal/processing/workers | |
parent | [bugfix]: Add missing Link headers in Swagger spec (#2751) (diff) | |
download | gotosocial-ab2d063fcb04f241a3147c843a021491f5fc0a55.tar.xz |
[feature] Process outgoing Move from clientAPI (#2750)
* prevent moved accounts from taking create-type actions
* update move logic
* federate move out
* indicate on web profile when an account has moved
* [docs] Add migration docs section
* lock while checking + setting move state
* use redirectFollowers func for clientAPI as well
* comment typo
* linter? i barely know 'er!
* Update internal/uris/uri.go
Co-authored-by: Daenney <daenney@users.noreply.github.com>
* add a couple tests for move
* fix little mistake exposed by tests (thanks tests)
* ensure Move marked as successful
* attach shared util funcs to struct
* lock whole account when doing move
* move moving check to after error check
* replace repeated text with error func
* linterrrrrr!!!!
* catch self follow case
---------
Co-authored-by: Daenney <daenney@users.noreply.github.com>
Diffstat (limited to 'internal/processing/workers')
-rw-r--r-- | internal/processing/workers/federate.go | 66 | ||||
-rw-r--r-- | internal/processing/workers/fromclientapi.go | 53 | ||||
-rw-r--r-- | internal/processing/workers/fromfediapi.go | 12 | ||||
-rw-r--r-- | internal/processing/workers/fromfediapi_move.go | 95 | ||||
-rw-r--r-- | internal/processing/workers/util.go | 240 | ||||
-rw-r--r-- | internal/processing/workers/wipestatus.go | 135 | ||||
-rw-r--r-- | internal/processing/workers/workers.go | 36 |
7 files changed, 377 insertions, 260 deletions
diff --git a/internal/processing/workers/federate.go b/internal/processing/workers/federate.go index aacb8dcc8..9fdb8f662 100644 --- a/internal/processing/workers/federate.go +++ b/internal/processing/workers/federate.go @@ -23,6 +23,7 @@ import ( "github.com/superseriousbusiness/activity/pub" "github.com/superseriousbusiness/activity/streams" + "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/federation" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" @@ -954,3 +955,68 @@ func (f *federate) Flag(ctx context.Context, report *gtsmodel.Report) error { return nil } + +func (f *federate) MoveAccount(ctx context.Context, account *gtsmodel.Account) error { + // Do nothing if it's not our + // account that's been moved. + if !account.IsLocal() { + return nil + } + + // Parse relevant URI(s). + outboxIRI, err := parseURI(account.OutboxURI) + if err != nil { + return err + } + + // Actor doing the Move. + actorIRI := account.Move.Origin + + // Destination Actor of the Move. + targetIRI := account.Move.Target + + followersIRI, err := parseURI(account.FollowersURI) + if err != nil { + return err + } + + publicIRI, err := parseURI(pub.PublicActivityPubIRI) + if err != nil { + return err + } + + // Create a new move. + move := streams.NewActivityStreamsMove() + + // Set the Move ID. + if err := ap.SetJSONLDIdStr(move, account.Move.URI); err != nil { + return err + } + + // Set the Actor for the Move. + ap.AppendActorIRIs(move, actorIRI) + + // Set the account's IRI as the 'object' property. + ap.AppendObjectIRIs(move, actorIRI) + + // Set the target's IRI as the 'target' property. + ap.AppendTargetIRIs(move, targetIRI) + + // Address the move To followers. + ap.AppendTo(move, followersIRI) + + // Address the move CC public. + ap.AppendCc(move, publicIRI) + + // Send the Move via the Actor's outbox. + if _, err := f.FederatingActor().Send( + ctx, outboxIRI, move, + ); err != nil { + return gtserror.Newf( + "error sending activity %T via outbox %s: %w", + move, outboxIRI, err, + ) + } + + return nil +} diff --git a/internal/processing/workers/fromclientapi.go b/internal/processing/workers/fromclientapi.go index 05b9acc1f..c7e78fee2 100644 --- a/internal/processing/workers/fromclientapi.go +++ b/internal/processing/workers/fromclientapi.go @@ -39,12 +39,12 @@ import ( // specifically for messages originating // from the client/REST API. type clientAPI struct { - state *state.State - converter *typeutils.Converter - surface *surface - federate *federate - wipeStatus wipeStatus - account *account.Processor + state *state.State + converter *typeutils.Converter + surface *surface + federate *federate + account *account.Processor + utilF *utilF } func (p *Processor) EnqueueClientAPI(cctx context.Context, msgs ...messages.FromClientAPI) { @@ -194,6 +194,15 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg messages.From case ap.ObjectProfile: return p.clientAPI.ReportAccount(ctx, cMsg) } + + // MOVE SOMETHING + case ap.ActivityMove: + switch cMsg.APObjectType { //nolint:gocritic + + // MOVE PROFILE/ACCOUNT + case ap.ObjectProfile, ap.ActorPerson: + return p.clientAPI.MoveAccount(ctx, cMsg) + } } return gtserror.Newf("unhandled: %s %s", cMsg.APActivityType, cMsg.APObjectType) @@ -576,7 +585,7 @@ func (p *clientAPI) DeleteStatus(ctx context.Context, cMsg messages.FromClientAP return gtserror.Newf("db error populating status: %w", err) } - if err := p.wipeStatus(ctx, status, deleteAttachments); err != nil { + if err := p.utilF.wipeStatus(ctx, status, deleteAttachments); err != nil { log.Errorf(ctx, "error wiping status: %v", err) } @@ -641,3 +650,33 @@ func (p *clientAPI) ReportAccount(ctx context.Context, cMsg messages.FromClientA return nil } + +func (p *clientAPI) MoveAccount(ctx context.Context, cMsg messages.FromClientAPI) error { + // Redirect each local follower of + // OriginAccount to follow move target. + p.utilF.redirectFollowers(ctx, cMsg.OriginAccount, cMsg.TargetAccount) + + // At this point, we know OriginAccount has the + // Move set on it. Just make sure it's populated. + if err := p.state.DB.PopulateMove(ctx, cMsg.OriginAccount.Move); err != nil { + return gtserror.Newf("error populating Move: %w", err) + } + + // Now send the Move message out to + // OriginAccount's (remote) followers. + if err := p.federate.MoveAccount(ctx, cMsg.OriginAccount); err != nil { + return gtserror.Newf("error federating account move: %w", err) + } + + // Mark the move attempt as successful. + cMsg.OriginAccount.Move.SucceededAt = cMsg.OriginAccount.Move.AttemptedAt + if err := p.state.DB.UpdateMove( + ctx, + cMsg.OriginAccount.Move, + "succeeded_at", + ); err != nil { + return gtserror.Newf("error marking move as successful: %w", err) + } + + return nil +} diff --git a/internal/processing/workers/fromfediapi.go b/internal/processing/workers/fromfediapi.go index 62cb58c83..2fc3b4b26 100644 --- a/internal/processing/workers/fromfediapi.go +++ b/internal/processing/workers/fromfediapi.go @@ -39,11 +39,11 @@ import ( // specifically for messages originating // from the federation/ActivityPub API. type fediAPI struct { - state *state.State - surface *surface - federate *federate - wipeStatus wipeStatus - account *account.Processor + state *state.State + surface *surface + federate *federate + account *account.Processor + utilF *utilF } func (p *Processor) EnqueueFediAPI(cctx context.Context, msgs ...messages.FromFediAPI) { @@ -563,7 +563,7 @@ func (p *fediAPI) DeleteStatus(ctx context.Context, fMsg messages.FromFediAPI) e return gtserror.Newf("%T not parseable as *gtsmodel.Status", fMsg.GTSModel) } - if err := p.wipeStatus(ctx, status, deleteAttachments); err != nil { + if err := p.utilF.wipeStatus(ctx, status, deleteAttachments); err != nil { log.Errorf(ctx, "error wiping status: %v", err) } diff --git a/internal/processing/workers/fromfediapi_move.go b/internal/processing/workers/fromfediapi_move.go index 2223a21f5..0188a5d14 100644 --- a/internal/processing/workers/fromfediapi_move.go +++ b/internal/processing/workers/fromfediapi_move.go @@ -22,7 +22,6 @@ import ( "errors" "time" - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing" "github.com/superseriousbusiness/gotosocial/internal/gtscontext" @@ -380,7 +379,7 @@ func (p *fediAPI) MoveAccount(ctx context.Context, fMsg messages.FromFediAPI) er // Transfer originAcct's followers // on this instance to targetAcct. - redirectOK := p.RedirectAccountFollowers( + redirectOK := p.utilF.redirectFollowers( ctx, originAcct, targetAcct, @@ -422,98 +421,6 @@ func (p *fediAPI) MoveAccount(ctx context.Context, fMsg messages.FromFediAPI) er return nil } -// RedirectAccountFollowers redirects all local -// followers of originAcct to targetAcct. -// -// Both accounts must be fully dereferenced -// already, and the Move must be valid. -// -// Callers to this function MUST have obtained -// a lock already by calling FedLocks.Lock. -// -// Return bool will be true if all goes OK. -func (p *fediAPI) RedirectAccountFollowers( - 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 := p.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 = p.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. - if _, err := p.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 := p.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 -} - // RemoveAccountFollowing removes all // follows owned by the move originAcct. // diff --git a/internal/processing/workers/util.go b/internal/processing/workers/util.go new file mode 100644 index 000000000..a38ecd336 --- /dev/null +++ b/internal/processing/workers/util.go @@ -0,0 +1,240 @@ +// 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 "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtscontext" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" + "github.com/superseriousbusiness/gotosocial/internal/processing/account" + "github.com/superseriousbusiness/gotosocial/internal/processing/media" + "github.com/superseriousbusiness/gotosocial/internal/state" +) + +// utilF wraps util functions used by both +// the fromClientAPI and fromFediAPI functions. +type utilF struct { + state *state.State + media *media.Processor + account *account.Processor + surface *surface +} + +// wipeStatus encapsulates common logic +// used to totally delete a status + all +// its attachments, notifications, boosts, +// and timeline entries. +func (u *utilF) wipeStatus( + ctx context.Context, + statusToDelete *gtsmodel.Status, + deleteAttachments bool, +) error { + var errs gtserror.MultiError + + // Either delete all attachments for this status, + // or simply unattach + clean them separately later. + // + // Reason to unattach rather than delete is that + // the poster might want to reattach them to another + // status immediately (in case of delete + redraft) + if deleteAttachments { + // todo:u.state.DB.DeleteAttachmentsForStatus + for _, id := range statusToDelete.AttachmentIDs { + 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 statusToDelete.AttachmentIDs { + if _, err := u.media.Unattach(ctx, statusToDelete.Account, id); err != nil { + errs.Appendf("error unattaching media: %w", err) + } + } + } + + // delete all mention entries generated by this status + // todo:u.state.DB.DeleteMentionsForStatus + for _, id := range statusToDelete.MentionIDs { + if err := u.state.DB.DeleteMentionByID(ctx, id); err != nil { + errs.Appendf("error deleting status mention: %w", err) + } + } + + // delete all notification entries generated by this status + if err := u.state.DB.DeleteNotificationsForStatus(ctx, statusToDelete.ID); err != nil { + errs.Appendf("error deleting status notifications: %w", err) + } + + // delete all bookmarks that point to this status + if err := u.state.DB.DeleteStatusBookmarksForStatus(ctx, statusToDelete.ID); err != nil { + errs.Appendf("error deleting status bookmarks: %w", err) + } + + // delete all faves of this status + if err := u.state.DB.DeleteStatusFavesForStatus(ctx, statusToDelete.ID); err != nil { + errs.Appendf("error deleting status faves: %w", err) + } + + if pollID := statusToDelete.PollID; pollID != "" { + // Delete this poll by ID from the database. + if err := u.state.DB.DeletePollByID(ctx, pollID); err != nil { + errs.Appendf("error deleting status poll: %w", err) + } + + // Delete any poll votes pointing to this poll ID. + if err := u.state.DB.DeletePollVotes(ctx, pollID); err != nil { + errs.Appendf("error deleting status poll votes: %w", err) + } + + // Cancel any scheduled expiry task for poll. + _ = u.state.Workers.Scheduler.Cancel(pollID) + } + + // delete all boosts for this status + 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), + statusToDelete.ID) + if err != nil { + errs.Appendf("error fetching status boosts: %w", err) + } + + for _, boost := range boosts { + if err := u.surface.deleteStatusFromTimelines(ctx, boost.ID); err != nil { + errs.Appendf("error deleting boost from timelines: %w", err) + } + if err := u.state.DB.DeleteStatusByID(ctx, boost.ID); err != nil { + errs.Appendf("error deleting boost: %w", err) + } + } + + // delete this status from any and all timelines + if err := u.surface.deleteStatusFromTimelines(ctx, statusToDelete.ID); err != nil { + errs.Appendf("error deleting status from timelines: %w", err) + } + + // finally, delete the status itself + if err := u.state.DB.DeleteStatusByID(ctx, statusToDelete.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 *utilF) 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 +} diff --git a/internal/processing/workers/wipestatus.go b/internal/processing/workers/wipestatus.go deleted file mode 100644 index 90a037928..000000000 --- a/internal/processing/workers/wipestatus.go +++ /dev/null @@ -1,135 +0,0 @@ -// 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" - - "github.com/superseriousbusiness/gotosocial/internal/gtscontext" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/processing/media" - "github.com/superseriousbusiness/gotosocial/internal/state" -) - -// wipeStatus encapsulates common logic used to totally delete a status -// + all its attachments, notifications, boosts, and timeline entries. -type wipeStatus func(context.Context, *gtsmodel.Status, bool) error - -// wipeStatusF returns a wipeStatus util function. -func wipeStatusF(state *state.State, media *media.Processor, surface *surface) wipeStatus { - return func( - ctx context.Context, - statusToDelete *gtsmodel.Status, - deleteAttachments bool, - ) error { - var errs gtserror.MultiError - - // Either delete all attachments for this status, - // or simply unattach + clean them separately later. - // - // Reason to unattach rather than delete is that - // the poster might want to reattach them to another - // status immediately (in case of delete + redraft) - if deleteAttachments { - // todo:state.DB.DeleteAttachmentsForStatus - for _, id := range statusToDelete.AttachmentIDs { - if err := media.Delete(ctx, id); err != nil { - errs.Appendf("error deleting media: %w", err) - } - } - } else { - // todo:state.DB.UnattachAttachmentsForStatus - for _, id := range statusToDelete.AttachmentIDs { - if _, err := media.Unattach(ctx, statusToDelete.Account, id); err != nil { - errs.Appendf("error unattaching media: %w", err) - } - } - } - - // delete all mention entries generated by this status - // todo:state.DB.DeleteMentionsForStatus - for _, id := range statusToDelete.MentionIDs { - if err := state.DB.DeleteMentionByID(ctx, id); err != nil { - errs.Appendf("error deleting status mention: %w", err) - } - } - - // delete all notification entries generated by this status - if err := state.DB.DeleteNotificationsForStatus(ctx, statusToDelete.ID); err != nil { - errs.Appendf("error deleting status notifications: %w", err) - } - - // delete all bookmarks that point to this status - if err := state.DB.DeleteStatusBookmarksForStatus(ctx, statusToDelete.ID); err != nil { - errs.Appendf("error deleting status bookmarks: %w", err) - } - - // delete all faves of this status - if err := state.DB.DeleteStatusFavesForStatus(ctx, statusToDelete.ID); err != nil { - errs.Appendf("error deleting status faves: %w", err) - } - - if pollID := statusToDelete.PollID; pollID != "" { - // Delete this poll by ID from the database. - if err := state.DB.DeletePollByID(ctx, pollID); err != nil { - errs.Appendf("error deleting status poll: %w", err) - } - - // Delete any poll votes pointing to this poll ID. - if err := state.DB.DeletePollVotes(ctx, pollID); err != nil { - errs.Appendf("error deleting status poll votes: %w", err) - } - - // Cancel any scheduled expiry task for poll. - _ = state.Workers.Scheduler.Cancel(pollID) - } - - // delete all boosts for this status + remove them from timelines - boosts, err := 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), - statusToDelete.ID) - if err != nil { - errs.Appendf("error fetching status boosts: %w", err) - } - - for _, boost := range boosts { - if err := surface.deleteStatusFromTimelines(ctx, boost.ID); err != nil { - errs.Appendf("error deleting boost from timelines: %w", err) - } - if err := state.DB.DeleteStatusByID(ctx, boost.ID); err != nil { - errs.Appendf("error deleting boost: %w", err) - } - } - - // delete this status from any and all timelines - if err := surface.deleteStatusFromTimelines(ctx, statusToDelete.ID); err != nil { - errs.Appendf("error deleting status from timelines: %w", err) - } - - // finally, delete the status itself - if err := state.DB.DeleteStatusByID(ctx, statusToDelete.ID); err != nil { - errs.Appendf("error deleting status: %w", err) - } - - return errs.Combine() - } -} diff --git a/internal/processing/workers/workers.go b/internal/processing/workers/workers.go index c0612de27..8488e501c 100644 --- a/internal/processing/workers/workers.go +++ b/internal/processing/workers/workers.go @@ -63,30 +63,30 @@ func New( converter: converter, } - // Init shared logic wipe - // status util func. - wipeStatus := wipeStatusF( - state, - media, - surface, - ) + // Init shared util funcs. + utilF := &utilF{ + state: state, + media: media, + account: account, + surface: surface, + } return Processor{ workers: &state.Workers, clientAPI: &clientAPI{ - state: state, - converter: converter, - surface: surface, - federate: federate, - wipeStatus: wipeStatus, - account: account, + state: state, + converter: converter, + surface: surface, + federate: federate, + account: account, + utilF: utilF, }, fediAPI: &fediAPI{ - state: state, - surface: surface, - federate: federate, - wipeStatus: wipeStatus, - account: account, + state: state, + surface: surface, + federate: federate, + account: account, + utilF: utilF, }, } } |