diff options
Diffstat (limited to 'internal/api')
-rw-r--r-- | internal/api/client.go | 4 | ||||
-rw-r--r-- | internal/api/client/exports/blocks.go | 76 | ||||
-rw-r--r-- | internal/api/client/exports/exports.go | 54 | ||||
-rw-r--r-- | internal/api/client/exports/exports_test.go | 275 | ||||
-rw-r--r-- | internal/api/client/exports/followers.go | 76 | ||||
-rw-r--r-- | internal/api/client/exports/following.go | 76 | ||||
-rw-r--r-- | internal/api/client/exports/lists.go | 76 | ||||
-rw-r--r-- | internal/api/client/exports/mutes.go | 76 | ||||
-rw-r--r-- | internal/api/client/exports/stats.go | 77 | ||||
-rw-r--r-- | internal/api/model/exportimport.go | 60 | ||||
-rw-r--r-- | internal/api/util/mime.go | 1 | ||||
-rw-r--r-- | internal/api/util/negotiate.go | 6 | ||||
-rw-r--r-- | internal/api/util/response.go | 42 |
13 files changed, 899 insertions, 0 deletions
diff --git a/internal/api/client.go b/internal/api/client.go index 18ab9b50b..64f185430 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -28,6 +28,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/api/client/bookmarks" "github.com/superseriousbusiness/gotosocial/internal/api/client/conversations" "github.com/superseriousbusiness/gotosocial/internal/api/client/customemojis" + "github.com/superseriousbusiness/gotosocial/internal/api/client/exports" "github.com/superseriousbusiness/gotosocial/internal/api/client/favourites" "github.com/superseriousbusiness/gotosocial/internal/api/client/featuredtags" filtersV1 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v1" @@ -68,6 +69,7 @@ type Client struct { bookmarks *bookmarks.Module // api/v1/bookmarks conversations *conversations.Module // api/v1/conversations customEmojis *customemojis.Module // api/v1/custom_emojis + exports *exports.Module // api/v1/exports favourites *favourites.Module // api/v1/favourites featuredTags *featuredtags.Module // api/v1/featured_tags filtersV1 *filtersV1.Module // api/v1/filters @@ -116,6 +118,7 @@ func (c *Client) Route(r *router.Router, m ...gin.HandlerFunc) { c.bookmarks.Route(h) c.conversations.Route(h) c.customEmojis.Route(h) + c.exports.Route(h) c.favourites.Route(h) c.featuredTags.Route(h) c.filtersV1.Route(h) @@ -152,6 +155,7 @@ func NewClient(state *state.State, p *processing.Processor) *Client { bookmarks: bookmarks.New(p), conversations: conversations.New(p), customEmojis: customemojis.New(p), + exports: exports.New(p), favourites: favourites.New(p), featuredTags: featuredtags.New(p), filtersV1: filtersV1.New(p), diff --git a/internal/api/client/exports/blocks.go b/internal/api/client/exports/blocks.go new file mode 100644 index 000000000..c31e2b0b4 --- /dev/null +++ b/internal/api/client/exports/blocks.go @@ -0,0 +1,76 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package exports + +import ( + "net/http" + + "github.com/gin-gonic/gin" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +// ExportBlocksGETHandler swagger:operation GET /api/v1/exports/blocks.csv exportBlocks +// +// Export a CSV file of accounts that you block. +// +// --- +// tags: +// - import-export +// +// produces: +// - text/csv +// +// security: +// - OAuth2 Bearer: +// - read:blocks +// +// responses: +// '200': +// name: accounts +// description: CSV file of accounts that you block. +// '401': +// description: unauthorized +// '406': +// description: not acceptable +// '500': +// description: internal server error +func (m *Module) ExportBlocksGETHandler(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 _, err := apiutil.NegotiateAccept(c, apiutil.CSVHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1) + return + } + + records, errWithCode := m.processor.Account().ExportBlocks( + c.Request.Context(), + authed.Account, + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + apiutil.EncodeCSVResponse(c.Writer, c.Request, http.StatusOK, records) +} diff --git a/internal/api/client/exports/exports.go b/internal/api/client/exports/exports.go new file mode 100644 index 000000000..90a246a74 --- /dev/null +++ b/internal/api/client/exports/exports.go @@ -0,0 +1,54 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package exports + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/processing" +) + +const ( + BasePath = "/v1/exports" + StatsPath = BasePath + "/stats" + FollowingPath = BasePath + "/following.csv" + FollowersPath = BasePath + "/followers.csv" + ListsPath = BasePath + "/lists.csv" + BlocksPath = BasePath + "/blocks.csv" + MutesPath = BasePath + "/mutes.csv" +) + +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.MethodGet, StatsPath, m.ExportStatsGETHandler) + attachHandler(http.MethodGet, FollowingPath, m.ExportFollowingGETHandler) + attachHandler(http.MethodGet, FollowersPath, m.ExportFollowersGETHandler) + attachHandler(http.MethodGet, ListsPath, m.ExportListsGETHandler) + attachHandler(http.MethodGet, BlocksPath, m.ExportBlocksGETHandler) + attachHandler(http.MethodGet, MutesPath, m.ExportMutesGETHandler) +} diff --git a/internal/api/client/exports/exports_test.go b/internal/api/client/exports/exports_test.go new file mode 100644 index 000000000..1943f2582 --- /dev/null +++ b/internal/api/client/exports/exports_test.go @@ -0,0 +1,275 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package exports_test + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/api/client/exports" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" + "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 ExportsTestSuite 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 + exportsModule *exports.Module +} + +func (suite *ExportsTestSuite) SetupSuite() { + suite.testTokens = testrig.NewTestTokens() + suite.testClients = testrig.NewTestClients() + suite.testApplications = testrig.NewTestApplications() + suite.testUsers = testrig.NewTestUsers() + suite.testAccounts = testrig.NewTestAccounts() +} + +func (suite *ExportsTestSuite) SetupTest() { + suite.state.Caches.Init() + testrig.StartNoopWorkers(&suite.state) + + 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, + ) + + suite.exportsModule = exports.New(processor) +} + +func (suite *ExportsTestSuite) TriggerHandler( + handler gin.HandlerFunc, + path string, + contentType string, + application *gtsmodel.Application, + token *gtsmodel.Token, + user *gtsmodel.User, + account *gtsmodel.Account, +) *httptest.ResponseRecorder { + // 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.SessionAuthorizedApplication, application) + ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(token)) + ctx.Set(oauth.SessionAuthorizedUser, user) + ctx.Set(oauth.SessionAuthorizedAccount, account) + + // Create test request. + target := "http://localhost:8080/api" + path + ctx.Request = httptest.NewRequest(http.MethodGet, target, nil) + ctx.Request.Header.Set("Accept", contentType) + + // Trigger handler. + handler(ctx) + + return recorder +} + +func (suite *ExportsTestSuite) TearDownTest() { + testrig.StandardDBTeardown(suite.state.DB) + testrig.StandardStorageTeardown(suite.state.Storage) + testrig.StopWorkers(&suite.state) +} + +func (suite *ExportsTestSuite) TestExports() { + type testCase struct { + handler gin.HandlerFunc + path string + contentType string + application *gtsmodel.Application + token *gtsmodel.Token + user *gtsmodel.User + account *gtsmodel.Account + expect string + } + + testCases := []testCase{ + // Export Following + { + handler: suite.exportsModule.ExportFollowingGETHandler, + path: exports.FollowingPath, + contentType: apiutil.TextCSV, + application: suite.testApplications["application_1"], + token: suite.testTokens["local_account_1"], + user: suite.testUsers["local_account_1"], + account: suite.testAccounts["local_account_1"], + expect: `Account address,Show boosts +admin@localhost:8080,true +1happyturtle@localhost:8080,true +`, + }, + // Export Followers. + { + handler: suite.exportsModule.ExportFollowersGETHandler, + path: exports.FollowingPath, + contentType: apiutil.TextCSV, + application: suite.testApplications["application_1"], + token: suite.testTokens["local_account_1"], + user: suite.testUsers["local_account_1"], + account: suite.testAccounts["local_account_1"], + expect: `Account address +1happyturtle@localhost:8080 +admin@localhost:8080 +`, + }, + // Export Lists. + { + handler: suite.exportsModule.ExportListsGETHandler, + path: exports.ListsPath, + contentType: apiutil.TextCSV, + application: suite.testApplications["application_1"], + token: suite.testTokens["local_account_1"], + user: suite.testUsers["local_account_1"], + account: suite.testAccounts["local_account_1"], + expect: `Cool Ass Posters From This Instance,admin@localhost:8080 +Cool Ass Posters From This Instance,1happyturtle@localhost:8080 +`, + }, + // Export Mutes. + { + handler: suite.exportsModule.ExportMutesGETHandler, + path: exports.MutesPath, + contentType: apiutil.TextCSV, + application: suite.testApplications["application_1"], + token: suite.testTokens["local_account_1"], + user: suite.testUsers["local_account_1"], + account: suite.testAccounts["local_account_1"], + expect: `Account address,Hide notifications +`, + }, + // Export Blocks. + { + handler: suite.exportsModule.ExportBlocksGETHandler, + path: exports.BlocksPath, + contentType: apiutil.TextCSV, + application: suite.testApplications["application_1"], + token: suite.testTokens["local_account_2"], + user: suite.testUsers["local_account_2"], + account: suite.testAccounts["local_account_2"], + expect: `foss_satan@fossbros-anonymous.io +`, + }, + // Export Stats. + { + handler: suite.exportsModule.ExportStatsGETHandler, + path: exports.StatsPath, + contentType: apiutil.AppJSON, + application: suite.testApplications["application_1"], + token: suite.testTokens["local_account_1"], + user: suite.testUsers["local_account_1"], + account: suite.testAccounts["local_account_1"], + expect: `{ + "media_storage": "", + "followers_count": 2, + "following_count": 2, + "statuses_count": 8, + "lists_count": 1, + "blocks_count": 0, + "mutes_count": 0 +}`, + }, + } + + for _, test := range testCases { + recorder := suite.TriggerHandler( + test.handler, + test.path, + test.contentType, + test.application, + test.token, + test.user, + test.account, + ) + + // Check response code. + suite.EqualValues(http.StatusOK, recorder.Code) + + // Check response body. + b, err := io.ReadAll(recorder.Body) + if err != nil { + suite.FailNow(err.Error()) + } + + // If json response, indent it nicely. + if recorder.Result().Header.Get("Content-Type") == "application/json" { + dst := &bytes.Buffer{} + if err := json.Indent(dst, b, "", " "); err != nil { + suite.FailNow(err.Error()) + } + b = dst.Bytes() + } + + suite.Equal(test.expect, string(b)) + } +} + +func TestExportsTestSuite(t *testing.T) { + suite.Run(t, new(ExportsTestSuite)) +} diff --git a/internal/api/client/exports/followers.go b/internal/api/client/exports/followers.go new file mode 100644 index 000000000..ceef94659 --- /dev/null +++ b/internal/api/client/exports/followers.go @@ -0,0 +1,76 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package exports + +import ( + "net/http" + + "github.com/gin-gonic/gin" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +// ExportFollowersGETHandler swagger:operation GET /api/v1/exports/followers.csv exportFollowers +// +// Export a CSV file of accounts that follow you. +// +// --- +// tags: +// - import-export +// +// produces: +// - text/csv +// +// security: +// - OAuth2 Bearer: +// - read:follows +// +// responses: +// '200': +// name: accounts +// description: CSV file of accounts that follow you. +// '401': +// description: unauthorized +// '406': +// description: not acceptable +// '500': +// description: internal server error +func (m *Module) ExportFollowersGETHandler(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 _, err := apiutil.NegotiateAccept(c, apiutil.CSVHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1) + return + } + + records, errWithCode := m.processor.Account().ExportFollowers( + c.Request.Context(), + authed.Account, + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + apiutil.EncodeCSVResponse(c.Writer, c.Request, http.StatusOK, records) +} diff --git a/internal/api/client/exports/following.go b/internal/api/client/exports/following.go new file mode 100644 index 000000000..e61cafc2a --- /dev/null +++ b/internal/api/client/exports/following.go @@ -0,0 +1,76 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package exports + +import ( + "net/http" + + "github.com/gin-gonic/gin" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +// ExportFollowingGETHandler swagger:operation GET /api/v1/exports/following.csv exportFollowing +// +// Export a CSV file of accounts that you follow. +// +// --- +// tags: +// - import-export +// +// produces: +// - text/csv +// +// security: +// - OAuth2 Bearer: +// - read:follows +// +// responses: +// '200': +// name: accounts +// description: CSV file of accounts that you follow. +// '401': +// description: unauthorized +// '406': +// description: not acceptable +// '500': +// description: internal server error +func (m *Module) ExportFollowingGETHandler(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 _, err := apiutil.NegotiateAccept(c, apiutil.CSVHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1) + return + } + + records, errWithCode := m.processor.Account().ExportFollowing( + c.Request.Context(), + authed.Account, + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + apiutil.EncodeCSVResponse(c.Writer, c.Request, http.StatusOK, records) +} diff --git a/internal/api/client/exports/lists.go b/internal/api/client/exports/lists.go new file mode 100644 index 000000000..2debcc701 --- /dev/null +++ b/internal/api/client/exports/lists.go @@ -0,0 +1,76 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package exports + +import ( + "net/http" + + "github.com/gin-gonic/gin" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +// ExportListsGETHandler swagger:operation GET /api/v1/exports/lists.csv exportLists +// +// Export a CSV file of lists created by you. +// +// --- +// tags: +// - import-export +// +// produces: +// - text/csv +// +// security: +// - OAuth2 Bearer: +// - read:lists +// +// responses: +// '200': +// name: accounts +// description: CSV file of lists. +// '401': +// description: unauthorized +// '406': +// description: not acceptable +// '500': +// description: internal server error +func (m *Module) ExportListsGETHandler(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 _, err := apiutil.NegotiateAccept(c, apiutil.CSVHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1) + return + } + + records, errWithCode := m.processor.Account().ExportLists( + c.Request.Context(), + authed.Account, + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + apiutil.EncodeCSVResponse(c.Writer, c.Request, http.StatusOK, records) +} diff --git a/internal/api/client/exports/mutes.go b/internal/api/client/exports/mutes.go new file mode 100644 index 000000000..ab49b7719 --- /dev/null +++ b/internal/api/client/exports/mutes.go @@ -0,0 +1,76 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package exports + +import ( + "net/http" + + "github.com/gin-gonic/gin" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +// ExportMutesGETHandler swagger:operation GET /api/v1/exports/mutes.csv exportMutes +// +// Export a CSV file of accounts that you mute. +// +// --- +// tags: +// - import-export +// +// produces: +// - text/csv +// +// security: +// - OAuth2 Bearer: +// - read:mutes +// +// responses: +// '200': +// name: accounts +// description: CSV file of accounts that you mute. +// '401': +// description: unauthorized +// '406': +// description: not acceptable +// '500': +// description: internal server error +func (m *Module) ExportMutesGETHandler(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 _, err := apiutil.NegotiateAccept(c, apiutil.CSVHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1) + return + } + + records, errWithCode := m.processor.Account().ExportMutes( + c.Request.Context(), + authed.Account, + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + apiutil.EncodeCSVResponse(c.Writer, c.Request, http.StatusOK, records) +} diff --git a/internal/api/client/exports/stats.go b/internal/api/client/exports/stats.go new file mode 100644 index 000000000..9e3f1b600 --- /dev/null +++ b/internal/api/client/exports/stats.go @@ -0,0 +1,77 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package exports + +import ( + "net/http" + + "github.com/gin-gonic/gin" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +// ExportStatsGETHandler swagger:operation GET /api/v1/exports/stats exportStats +// +// Returns informational stats on the number of items that can be exported for requesting account. +// +// --- +// tags: +// - import-export +// +// produces: +// - application/json +// +// security: +// - OAuth2 Bearer: +// - read:account +// +// responses: +// '200': +// description: Export stats for the requesting account. +// schema: +// "$ref": "#/definitions/accountExportStats" +// '401': +// description: unauthorized +// '406': +// description: not acceptable +// '500': +// description: internal server error +func (m *Module) ExportStatsGETHandler(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 _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1) + return + } + + exportStats, errWithCode := m.processor.Account().ExportStats( + c.Request.Context(), + authed.Account, + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + apiutil.JSON(c, http.StatusOK, exportStats) +} diff --git a/internal/api/model/exportimport.go b/internal/api/model/exportimport.go new file mode 100644 index 000000000..d87ed8cd3 --- /dev/null +++ b/internal/api/model/exportimport.go @@ -0,0 +1,60 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package model + +// AccountExportStats models an account's stats +// specifically for the purpose of informing about +// export sizes at the /api/v1/exports/stats endpoint. +// +// swagger:model accountExportStats +type AccountExportStats struct { + // TODO: String representation of media storage size attributed to this account. + // + // example: 500MB + MediaStorage string `json:"media_storage"` + + // Number of accounts following this account. + // + // example: 50 + FollowersCount int `json:"followers_count"` + + // Number of accounts followed by this account. + // + // example: 50 + FollowingCount int `json:"following_count"` + + // Number of statuses created by this account. + // + // example: 81986 + StatusesCount int `json:"statuses_count"` + + // Number of lists created by this account. + // + // example: 10 + ListsCount int `json:"lists_count"` + + // Number of accounts blocked by this account. + // + // example: 15 + BlocksCount int `json:"blocks_count"` + + // Number of accounts muted by this account. + // + // example: 11 + MutesCount int `json:"mutes_count"` +} diff --git a/internal/api/util/mime.go b/internal/api/util/mime.go index 8dcbfc28c..4d8946e5d 100644 --- a/internal/api/util/mime.go +++ b/internal/api/util/mime.go @@ -35,6 +35,7 @@ const ( TextXML = `text/xml` TextHTML = `text/html` TextCSS = `text/css` + TextCSV = `text/csv` ) // JSONContentType returns whether is application/json(;charset=utf-8)? content-type. diff --git a/internal/api/util/negotiate.go b/internal/api/util/negotiate.go index 5b4f54bc6..4910b7aef 100644 --- a/internal/api/util/negotiate.go +++ b/internal/api/util/negotiate.go @@ -88,6 +88,12 @@ var HostMetaHeaders = []string{ AppXML, } +// CSVHeaders just contains the text/csv +// MIME type, used for import/export. +var CSVHeaders = []string{ + TextCSV, +} + // NegotiateAccept takes the *gin.Context from an incoming request, and a // slice of Offers, and performs content negotiation for the given request // with the given content-type offers. It will return a string representation diff --git a/internal/api/util/response.go b/internal/api/util/response.go index afdc578aa..01f15ccfb 100644 --- a/internal/api/util/response.go +++ b/internal/api/util/response.go @@ -18,6 +18,7 @@ package util import ( + "encoding/csv" "encoding/json" "encoding/xml" "io" @@ -213,6 +214,47 @@ func EncodeXMLResponse( putBuf(buf) } +// EncodeCSVResponse encodes 'records' as CSV HTTP response +// to ResponseWriter with given status code, using CSV content-type. +func EncodeCSVResponse( + rw http.ResponseWriter, + r *http.Request, + statusCode int, + records [][]string, +) { + // Acquire buffer. + buf := getBuf() + + // Wrap buffer in CSV writer. + csvWriter := csv.NewWriter(buf) + + // Write all the records to the buffer. + if err := csvWriter.WriteAll(records); err == nil { + // Respond with the now-known + // size byte slice within buf. + WriteResponseBytes(rw, r, + statusCode, + TextCSV, + buf.B, + ) + } else { + // This will always be an csv error, we + // can't really add any more useful context. + log.Error(r.Context(), err) + + // Any error returned here is unrecoverable, + // set Internal Server Error JSON response. + WriteResponseBytes(rw, r, + http.StatusInternalServerError, + AppJSON, + StatusInternalServerErrorJSON, + ) + } + + // Release. + putBuf(buf) +} + // writeResponseUnknownLength handles reading data of unknown legnth // efficiently into memory, and passing on to WriteResponseBytes(). func writeResponseUnknownLength( |