From 7b5917d6ae48f83c92f92d7277960cfa6ae8ec56 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Fri, 2 Aug 2024 13:41:46 +0200 Subject: [PATCH] [feature] Allow import of following and blocks via CSV (#3150) * [feature] Import follows + blocks via settings panel * test import follows --- docs/api/swagger.yaml | 49 +++ docs/assets/user-settings-export-import.png | Bin 0 -> 100113 bytes docs/user_guide/migration.md | 4 +- docs/user_guide/settings.md | 36 +- internal/api/auth/token_test.go | 8 +- internal/api/client.go | 4 + .../api/client/accounts/accountdelete_test.go | 6 +- .../api/client/accounts/accountupdate_test.go | 6 +- internal/api/client/admin/emojicreate_test.go | 8 +- internal/api/client/admin/emojiupdate_test.go | 22 +- internal/api/client/import/import.go | 195 +++++++++ internal/api/client/import/import_test.go | 210 ++++++++++ .../api/client/instance/instancepatch_test.go | 9 +- .../api/client/lists/listaccountsadd_test.go | 2 +- internal/api/client/media/mediacreate_test.go | 8 +- internal/api/client/media/mediaupdate_test.go | 4 +- internal/api/client/polls/polls_vote_test.go | 2 +- internal/api/model/exportimport.go | 22 ++ internal/processing/account/import.go | 374 ++++++++++++++++++ internal/typeutils/csv.go | 135 +++++++ internal/workers/workers.go | 12 + testrig/util.go | 70 +++- .../settings/lib/query/user/export-import.ts | 11 + .../views/user/export-import/import.tsx | 98 +++++ .../views/user/export-import/index.tsx | 2 + 25 files changed, 1247 insertions(+), 50 deletions(-) create mode 100644 docs/assets/user-settings-export-import.png create mode 100644 internal/api/client/import/import.go create mode 100644 internal/api/client/import/import_test.go create mode 100644 internal/processing/account/import.go create mode 100644 web/source/settings/views/user/export-import/import.tsx diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml index 1d5b80ed1..9c21a0a31 100644 --- a/docs/api/swagger.yaml +++ b/docs/api/swagger.yaml @@ -7128,6 +7128,55 @@ paths: summary: Get an array of all hashtags that you currently follow. tags: - tags + /api/v1/import: + post: + consumes: + - multipart/form-data + description: |- + This can be used to migrate data from a Mastodon-compatible CSV file to a GoToSocial account. + + Uploaded data will be processed asynchronously, and not all entries may be processed depending + on domain blocks, user-level blocks, network availability of referenced accounts and statuses, etc. + operationId: importData + parameters: + - description: The CSV data file to upload. + in: formData + name: data + required: true + type: file + - description: |- + Type of entries contained in the data file: + - `following` - accounts to follow. - `blocks` - accounts to block. + in: formData + name: type + required: true + type: string + - default: merge + description: |- + Mode to use when creating entries from the data file: + - `merge` to merge entries in file with existing entries. - `overwrite` to replace existing entries with entries in file. + in: formData + name: mode + type: string + produces: + - application/json + responses: + "202": + description: Upload accepted. + "400": + description: bad request + "401": + description: unauthorized + "406": + description: not acceptable + "500": + description: internal server error + security: + - OAuth2 Bearer: + - write:accounts + summary: Upload some CSV-formatted data to your account. + tags: + - import-export /api/v1/instance: get: operationId: instanceGetV1 diff --git a/docs/assets/user-settings-export-import.png b/docs/assets/user-settings-export-import.png new file mode 100644 index 0000000000000000000000000000000000000000..04b1bb7230e829d8318201d49be2d4ccf72b134a GIT binary patch literal 100113 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
    WcKL(NE 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. + +package importdata + +import ( + "errors" + "fmt" + "net/http" + "slices" + "strings" + + "github.com/gin-gonic/gin" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/oauth" + "github.com/superseriousbusiness/gotosocial/internal/processing" +) + +const ( + BasePath = "/v1/import" +) + +var types = []string{ + "following", + "blocks", +} + +var modes = []string{ + "merge", + "overwrite", +} + +type Module struct { + processor *processing.Processor +} + +func New(processor *processing.Processor) *Module { + return &Module{ + processor: processor, + } +} + +func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { + attachHandler(http.MethodPost, BasePath, m.ImportPOSTHandler) +} + +// ImportPOSTHandler swagger:operation POST /api/v1/import importData +// +// Upload some CSV-formatted data to your account. +// +// This can be used to migrate data from a Mastodon-compatible CSV file to a GoToSocial account. +// +// Uploaded data will be processed asynchronously, and not all entries may be processed depending +// on domain blocks, user-level blocks, network availability of referenced accounts and statuses, etc. +// +// --- +// tags: +// - import-export +// +// consumes: +// - multipart/form-data +// +// produces: +// - application/json +// +// parameters: +// - +// name: data +// in: formData +// description: The CSV data file to upload. +// type: file +// required: true +// - +// name: type +// in: formData +// description: >- +// Type of entries contained in the data file: +// +// - `following` - accounts to follow. +// - `blocks` - accounts to block. +// type: string +// required: true +// - +// name: mode +// in: formData +// description: >- +// Mode to use when creating entries from the data file: +// +// - `merge` to merge entries in file with existing entries. +// - `overwrite` to replace existing entries with entries in file. +// type: string +// default: merge +// +// security: +// - OAuth2 Bearer: +// - write:accounts +// +// responses: +// '202': +// description: Upload accepted. +// '400': +// description: bad request +// '401': +// description: unauthorized +// '406': +// description: not acceptable +// '500': +// description: internal server error +func (m *Module) ImportPOSTHandler(c *gin.Context) { + authed, err := oauth.Authed(c, true, true, true, true) + if err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1) + return + } + + if authed.Account.IsMoving() { + apiutil.ForbiddenAfterMove(c) + return + } + + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1) + return + } + + form := &apimodel.ImportRequest{} + if err := c.ShouldBind(form); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) + return + } + + if form.Data == nil { + const text = "no data file provided" + err := errors.New(text) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, text), m.processor.InstanceGetV1) + return + } + + if form.Type == "" { + const text = "no type provided" + err := errors.New(text) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, text), m.processor.InstanceGetV1) + return + } + + form.Type = strings.ToLower(form.Type) + if !slices.Contains(types, form.Type) { + text := fmt.Sprintf("type %s not recognized, valid types are: %+v", form.Type, types) + err := errors.New(text) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, text), m.processor.InstanceGetV1) + return + } + + if form.Mode != "" { + form.Mode = strings.ToLower(form.Mode) + if !slices.Contains(modes, form.Mode) { + text := fmt.Sprintf("mode %s not recognized, valid modes are: %+v", form.Mode, modes) + err := errors.New(text) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, text), m.processor.InstanceGetV1) + return + } + } + overwrite := form.Mode == "overwrite" + + // Trigger the import. + errWithCode := m.processor.Account().ImportData( + c.Request.Context(), + authed.Account, + form.Data, + form.Type, + overwrite, + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + apiutil.JSON(c, http.StatusAccepted, gin.H{"status": "accepted"}) +} diff --git a/internal/api/client/import/import_test.go b/internal/api/client/import/import_test.go new file mode 100644 index 000000000..5129f862e --- /dev/null +++ b/internal/api/client/import/import_test.go @@ -0,0 +1,210 @@ +// 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 . + +package importdata_test + +import ( + "bytes" + "context" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/suite" + importdata "github.com/superseriousbusiness/gotosocial/internal/api/client/import" + "github.com/superseriousbusiness/gotosocial/internal/filter/visibility" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/oauth" + "github.com/superseriousbusiness/gotosocial/internal/state" + "github.com/superseriousbusiness/gotosocial/internal/typeutils" + "github.com/superseriousbusiness/gotosocial/testrig" +) + +type ImportTestSuite struct { + // Suite interfaces + suite.Suite + state state.State + + // standard suite models + testTokens map[string]*gtsmodel.Token + testClients map[string]*gtsmodel.Client + testApplications map[string]*gtsmodel.Application + testUsers map[string]*gtsmodel.User + testAccounts map[string]*gtsmodel.Account + + // module being tested + importModule *importdata.Module +} + +func (suite *ImportTestSuite) SetupSuite() { + suite.testTokens = testrig.NewTestTokens() + suite.testClients = testrig.NewTestClients() + suite.testApplications = testrig.NewTestApplications() + suite.testUsers = testrig.NewTestUsers() + suite.testAccounts = testrig.NewTestAccounts() +} + +func (suite *ImportTestSuite) SetupTest() { + suite.state.Caches.Init() + + testrig.InitTestConfig() + testrig.InitTestLog() + + suite.state.DB = testrig.NewTestDB(&suite.state) + suite.state.Storage = testrig.NewInMemoryStorage() + + testrig.StartTimelines( + &suite.state, + visibility.NewFilter(&suite.state), + typeutils.NewConverter(&suite.state), + ) + + testrig.StandardDBSetup(suite.state.DB, nil) + testrig.StandardStorageSetup(suite.state.Storage, "../../../../testrig/media") + + mediaManager := testrig.NewTestMediaManager(&suite.state) + + federator := testrig.NewTestFederator( + &suite.state, + testrig.NewTestTransportController( + &suite.state, + testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), + ), + mediaManager, + ) + + processor := testrig.NewTestProcessor( + &suite.state, + federator, + testrig.NewEmailSender("../../../../web/template/", nil), + mediaManager, + ) + testrig.StartWorkers(&suite.state, processor.Workers()) + + suite.importModule = importdata.New(processor) +} + +func (suite *ImportTestSuite) TriggerHandler( + importData string, + importType string, + importMode string, +) { + // Set up request. + recorder := httptest.NewRecorder() + ctx, _ := testrig.CreateGinTestContext(recorder, nil) + + // Authorize the request ctx as though it + // had passed through API auth handlers. + ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) + ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"])) + ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) + ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) + + // Create test request. + b, w, err := testrig.CreateMultipartFormData( + testrig.StringToDataF("data", "data.csv", importData), + map[string][]string{ + "type": {importType}, + "mode": {importMode}, + }, + ) + if err != nil { + suite.FailNow(err.Error()) + } + + target := "http://localhost:8080/api/v1/import" + ctx.Request = httptest.NewRequest(http.MethodPost, target, bytes.NewReader(b.Bytes())) + ctx.Request.Header.Set("Accept", "application/json") + ctx.Request.Header.Set("Content-Type", w.FormDataContentType()) + + // Trigger handler. + suite.importModule.ImportPOSTHandler(ctx) + + if code := recorder.Code; code != http.StatusAccepted { + b, err := io.ReadAll(recorder.Body) + if err != nil { + panic(err) + } + suite.FailNow("", "expected 202, got %d: %s", code, string(b)) + } +} + +func (suite *ImportTestSuite) TearDownTest() { + testrig.StandardDBTeardown(suite.state.DB) + testrig.StandardStorageTeardown(suite.state.Storage) + testrig.StopWorkers(&suite.state) +} + +func (suite *ImportTestSuite) TestImportFollows() { + var ( + ctx = context.Background() + testAccount = suite.testAccounts["local_account_1"] + ) + + // Clear existing follows from Zork. + if err := suite.state.DB.DeleteAccountFollows(ctx, testAccount.ID); err != nil { + suite.FailNow(err.Error()) + } + + // Have zork refollow turtle and admin. + data := `Account address,Show boosts +admin@localhost:8080,true +1happyturtle@localhost:8080,true +` + + // Trigger the import handler. + suite.TriggerHandler(data, "following", "merge") + + // Wait for zork to be + // following admin. + if !testrig.WaitFor(func() bool { + f, err := suite.state.DB.IsFollowing( + ctx, + testAccount.ID, + suite.testAccounts["admin_account"].ID, + ) + if err != nil { + suite.FailNow(err.Error()) + } + + return f + }) { + suite.FailNow("timed out waiting for zork to follow admin") + } + + // Wait for zork to be + // follow req'ing turtle. + if !testrig.WaitFor(func() bool { + f, err := suite.state.DB.IsFollowRequested( + ctx, + testAccount.ID, + suite.testAccounts["local_account_2"].ID, + ) + if err != nil { + suite.FailNow(err.Error()) + } + + return f + }) { + suite.FailNow("timed out waiting for zork to follow req turtle") + } +} + +func TestImportTestSuite(t *testing.T) { + suite.Run(t, new(ImportTestSuite)) +} diff --git a/internal/api/client/instance/instancepatch_test.go b/internal/api/client/instance/instancepatch_test.go index f12638f82..bb391537e 100644 --- a/internal/api/client/instance/instancepatch_test.go +++ b/internal/api/client/instance/instancepatch_test.go @@ -37,7 +37,12 @@ type InstancePatchTestSuite struct { } func (suite *InstancePatchTestSuite) instancePatch(fieldName string, fileName string, extraFields map[string][]string) (code int, body []byte) { - requestBody, w, err := testrig.CreateMultipartFormData(fieldName, fileName, extraFields) + var dataF testrig.DataF + if fieldName != "" && fileName != "" { + dataF = testrig.FileToDataF(fieldName, fileName) + } + + requestBody, w, err := testrig.CreateMultipartFormData(dataF, extraFields) if err != nil { suite.FailNow(err.Error()) } @@ -499,7 +504,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch4() { func (suite *InstancePatchTestSuite) TestInstancePatch5() { requestBody, w, err := testrig.CreateMultipartFormData( - "", "", + nil, map[string][]string{ "short_description": {"

    This is some html, which is allowed in short descriptions.

    "}, }) diff --git a/internal/api/client/lists/listaccountsadd_test.go b/internal/api/client/lists/listaccountsadd_test.go index 492996882..7e44eeed3 100644 --- a/internal/api/client/lists/listaccountsadd_test.go +++ b/internal/api/client/lists/listaccountsadd_test.go @@ -60,7 +60,7 @@ func (suite *ListAccountsAddTestSuite) postListAccounts( requestPath := config.GetProtocol() + "://" + config.GetHost() + "/api/" + lists.BasePath + "/" + listID + "/accounts" // Prepare test body. - buf, w, err := testrig.CreateMultipartFormData("", "", map[string][]string{ + buf, w, err := testrig.CreateMultipartFormData(nil, map[string][]string{ "account_ids[]": accountIDs, }) diff --git a/internal/api/client/media/mediacreate_test.go b/internal/api/client/media/mediacreate_test.go index c256d18dc..4c2725681 100644 --- a/internal/api/client/media/mediacreate_test.go +++ b/internal/api/client/media/mediacreate_test.go @@ -149,7 +149,7 @@ func (suite *MediaCreateTestSuite) TestMediaCreateSuccessful() { } // create the request - buf, w, err := testrig.CreateMultipartFormData("file", "../../../../testrig/media/test-jpeg.jpg", map[string][]string{ + buf, w, err := testrig.CreateMultipartFormData(testrig.FileToDataF("file", "../../../../testrig/media/test-jpeg.jpg"), map[string][]string{ "description": {"this is a test image -- a cool background from somewhere"}, "focus": {"-0.5,0.5"}, }) @@ -234,7 +234,7 @@ func (suite *MediaCreateTestSuite) TestMediaCreateSuccessfulV2() { } // create the request - buf, w, err := testrig.CreateMultipartFormData("file", "../../../../testrig/media/test-jpeg.jpg", map[string][]string{ + buf, w, err := testrig.CreateMultipartFormData(testrig.FileToDataF("file", "../../../../testrig/media/test-jpeg.jpg"), map[string][]string{ "description": {"this is a test image -- a cool background from somewhere"}, "focus": {"-0.5,0.5"}, }) @@ -317,7 +317,7 @@ func (suite *MediaCreateTestSuite) TestMediaCreateLongDescription() { description := base64.RawStdEncoding.EncodeToString(descriptionBytes) // create the request - buf, w, err := testrig.CreateMultipartFormData("file", "../../../../testrig/media/test-jpeg.jpg", map[string][]string{ + buf, w, err := testrig.CreateMultipartFormData(testrig.FileToDataF("file", "../../../../testrig/media/test-jpeg.jpg"), map[string][]string{ "description": {description}, "focus": {"-0.5,0.5"}, }) @@ -358,7 +358,7 @@ func (suite *MediaCreateTestSuite) TestMediaCreateTooShortDescription() { ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) // create the request - buf, w, err := testrig.CreateMultipartFormData("file", "../../../../testrig/media/test-jpeg.jpg", map[string][]string{ + buf, w, err := testrig.CreateMultipartFormData(testrig.FileToDataF("file", "../../../../testrig/media/test-jpeg.jpg"), map[string][]string{ "description": {""}, // provide an empty description "focus": {"-0.5,0.5"}, }) diff --git a/internal/api/client/media/mediaupdate_test.go b/internal/api/client/media/mediaupdate_test.go index 43b2b6c51..c3a1fb340 100644 --- a/internal/api/client/media/mediaupdate_test.go +++ b/internal/api/client/media/mediaupdate_test.go @@ -140,7 +140,7 @@ func (suite *MediaUpdateTestSuite) TestUpdateImage() { ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) // create the request - buf, w, err := testrig.CreateMultipartFormData("", "", map[string][]string{ + buf, w, err := testrig.CreateMultipartFormData(nil, map[string][]string{ "id": {toUpdate.ID}, "description": {"new description!"}, "focus": {"-0.1,0.3"}, @@ -201,7 +201,7 @@ func (suite *MediaUpdateTestSuite) TestUpdateImageShortDescription() { ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) // create the request - buf, w, err := testrig.CreateMultipartFormData("", "", map[string][]string{ + buf, w, err := testrig.CreateMultipartFormData(nil, map[string][]string{ "id": {toUpdate.ID}, "description": {"new description!"}, "focus": {"-0.1,0.3"}, diff --git a/internal/api/client/polls/polls_vote_test.go b/internal/api/client/polls/polls_vote_test.go index 01bd941d3..54f98c192 100644 --- a/internal/api/client/polls/polls_vote_test.go +++ b/internal/api/client/polls/polls_vote_test.go @@ -107,7 +107,7 @@ func (suite *PollCreateTestSuite) formVoteInPoll( choicesStrs = append(choicesStrs, strconv.Itoa(choice)) } - body, w, err := testrig.CreateMultipartFormData("", "", map[string][]string{ + body, w, err := testrig.CreateMultipartFormData(nil, map[string][]string{ "choices[]": choicesStrs, }) diff --git a/internal/api/model/exportimport.go b/internal/api/model/exportimport.go index d87ed8cd3..88ea5489d 100644 --- a/internal/api/model/exportimport.go +++ b/internal/api/model/exportimport.go @@ -17,6 +17,8 @@ package model +import "mime/multipart" + // AccountExportStats models an account's stats // specifically for the purpose of informing about // export sizes at the /api/v1/exports/stats endpoint. @@ -58,3 +60,23 @@ type AccountExportStats struct { // example: 11 MutesCount int `json:"mutes_count"` } + +// AttachmentRequest models media attachment creation parameters. +// +// swagger: ignore +type ImportRequest struct { + // The CSV data to upload. + Data *multipart.FileHeader `form:"data" binding:"required"` + // Type of entries contained in the data file. + // + // - `following` - accounts to follow. + // - `lists` - lists of accounts. + // - `blocks` - accounts to block. + // - `mutes` - accounts to mute. + // - `bookmarks` - statuses to bookmark. + Type string `form:"type" binding:"required"` + // Mode to use when creating entries from the data file: + // - `merge` to merge entries in file with existing entries. + // - `overwrite` to replace existing entries with entries in file. + Mode string `form:"mode"` +} diff --git a/internal/processing/account/import.go b/internal/processing/account/import.go new file mode 100644 index 000000000..200d971b8 --- /dev/null +++ b/internal/processing/account/import.go @@ -0,0 +1,374 @@ +// 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 . + +package account + +import ( + "context" + "encoding/csv" + "errors" + "fmt" + "mime/multipart" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" +) + +func (p *Processor) ImportData( + ctx context.Context, + requester *gtsmodel.Account, + data *multipart.FileHeader, + importType string, + overwrite bool, +) gtserror.WithCode { + switch importType { + + case "following": + return p.importFollowing( + ctx, + requester, + data, + overwrite, + ) + + case "blocks": + return p.importBlocks( + ctx, + requester, + data, + overwrite, + ) + + default: + const text = "import type not yet supported" + return gtserror.NewErrorUnprocessableEntity(errors.New(text), text) + } +} + +func (p *Processor) importFollowing( + ctx context.Context, + requester *gtsmodel.Account, + followingData *multipart.FileHeader, + overwrite bool, +) gtserror.WithCode { + file, err := followingData.Open() + if err != nil { + err := fmt.Errorf("error opening following data file: %w", err) + return gtserror.NewErrorBadRequest(err, err.Error()) + } + defer file.Close() + + // Parse records out of the file. + records, err := csv.NewReader(file).ReadAll() + if err != nil { + err := fmt.Errorf("error reading following data file: %w", err) + return gtserror.NewErrorBadRequest(err, err.Error()) + } + + // Convert the records into a slice of barebones follows. + // + // Only TargetAccount.Username, TargetAccount.Domain, + // and ShowReblogs will be set on each Follow. + follows, err := p.converter.CSVToFollowing(ctx, records) + if err != nil { + err := fmt.Errorf("error converting records to follows: %w", err) + return gtserror.NewErrorBadRequest(err, err.Error()) + } + + // Do remaining processing of this import asynchronously. + f := importFollowingAsyncF(p, requester, follows, overwrite) + p.state.Workers.Processing.Queue.Push(f) + + return nil +} + +func importFollowingAsyncF( + p *Processor, + requester *gtsmodel.Account, + follows []*gtsmodel.Follow, + overwrite bool, +) func(context.Context) { + return func(ctx context.Context) { + // Map used to store wanted + // follow targets (if overwriting). + var wantedFollows map[string]struct{} + + if overwrite { + // If we're overwriting, we need to get current + // follow(-req)s owned by requester *before* + // making any changes, so that we can remove + // unwanted follows after we've created new ones. + prevFollows, err := p.state.DB.GetAccountFollows(ctx, requester.ID, nil) + if err != nil { + log.Errorf(ctx, "db error getting following: %v", err) + return + } + + prevFollowReqs, err := p.state.DB.GetAccountFollowRequesting(ctx, requester.ID, nil) + if err != nil { + log.Errorf(ctx, "db error getting follow requesting: %v", err) + return + } + + // Initialize new follows map. + wantedFollows = make(map[string]struct{}, len(follows)) + + // Once we've created (or tried to create) + // the required follows, go through previous + // follow(-request)s and remove unwanted ones. + defer func() { + + // AccountIDs to unfollow. + toRemove := []string{} + + // Check previous follows. + for _, prev := range prevFollows { + username := prev.TargetAccount.Username + domain := prev.TargetAccount.Domain + + _, wanted := wantedFollows[username+"@"+domain] + if !wanted { + toRemove = append(toRemove, prev.TargetAccountID) + } + } + + // Now any pending follow requests. + for _, prev := range prevFollowReqs { + username := prev.TargetAccount.Username + domain := prev.TargetAccount.Domain + + _, wanted := wantedFollows[username+"@"+domain] + if !wanted { + toRemove = append(toRemove, prev.TargetAccountID) + } + } + + // Remove each discovered + // unwanted follow. + for _, accountID := range toRemove { + if _, errWithCode := p.FollowRemove( + ctx, + requester, + accountID, + ); errWithCode != nil { + log.Errorf(ctx, "could not unfollow account: %v", errWithCode.Unwrap()) + continue + } + } + }() + } + + // Go through the follows parsed from CSV + // file, and create / update each one. + for _, follow := range follows { + var ( + // Username of the target. + username = follow.TargetAccount.Username + + // Domain of the target. + // Empty for our domain. + domain = follow.TargetAccount.Domain + + // Show reblogs on + // the new follow. + showReblogs = follow.ShowReblogs + ) + + if overwrite { + // We'll be overwriting, so store + // this new follow in our handy map. + wantedFollows[username+"@"+domain] = struct{}{} + } + + // Get the target account, dereferencing it if necessary. + targetAcct, _, err := p.federator.Dereferencer.GetAccountByUsernameDomain( + ctx, + requester.Username, + username, + domain, + ) + if err != nil { + log.Errorf(ctx, "could not retrieve account: %v", err) + continue + } + + // Use the processor's FollowCreate function + // to create or update the follow. This takes + // account of existing follows, and also sends + // the follow to the FromClientAPI processor. + if _, errWithCode := p.FollowCreate( + ctx, + requester, + &apimodel.AccountFollowRequest{ + ID: targetAcct.ID, + Reblogs: showReblogs, + }, + ); errWithCode != nil { + log.Errorf(ctx, "could not follow account: %v", errWithCode.Unwrap()) + continue + } + } + } +} + +func (p *Processor) importBlocks( + ctx context.Context, + requester *gtsmodel.Account, + blocksData *multipart.FileHeader, + overwrite bool, +) gtserror.WithCode { + file, err := blocksData.Open() + if err != nil { + err := fmt.Errorf("error opening blocks data file: %w", err) + return gtserror.NewErrorBadRequest(err, err.Error()) + } + defer file.Close() + + // Parse records out of the file. + records, err := csv.NewReader(file).ReadAll() + if err != nil { + err := fmt.Errorf("error reading blocks data file: %w", err) + return gtserror.NewErrorBadRequest(err, err.Error()) + } + + // Convert the records into a slice of barebones blocks. + // + // Only TargetAccount.Username and TargetAccount.Domain, + // will be set on each Block. + blocks, err := p.converter.CSVToBlocks(ctx, records) + if err != nil { + err := fmt.Errorf("error converting records to blocks: %w", err) + return gtserror.NewErrorBadRequest(err, err.Error()) + } + + // Do remaining processing of this import asynchronously. + f := importBlocksAsyncF(p, requester, blocks, overwrite) + p.state.Workers.Processing.Queue.Push(f) + + return nil +} + +func importBlocksAsyncF( + p *Processor, + requester *gtsmodel.Account, + blocks []*gtsmodel.Block, + overwrite bool, +) func(context.Context) { + return func(ctx context.Context) { + // Map used to store wanted + // block targets (if overwriting). + var wantedBlocks map[string]struct{} + + if overwrite { + // If we're overwriting, we need to get current + // blocks owned by requester *before* making any + // changes, so that we can remove unwanted blocks + // after we've created new ones. + var ( + prevBlocks []*gtsmodel.Block + err error + ) + + prevBlocks, err = p.state.DB.GetAccountBlocks(ctx, requester.ID, nil) + if err != nil { + log.Errorf(ctx, "db error getting blocks: %v", err) + return + } + + // Initialize new blocks map. + wantedBlocks = make(map[string]struct{}, len(blocks)) + + // Once we've created (or tried to create) + // the required blocks, go through previous + // blocks and remove unwanted ones. + defer func() { + for _, prev := range prevBlocks { + username := prev.TargetAccount.Username + domain := prev.TargetAccount.Domain + + _, wanted := wantedBlocks[username+"@"+domain] + if wanted { + // Leave this + // one alone. + continue + } + + if _, errWithCode := p.BlockRemove( + ctx, + requester, + prev.TargetAccountID, + ); errWithCode != nil { + log.Errorf(ctx, "could not unblock account: %v", errWithCode.Unwrap()) + continue + } + } + }() + } + + // Go through the blocks parsed from CSV + // file, and create / update each one. + for _, block := range blocks { + var ( + // Username of the target. + username = block.TargetAccount.Username + + // Domain of the target. + // Empty for our domain. + domain = block.TargetAccount.Domain + ) + + if overwrite { + // We'll be overwriting, so store + // this new block in our handy map. + wantedBlocks[username+"@"+domain] = struct{}{} + } + + // Get the target account, dereferencing it if necessary. + targetAcct, _, err := p.federator.Dereferencer.GetAccountByUsernameDomain( + ctx, + // Provide empty request user to use the + // instance account to deref the account. + // + // It's pointless to make lots of calls + // to a remote from an account that's about + // to block that account. + "", + username, + domain, + ) + if err != nil { + log.Errorf(ctx, "could not retrieve account: %v", err) + continue + } + + // Use the processor's BlockCreate function + // to create or update the block. This takes + // account of existing blocks, and also sends + // the block to the FromClientAPI processor. + if _, errWithCode := p.BlockCreate( + ctx, + requester, + targetAcct.ID, + ); errWithCode != nil { + log.Errorf(ctx, "could not block account: %v", errWithCode.Unwrap()) + continue + } + } + } +} diff --git a/internal/typeutils/csv.go b/internal/typeutils/csv.go index 2ef56cb0c..063e31d54 100644 --- a/internal/typeutils/csv.go +++ b/internal/typeutils/csv.go @@ -26,6 +26,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtscontext" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/util" ) func (c *Converter) AccountToExportStats( @@ -383,3 +384,137 @@ func (c *Converter) MutesToCSV( return records, nil } + +// CSVToFollowing converts a slice of CSV records +// to a slice of barebones *gtsmodel.Follow's, +// ready for further processing. +// +// Only TargetAccount.Username, TargetAccount.Domain, +// and ShowReblogs will be set on each Follow. +func (c *Converter) CSVToFollowing( + ctx context.Context, + records [][]string, +) ([]*gtsmodel.Follow, error) { + // We need to know our own domain for this. + // Try account domain, fall back to host. + var ( + thisHost = config.GetHost() + thisAccountDomain = config.GetAccountDomain() + follows = make([]*gtsmodel.Follow, 0, len(records)) + ) + + for _, record := range records { + if len(record) != 2 { + // Badly formatted, + // skip this one. + continue + } + + namestring := record[0] + if namestring == "" { + // Badly formatted, + // skip this one. + continue + } + + // Prepend with "@" + // if not included. + if namestring[0] != '@' { + namestring = "@" + namestring + } + + username, domain, err := util.ExtractNamestringParts(namestring) + if err != nil { + // Badly formatted, + // skip this one. + continue + } + + if domain == thisHost || domain == thisAccountDomain { + // Clear the domain, + // since it's ours. + domain = "" + } + + showReblogs, err := strconv.ParseBool(record[1]) + if err != nil { + // Badly formatted, + // skip this one. + continue + } + + // Looks good, whack it in the slice. + follows = append(follows, >smodel.Follow{ + TargetAccount: >smodel.Account{ + Username: username, + Domain: domain, + }, + ShowReblogs: &showReblogs, + }) + } + + return follows, nil +} + +// CSVToBlocks converts a slice of CSV records +// to a slice of barebones *gtsmodel.Block's, +// ready for further processing. +// +// Only TargetAccount.Username and TargetAccount.Domain +// will be set on each Block. +func (c *Converter) CSVToBlocks( + ctx context.Context, + records [][]string, +) ([]*gtsmodel.Block, error) { + // We need to know our own domain for this. + // Try account domain, fall back to host. + var ( + thisHost = config.GetHost() + thisAccountDomain = config.GetAccountDomain() + blocks = make([]*gtsmodel.Block, 0, len(records)) + ) + + for _, record := range records { + if len(record) != 1 { + // Badly formatted, + // skip this one. + continue + } + + namestring := record[0] + if namestring == "" { + // Badly formatted, + // skip this one. + continue + } + + // Prepend with "@" + // if not included. + if namestring[0] != '@' { + namestring = "@" + namestring + } + + username, domain, err := util.ExtractNamestringParts(namestring) + if err != nil { + // Badly formatted, + // skip this one. + continue + } + + if domain == thisHost || domain == thisAccountDomain { + // Clear the domain, + // since it's ours. + domain = "" + } + + // Looks good, whack it in the slice. + blocks = append(blocks, >smodel.Block{ + TargetAccount: >smodel.Account{ + Username: username, + Domain: domain, + }, + }) + } + + return blocks, nil +} diff --git a/internal/workers/workers.go b/internal/workers/workers.go index 377a9d899..657522903 100644 --- a/internal/workers/workers.go +++ b/internal/workers/workers.go @@ -49,6 +49,11 @@ type Workers struct { // for asynchronous dereferencer jobs. Dereference FnWorkerPool + // Processing provides a worker pool + // for asynchronous processing jobs, + // eg., import tasks, admin tasks. + Processing FnWorkerPool + // prevent pass-by-value. _ nocopy } @@ -81,6 +86,10 @@ func (w *Workers) Start() { n = 4 * maxprocs w.Dereference.Start(n) log.Infof(nil, "started %d dereference workers", n) + + n = 4 * maxprocs + w.Processing.Start(n) + log.Infof(nil, "started %d processing workers", n) } // Stop will stop all of the contained @@ -101,6 +110,9 @@ func (w *Workers) Stop() { w.Dereference.Stop() log.Info(nil, "stopped dereference workers") + + w.Processing.Stop() + log.Info(nil, "stopped processing workers") } // nocopy when embedded will signal linter to diff --git a/testrig/util.go b/testrig/util.go index 31312f0af..957553d79 100644 --- a/testrig/util.go +++ b/testrig/util.go @@ -25,6 +25,7 @@ import ( "mime/multipart" "net/url" "os" + "path" "time" "codeberg.org/gruf/go-byteutil" @@ -82,6 +83,7 @@ func StartWorkers(state *state.State, processor *workers.Processor) { state.Workers.Client.Start(1) state.Workers.Federator.Start(1) state.Workers.Dereference.Start(1) + state.Workers.Processing.Start(1) } func StopWorkers(state *state.State) { @@ -89,6 +91,7 @@ func StopWorkers(state *state.State) { state.Workers.Client.Stop() state.Workers.Federator.Stop() state.Workers.Dereference.Stop() + state.Workers.Processing.Stop() } func StartTimelines(state *state.State, visFilter *visibility.Filter, converter *typeutils.Converter) { @@ -171,8 +174,22 @@ func EqualRequestURIs(u1, u2 any) bool { return uri1 == uri2 } -// CreateMultipartFormData is a handy function for taking a fieldname and a filename, and creating a multipart form bytes buffer -// with the file contents set in the given fieldname. The extraFields param can be used to add extra FormFields to the request, as necessary. +type DataF func() ( + fieldName string, + fileName string, + rc io.ReadCloser, + err error, +) + +// CreateMultipartFormData is a handy function for creating a multipart form bytes buffer with data. +// +// If data function is not nil, it should return the fieldName for the data in the form (eg., "data"), +// the fileName (eg., "data.csv"), a readcloser for getting the data, or an error if something goes wrong. +// +// The extraFields param can be used to add extra FormFields to the request, as necessary. +// +// Data function can be nil if only FormFields and string values are required. +// // The returned bytes.Buffer b can be used like so: // // httptest.NewRequest(http.MethodPost, "https://example.org/whateverpath", bytes.NewReader(b.Bytes())) @@ -180,21 +197,28 @@ func EqualRequestURIs(u1, u2 any) bool { // The returned *multipart.Writer w can be used to set the content type of the request, like so: // // req.Header.Set("Content-Type", w.FormDataContentType()) -func CreateMultipartFormData(fieldName string, fileName string, extraFields map[string][]string) (bytes.Buffer, *multipart.Writer, error) { - var b bytes.Buffer +func CreateMultipartFormData( + dataF DataF, + extraFields map[string][]string, +) (bytes.Buffer, *multipart.Writer, error) { + var ( + b bytes.Buffer + w = multipart.NewWriter(&b) + ) - w := multipart.NewWriter(&b) - var fw io.Writer - - if fileName != "" { - file, err := os.Open(fileName) + if dataF != nil { + fieldName, fileName, rc, err := dataF() if err != nil { return b, nil, err } - if fw, err = w.CreateFormFile(fieldName, file.Name()); err != nil { + defer rc.Close() + + fw, err := w.CreateFormFile(fieldName, fileName) + if err != nil { return b, nil, err } - if _, err = io.Copy(fw, file); err != nil { + + if _, err = io.Copy(fw, rc); err != nil { return b, nil, err } } @@ -210,9 +234,33 @@ func CreateMultipartFormData(fieldName string, fileName string, extraFields map[ if err := w.Close(); err != nil { return b, nil, err } + return b, w, nil } +// FileToDataF is a convenience function for opening a +// file at the given filePath, and packaging it into a +// DataF for use in CreateMultipartFormData. +func FileToDataF(fieldName string, filePath string) DataF { + return func() (string, string, io.ReadCloser, error) { + file, err := os.Open(filePath) + if err != nil { + return "", "", nil, err + } + + return fieldName, path.Base(filePath), file, nil + } +} + +// StringToDataF is a convenience function for wrapping the +// given data into a DataF for use in CreateMultipartFormData. +func StringToDataF(fieldName string, fileName string, data string) DataF { + return func() (string, string, io.ReadCloser, error) { + rc := io.NopCloser(bytes.NewBufferString(data)) + return fieldName, fileName, rc, nil + } +} + // URLMustParse tries to parse the given URL and panics if it can't. // Should only be used in tests. func URLMustParse(stringURL string) *url.URL { diff --git a/web/source/settings/lib/query/user/export-import.ts b/web/source/settings/lib/query/user/export-import.ts index 56c48e364..006203a68 100644 --- a/web/source/settings/lib/query/user/export-import.ts +++ b/web/source/settings/lib/query/user/export-import.ts @@ -125,6 +125,16 @@ const extended = gtsApi.injectEndpoints({ return { data: null }; } }), + + importData: build.mutation({ + query: (formData) => ({ + method: "POST", + url: `/api/v1/import`, + asForm: true, + body: formData, + discardEmpty: true + }), + }), }) }); @@ -135,4 +145,5 @@ export const { useExportListsMutation, useExportBlocksMutation, useExportMutesMutation, + useImportDataMutation, } = extended; diff --git a/web/source/settings/views/user/export-import/import.tsx b/web/source/settings/views/user/export-import/import.tsx new file mode 100644 index 000000000..8cb7b22ba --- /dev/null +++ b/web/source/settings/views/user/export-import/import.tsx @@ -0,0 +1,98 @@ +/* + 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 . +*/ + +import React from "react"; +import { useImportDataMutation } from "../../../lib/query/user/export-import"; +import MutationButton from "../../../components/form/mutation-button"; +import useFormSubmit from "../../../lib/form/submit"; +import { useFileInput, useTextInput } from "../../../lib/form"; +import { FileInput, Select } from "../../../components/form/inputs"; + +export default function Import() { + const form = { + data: useFileInput("data"), + type: useTextInput("type", { defaultValue: "" }), + mode: useTextInput("mode", { defaultValue: "" }) + }; + + const [submitForm, result] = useFormSubmit(form, useImportDataMutation(), { + changedOnly: false, + onFinish: () => { + form.data.reset(); + form.type.reset(); + form.mode.reset(); + } + }); + + return ( +
    + + + + + + + + + + + ); +} diff --git a/web/source/settings/views/user/export-import/index.tsx b/web/source/settings/views/user/export-import/index.tsx index 2e3533318..b73779bac 100644 --- a/web/source/settings/views/user/export-import/index.tsx +++ b/web/source/settings/views/user/export-import/index.tsx @@ -22,6 +22,7 @@ import Export from "./export"; import Loading from "../../../components/loading"; import { Error } from "../../../components/error"; import { useExportStatsQuery } from "../../../lib/query/user/export-import"; +import Import from "./import"; export default function ExportImport() { const { @@ -52,6 +53,7 @@ export default function ExportImport() { your GoToSocial account. All exports and imports use Mastodon-compatible CSV files.

    + ); }