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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%0Dk1dkYahPiiXTjpYILK1_k9QnYsz7 zSSpp^Kyd2jYsxoBEXNcBRg@C`lFO-v>`J?3MoZ)(q()F0WC6w+EH)GQwW@tj&9#4!}=#25Qi9()o5gk`v zkL^v3Ir5d?jjQ6bXiDvaWFCJ_U&bQx1##Skr@1)a?~7Iuijr~OKALs6U9CF zuUS2+ciRFqA_aDQbH-ltdtCd$FUP3Sn>Q0)B>G?vwj9R1elXLzKWLz>+2e@k3X6G_ zK50sd`bP!3vqt9yDr^quIhmrP=7hx8vgv$^(G|xJgZU%nijqYl49xyi>S}#tCe>Wh z#Dg@?qg269$@9^>iEpe6CzuI73{T@Fr5qm67VPKqfovG`4rK9FSjeG#BjrzP`5o6{ zQyy4zARLw$tanjts7^bePi!;Xn9CMtKqNX3YLIn(A_a*E`E- zO;{pQz3UnuZP<0zExGL_gNsiKt_?0Pz9H^NgM~G*;+dzg9!-MTJ)80lBM$M9h8^|S zQ`v4tw0Iw5+4ofX?ArC)glbP{pn}ONw;?YwVgi~sXp&?ufbt7SebaU@`zUdPw+dYN zN@g@JGR_AQ(HZ>g_dh+Nh1(d3kQm?3V2B z1#>Nl^~7*d67$X2E66O7_)pcSwuOofmpjXwsuC54KlBnqjE(+B${WLu@-)812Slk7 zg9cUnoTh}@01`dTD>rU^S4dHzJymujK7|yjpufyV3HIvU-6V+EHuN=uH|9+%s-~md zgGUi=xZR@Bo{JgSuf}WpF(Vy0;4A1rq&DnkFNo;!T+`_?5+2^^fT(TQ8Qru0uvZ$= z&7{k#Wg}e%eGd?;4FaZBdM*wU&zSfchb&MQh4RZ1+OtPU?h+8fp_Ta%0dO+C`JXy3 zZxol#T=|xXAAW3E(M&)WLWQoc8;h|abpmJ!%*dK%%xuw6R0J#gK9~_G=ify1YI%}L z50D?gsS(XBwFZu^GD``=MW4|NliNZ{=N&1ZS1x@0>O`wSPg#oKkfAHKcUu_@E?Ze4 zDx2P;^AtiB;SG0bB#w@a&CASOkM8HDLxn2#pUq@=AUy;?OSkX!f48*u->$v=Gr`LJ Y6I5fx!w9CFR%G51BC^6oU-iHL8y;vf6#xJL literal 0 HcmV?d00001 diff --git a/docs/user_guide/migration.md b/docs/user_guide/migration.md index 8d37a0869..a2d2a4c36 100644 --- a/docs/user_guide/migration.md +++ b/docs/user_guide/migration.md @@ -74,5 +74,5 @@ Once you have triggered the move from your other account to your GoToSocial acco !!! tip To save yourself some trouble, consider setting your GoToSocial account to not require approval for new follow requests, just before triggering the migration. Once the migration is complete, turn approval of follow requests back on. Otherwise, you will have to manually approve every migrated follower from your old account. -!!! warning - While the move will indicate to your followers that they should follow you at your GoToSocial account, GoToSocial does not yet support importing a list of accounts you follow. Until we implement this, you will have to manually follow accounts again from your GoToSocial account. Please see [this issue](https://github.com/superseriousbusiness/gotosocial/issues/1048) for more details. +!!! tip + After moving your account, you may wish to import your list of followed accounts from your previous account into your GoToSocial account. [See here](./settings.md#import) for details on how to do this via the settings panel. diff --git a/docs/user_guide/settings.md b/docs/user_guide/settings.md index 0811381c0..ed069c19c 100644 --- a/docs/user_guide/settings.md +++ b/docs/user_guide/settings.md @@ -207,8 +207,40 @@ Please see the [migration document](./migration.md) for more information on movi ## Export & Import -In the export & import section, you can export data from your GoToSocial account, or import data into it (TODO). +In the export & import section, you can export data from your GoToSocial account, or import data into it. + +![The export/import page.](../assets/user-settings-export-import.png) ### Export -To export your following, followers, lists, account blocks, or account mutes, you can use the button on this page. All exports will be served in Mastodon-compatible CSV format, so you can import them later into Mastodon or another GoToSocial instance, if you like. +To export your following, followers, lists, account blocks, or account mutes, you can use the button on this page. + +All exports will be served in Mastodon-compatible CSV format, so you can import them later into Mastodon or another GoToSocial instance, if you like. + +### Import + +You can use the import section to import data from another account into your GoToSocial account, using CSV files exported from the other account. + +This is useful in cases where you've [migrated your account](./migration.md) to a GoToSocial account, and you want to keep your list of accounts that you followed, blocked, etc., on your previous account. + +To import data into your account, first click on "Browse" and select a Mastodon-compatible CSV file [exported from Mastodon](https://docs.joinmastodon.org/user/moving/#export) or another compatible instance. + +Then, use the drop-down selector to pick what kind of data you are uploading via the CSV file. + +!!! warning + Be careful when selecting "type" or you may end up accidentally blocking a bunch of accounts you meant to follow, or vice versa! + +Then choose whether you want to either **merge** the new data with the existing data of that type on your GoToSocial account, or whether you want to **overwrite** existing data of that type with the data contained in the CSV file. + +If you choose **merge**, then any data contained in the CSV file will be added to existing data without removing any of that existing data. + +For example, if you follow `account1`, and `account2` from your GoToSocial account, and you're uploading a CSV file containing follows of `account3`, and `account4`, and using mode **merge**, then at the end of the import you will be following `account1`, `account2`, `account3`, and `account4`. + +If you choose **overwrite**, then any data contained in the CSV file will *replace* the existing data, by removing entries not contained in the CSV file. + +For example, if you follow `account1`, and `account2` from your GoToSocial account, and you're uploading a CSV file containing follows of `account3`, and `account4`, and using mode **overwrite**, then at the end of the import you will be following `account3`, and `account4`. Your follows of `account1` and `account2` will be removed. + +Both merge and overwrite operations are idempotent, which basically means that duplicate entries in the existing data and in the CSV file are not an issue, and you can do imports of the same data multiple times if you need to retry importing for whatever reason. + +!!! info + For a variety of reasons, it will not always be possible to recreate every entry in an uploaded CSV file via importing. For example, say you are trying to import a CSV of follows containing `example_account`, but `example_account`'s instance has gone offline, or their instance blocks yours, or your instance blocks theirs, etc. In this case, the follow of `example_account` would not be created. diff --git a/internal/api/auth/token_test.go b/internal/api/auth/token_test.go index c97fce3b9..1c53b5b2e 100644 --- a/internal/api/auth/token_test.go +++ b/internal/api/auth/token_test.go @@ -57,7 +57,7 @@ func (suite *TokenTestSuite) TestRetrieveClientCredentialsOK() { testClient := suite.testClients["local_account_1"] requestBody, w, err := testrig.CreateMultipartFormData( - "", "", + nil, map[string][]string{ "grant_type": {"client_credentials"}, "client_id": {testClient.ID}, @@ -103,7 +103,7 @@ func (suite *TokenTestSuite) TestRetrieveAuthorizationCodeOK() { testUserAuthorizationToken := suite.testTokens["local_account_1_user_authorization_token"] requestBody, w, err := testrig.CreateMultipartFormData( - "", "", + nil, map[string][]string{ "grant_type": {"authorization_code"}, "client_id": {testClient.ID}, @@ -148,7 +148,7 @@ func (suite *TokenTestSuite) TestRetrieveAuthorizationCodeNoCode() { testClient := suite.testClients["local_account_1"] requestBody, w, err := testrig.CreateMultipartFormData( - "", "", + nil, map[string][]string{ "grant_type": {"authorization_code"}, "client_id": {testClient.ID}, @@ -180,7 +180,7 @@ func (suite *TokenTestSuite) TestRetrieveAuthorizationCodeWrongGrantType() { testClient := suite.testClients["local_account_1"] requestBody, w, err := testrig.CreateMultipartFormData( - "", "", + nil, map[string][]string{ "grant_type": {"client_credentials"}, "client_id": {testClient.ID}, diff --git a/internal/api/client.go b/internal/api/client.go index 64f185430..65d4f29d5 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -35,6 +35,7 @@ import ( filtersV2 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v2" "github.com/superseriousbusiness/gotosocial/internal/api/client/followedtags" "github.com/superseriousbusiness/gotosocial/internal/api/client/followrequests" + importdata "github.com/superseriousbusiness/gotosocial/internal/api/client/import" "github.com/superseriousbusiness/gotosocial/internal/api/client/instance" "github.com/superseriousbusiness/gotosocial/internal/api/client/interactionpolicies" "github.com/superseriousbusiness/gotosocial/internal/api/client/lists" @@ -76,6 +77,7 @@ type Client struct { filtersV2 *filtersV2.Module // api/v2/filters followRequests *followrequests.Module // api/v1/follow_requests followedTags *followedtags.Module // api/v1/followed_tags + importData *importdata.Module // api/v1/import instance *instance.Module // api/v1/instance interactionPolicies *interactionpolicies.Module // api/v1/interaction_policies lists *lists.Module // api/v1/lists @@ -125,6 +127,7 @@ func (c *Client) Route(r *router.Router, m ...gin.HandlerFunc) { c.filtersV2.Route(h) c.followRequests.Route(h) c.followedTags.Route(h) + c.importData.Route(h) c.instance.Route(h) c.interactionPolicies.Route(h) c.lists.Route(h) @@ -162,6 +165,7 @@ func NewClient(state *state.State, p *processing.Processor) *Client { filtersV2: filtersV2.New(p), followRequests: followrequests.New(p), followedTags: followedtags.New(p), + importData: importdata.New(p), instance: instance.New(p), interactionPolicies: interactionpolicies.New(p), lists: lists.New(p), diff --git a/internal/api/client/accounts/accountdelete_test.go b/internal/api/client/accounts/accountdelete_test.go index 2f5a25b4b..66a5fa097 100644 --- a/internal/api/client/accounts/accountdelete_test.go +++ b/internal/api/client/accounts/accountdelete_test.go @@ -35,7 +35,7 @@ func (suite *AccountDeleteTestSuite) TestAccountDeletePOSTHandler() { // set up the request // we're deleting zork requestBody, w, err := testrig.CreateMultipartFormData( - "", "", + nil, map[string][]string{ "password": {"password"}, }) @@ -57,7 +57,7 @@ func (suite *AccountDeleteTestSuite) TestAccountDeletePOSTHandlerWrongPassword() // set up the request // we're deleting zork requestBody, w, err := testrig.CreateMultipartFormData( - "", "", + nil, map[string][]string{ "password": {"aaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, }) @@ -79,7 +79,7 @@ func (suite *AccountDeleteTestSuite) TestAccountDeletePOSTHandlerNoPassword() { // set up the request // we're deleting zork requestBody, w, err := testrig.CreateMultipartFormData( - "", "", + nil, map[string][]string{}) if err != nil { panic(err) diff --git a/internal/api/client/accounts/accountupdate_test.go b/internal/api/client/accounts/accountupdate_test.go index 09996e998..d0def500c 100644 --- a/internal/api/client/accounts/accountupdate_test.go +++ b/internal/api/client/accounts/accountupdate_test.go @@ -51,7 +51,7 @@ func (suite *AccountUpdateTestSuite) updateAccountFromForm(data map[string][]str } func (suite *AccountUpdateTestSuite) updateAccountFromFormData(data map[string][]string, expectedHTTPStatus int, expectedBody string) (*apimodel.Account, error) { - requestBody, w, err := testrig.CreateMultipartFormData("", "", data) + requestBody, w, err := testrig.CreateMultipartFormData(nil, data) if err != nil { suite.FailNow(err.Error()) } @@ -59,8 +59,8 @@ func (suite *AccountUpdateTestSuite) updateAccountFromFormData(data map[string][ return suite.updateAccount(requestBody.Bytes(), w.FormDataContentType(), expectedHTTPStatus, expectedBody) } -func (suite *AccountUpdateTestSuite) updateAccountFromFormDataWithFile(fieldName string, fileName string, data map[string][]string, expectedHTTPStatus int, expectedBody string) (*apimodel.Account, error) { - requestBody, w, err := testrig.CreateMultipartFormData(fieldName, fileName, data) +func (suite *AccountUpdateTestSuite) updateAccountFromFormDataWithFile(fieldName string, filePath string, data map[string][]string, expectedHTTPStatus int, expectedBody string) (*apimodel.Account, error) { + requestBody, w, err := testrig.CreateMultipartFormData(testrig.FileToDataF(fieldName, filePath), data) if err != nil { suite.FailNow(err.Error()) } diff --git a/internal/api/client/admin/emojicreate_test.go b/internal/api/client/admin/emojicreate_test.go index a687fb0af..9e985459b 100644 --- a/internal/api/client/admin/emojicreate_test.go +++ b/internal/api/client/admin/emojicreate_test.go @@ -38,7 +38,7 @@ type EmojiCreateTestSuite struct { func (suite *EmojiCreateTestSuite) TestEmojiCreateNewCategory() { // set up the request requestBody, w, err := testrig.CreateMultipartFormData( - "image", "../../../../testrig/media/rainbow-original.png", + testrig.FileToDataF("image", "../../../../testrig/media/rainbow-original.png"), map[string][]string{ "shortcode": {"new_emoji"}, "category": {"Test Emojis"}, // this category doesn't exist yet @@ -111,7 +111,7 @@ func (suite *EmojiCreateTestSuite) TestEmojiCreateNewCategory() { func (suite *EmojiCreateTestSuite) TestEmojiCreateExistingCategory() { // set up the request requestBody, w, err := testrig.CreateMultipartFormData( - "image", "../../../../testrig/media/rainbow-original.png", + testrig.FileToDataF("image", "../../../../testrig/media/rainbow-original.png"), map[string][]string{ "shortcode": {"new_emoji"}, "category": {"cute stuff"}, // this category already exists @@ -184,7 +184,7 @@ func (suite *EmojiCreateTestSuite) TestEmojiCreateExistingCategory() { func (suite *EmojiCreateTestSuite) TestEmojiCreateNoCategory() { // set up the request requestBody, w, err := testrig.CreateMultipartFormData( - "image", "../../../../testrig/media/rainbow-original.png", + testrig.FileToDataF("image", "../../../../testrig/media/rainbow-original.png"), map[string][]string{ "shortcode": {"new_emoji"}, "category": {""}, @@ -257,7 +257,7 @@ func (suite *EmojiCreateTestSuite) TestEmojiCreateNoCategory() { func (suite *EmojiCreateTestSuite) TestEmojiCreateAlreadyExists() { // set up the request -- use a shortcode that already exists for an emoji in the database requestBody, w, err := testrig.CreateMultipartFormData( - "image", "../../../../testrig/media/rainbow-original.png", + testrig.FileToDataF("image", "../../../../testrig/media/rainbow-original.png"), map[string][]string{ "shortcode": {"rainbow"}, }) diff --git a/internal/api/client/admin/emojiupdate_test.go b/internal/api/client/admin/emojiupdate_test.go index 073e3cec0..5df43d7ae 100644 --- a/internal/api/client/admin/emojiupdate_test.go +++ b/internal/api/client/admin/emojiupdate_test.go @@ -44,7 +44,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateNewCategory() { // set up the request requestBody, w, err := testrig.CreateMultipartFormData( - "", "", + nil, map[string][]string{ "category": {"New Category"}, // this category doesn't exist yet "type": {"modify"}, @@ -121,7 +121,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateSwitchCategory() { // set up the request requestBody, w, err := testrig.CreateMultipartFormData( - "", "", + nil, map[string][]string{ "type": {"modify"}, "category": {"cute stuff"}, @@ -198,7 +198,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyRemoteToLocal() { // set up the request requestBody, w, err := testrig.CreateMultipartFormData( - "", "", + nil, map[string][]string{ "type": {"copy"}, "category": {"emojis i stole"}, @@ -276,7 +276,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateDisableEmoji() { // set up the request requestBody, w, err := testrig.CreateMultipartFormData( - "", "", + nil, map[string][]string{ "type": {"disable"}, }) @@ -317,7 +317,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateDisableLocalEmoji() { // set up the request requestBody, w, err := testrig.CreateMultipartFormData( - "", "", + nil, map[string][]string{ "type": {"disable"}, }) @@ -350,7 +350,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateModifyRemoteEmoji() { // set up the request requestBody, w, err := testrig.CreateMultipartFormData( - "image", "../../../../testrig/media/kip-original.gif", + testrig.FileToDataF("image", "../../../../testrig/media/kip-original.gif"), map[string][]string{ "type": {"modify"}, }) @@ -383,7 +383,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateModifyNoParams() { // set up the request requestBody, w, err := testrig.CreateMultipartFormData( - "", "", + nil, map[string][]string{ "type": {"modify"}, }) @@ -416,7 +416,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyLocalToLocal() { // set up the request requestBody, w, err := testrig.CreateMultipartFormData( - "", "", + nil, map[string][]string{ "type": {"copy"}, "shortcode": {"bottoms"}, @@ -450,7 +450,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyEmptyShortcode() { // set up the request requestBody, w, err := testrig.CreateMultipartFormData( - "", "", + nil, map[string][]string{ "type": {"copy"}, "shortcode": {""}, @@ -484,7 +484,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyNoShortcode() { // set up the request requestBody, w, err := testrig.CreateMultipartFormData( - "", "", + nil, map[string][]string{ "type": {"copy"}, }) @@ -517,7 +517,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyShortcodeAlreadyInUse() { // set up the request requestBody, w, err := testrig.CreateMultipartFormData( - "", "", + nil, map[string][]string{ "type": {"copy"}, "shortcode": {"rainbow"}, diff --git a/internal/api/client/import/import.go b/internal/api/client/import/import.go new file mode 100644 index 000000000..6d85a6b23 --- /dev/null +++ b/internal/api/client/import/import.go @@ -0,0 +1,195 @@ +// 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 + +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.

+ ); }