From 941893a774c83802afdc4cc76e1d30c59b6c5585 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Mon, 2 Jan 2023 13:10:50 +0100 Subject: [PATCH] [chore] The Big Middleware and API Refactor (tm) (#1250) * interim commit: start refactoring middlewares into package under router * another interim commit, this is becoming a big job * another fucking massive interim commit * refactor bookmarks to new style * ambassador, wiz zeze commits you are spoiling uz * she compiles, we're getting there * we're just normal men; we're just innocent men * apiutil * whoopsie * i'm glad noone reads commit msgs haha :blob_sweat: * use that weirdo go-bytesize library for maxMultipartMemory * fix media module paths --- CONTRIBUTING.md | 4 +- cmd/gotosocial/action/server/server.go | 157 +++++++----------- cmd/gotosocial/action/testrig/testrig.go | 146 +++++++--------- docs/api/ratelimiting.md | 12 +- docs/api/swagger.yaml | 10 +- example/config.yaml | 12 +- internal/api/activitypub.go | 66 ++++++++ .../{client => activitypub}/emoji/emoji.go | 19 +-- .../{s2s => activitypub}/emoji/emojiget.go | 29 +--- .../emoji/emojiget_test.go | 31 ++-- .../{s2s/user => activitypub/users}/common.go | 26 +-- .../user => activitypub/users}/followers.go | 18 +- .../user => activitypub/users}/following.go | 18 +- .../user => activitypub/users}/inboxpost.go | 14 +- .../users}/inboxpost_test.go | 28 ++-- .../user => activitypub/users}/outboxget.go | 20 +-- .../users}/outboxget_test.go | 22 +-- .../users}/publickeyget.go | 18 +- .../user => activitypub/users}/repliesget.go | 24 +-- .../users}/repliesget_test.go | 26 +-- .../user => activitypub/users}/statusget.go | 20 +-- .../users}/statusget_test.go | 16 +- internal/api/activitypub/users/user.go | 80 +++++++++ .../user => activitypub/users}/user_test.go | 34 ++-- .../user => activitypub/users}/userget.go | 18 +- .../users}/userget_test.go | 14 +- internal/api/apimodule.go | 37 ----- internal/api/auth.go | 64 +++++++ internal/api/auth/auth.go | 117 +++++++++++++ internal/api/{client => }/auth/auth_test.go | 24 +-- internal/api/{client => }/auth/authorize.go | 46 ++--- .../api/{client => }/auth/authorize_test.go | 10 +- internal/api/{client => }/auth/callback.go | 48 +++--- internal/api/{client => }/auth/oob.go | 18 +- internal/api/{client => }/auth/signin.go | 20 +-- internal/api/{client => }/auth/token.go | 12 +- internal/api/{client => }/auth/token_test.go | 0 internal/api/client.go | 129 ++++++++++++++ .../{account => accounts}/account_test.go | 8 +- .../{account => accounts}/accountcreate.go | 24 +-- .../accountcreate_test.go | 2 +- .../{account => accounts}/accountdelete.go | 16 +- .../accountdelete_test.go | 16 +- .../{account => accounts}/accountget.go | 14 +- .../account.go => accounts/accounts.go} | 66 +++----- .../{account => accounts}/accountupdate.go | 22 +-- .../accountupdate_test.go | 44 ++--- .../{account => accounts}/accountverify.go | 12 +- .../accountverify_test.go | 8 +- .../api/client/{account => accounts}/block.go | 14 +- .../{account => accounts}/block_test.go | 10 +- .../client/{account => accounts}/follow.go | 20 +-- .../{account => accounts}/follow_test.go | 10 +- .../client/{account => accounts}/followers.go | 14 +- .../client/{account => accounts}/following.go | 14 +- .../{account => accounts}/relationships.go | 18 +- .../client/{account => accounts}/statuses.go | 26 +-- .../{account => accounts}/statuses_test.go | 12 +- .../client/{account => accounts}/unblock.go | 14 +- .../client/{account => accounts}/unfollow.go | 14 +- internal/api/client/admin/accountaction.go | 18 +- internal/api/client/admin/admin.go | 41 ++--- internal/api/client/admin/admin_test.go | 2 +- .../api/client/admin/domainblockcreate.go | 26 +-- .../api/client/admin/domainblockdelete.go | 14 +- internal/api/client/admin/domainblockget.go | 16 +- internal/api/client/admin/domainblocksget.go | 14 +- .../api/client/admin/emojicategoriesget.go | 12 +- internal/api/client/admin/emojicreate.go | 22 +-- internal/api/client/admin/emojidelete.go | 14 +- internal/api/client/admin/emojiget.go | 14 +- internal/api/client/admin/emojisget.go | 16 +- internal/api/client/admin/emojiupdate.go | 36 ++-- internal/api/client/admin/mediacleanup.go | 14 +- internal/api/client/admin/mediarefetch.go | 8 +- internal/api/client/app/app_test.go | 21 --- .../api/client/{app => apps}/appcreate.go | 26 +-- .../api/client/{app/app.go => apps/apps.go} | 19 +-- internal/api/client/auth/auth.go | 105 ------------ internal/api/client/blocks/blocks.go | 17 +- internal/api/client/blocks/blocksget.go | 12 +- internal/api/client/bookmarks/bookmarks.go | 13 +- .../api/client/bookmarks/bookmarks_test.go | 10 +- internal/api/client/bookmarks/bookmarksget.go | 14 +- .../api/client/customemojis/customemojis.go | 45 +++++ .../client/customemojis/customemojisget.go | 76 +++++++++ internal/api/client/emoji/emojisget.go | 58 ------- internal/api/client/favourites/favourites.go | 17 +- .../api/client/favourites/favourites_test.go | 2 +- .../api/client/favourites/favouritesget.go | 12 +- .../api/client/{filter => filters}/filter.go | 17 +- .../client/{filter => filters}/filtersget.go | 8 +- .../authorize.go | 14 +- .../authorize_test.go | 8 +- .../followrequest.go | 23 +-- .../followrequest_test.go | 8 +- .../{followrequest => followrequests}/get.go | 12 +- .../get_test.go | 2 +- .../reject.go | 14 +- .../reject_test.go | 6 +- internal/api/client/instance/instance.go | 21 +-- internal/api/client/instance/instance_test.go | 2 +- internal/api/client/instance/instanceget.go | 8 +- internal/api/client/instance/instancepatch.go | 22 +-- .../api/client/instance/instancepeersget.go | 12 +- internal/api/client/list/listsgets.go | 25 --- internal/api/client/{list => lists}/list.go | 19 +-- .../robots.go => client/lists/listsgets.go} | 47 ++---- internal/api/client/media/media.go | 27 ++- internal/api/client/media/mediacreate.go | 36 ++-- internal/api/client/media/mediacreate_test.go | 53 ++---- internal/api/client/media/mediaget.go | 22 ++- internal/api/client/media/mediaupdate.go | 32 ++-- internal/api/client/media/mediaupdate_test.go | 31 ++-- .../notifications.go} | 21 +-- .../notificationsclear.go | 12 +- .../notificationsget.go | 14 +- internal/api/client/search/search.go | 23 +-- internal/api/client/search/search_test.go | 2 +- internal/api/client/search/searchget.go | 26 +-- .../api/client/{status => statuses}/status.go | 71 +++----- .../{status => statuses}/status_test.go | 8 +- .../{status => statuses}/statusbookmark.go | 14 +- .../statusbookmark_test.go | 8 +- .../{status => statuses}/statusboost.go | 14 +- .../{status => statuses}/statusboost_test.go | 28 ++-- .../{status => statuses}/statusboostedby.go | 10 +- .../statusboostedby_test.go | 8 +- .../{status => statuses}/statuscontext.go | 14 +- .../{status => statuses}/statuscreate.go | 22 +-- .../{status => statuses}/statuscreate_test.go | 52 +++--- .../{status => statuses}/statusdelete.go | 14 +- .../{status => statuses}/statusdelete_test.go | 12 +- .../client/{status => statuses}/statusfave.go | 14 +- .../{status => statuses}/statusfave_test.go | 19 ++- .../{status => statuses}/statusfavedby.go | 14 +- .../statusfavedby_test.go | 12 +- .../client/{status => statuses}/statusget.go | 14 +- .../{status => statuses}/statusget_test.go | 2 +- .../{status => statuses}/statusunbookmark.go | 14 +- .../statusunbookmark_test.go | 8 +- .../{status => statuses}/statusunboost.go | 14 +- .../{status => statuses}/statusunfave.go | 14 +- .../{status => statuses}/statusunfave_test.go | 22 +-- internal/api/client/streaming/stream.go | 60 +++++-- internal/api/client/streaming/streaming.go | 19 +-- .../api/client/streaming/streaming_test.go | 2 +- .../client/{timeline => timelines}/home.go | 16 +- .../client/{timeline => timelines}/public.go | 16 +- .../{timeline => timelines}/timeline.go | 21 +-- internal/api/client/user/passwordchange.go | 20 +-- internal/api/client/user/user.go | 17 +- internal/api/client/user/user_test.go | 2 +- internal/api/fileserver.go | 55 ++++++ .../api/{client => }/fileserver/fileserver.go | 28 +--- .../fileserver/fileserver_test.go | 12 +- .../api/{client => }/fileserver/servefile.go | 33 ++-- .../{client => }/fileserver/servefile_test.go | 2 +- internal/api/nodeinfo.go | 50 ++++++ internal/api/nodeinfo/nodeinfo.go | 46 +++++ .../api/{s2s => }/nodeinfo/nodeinfoget.go | 20 +-- internal/api/s2s/emoji/emoji.go | 53 ------ internal/api/s2s/nodeinfo/nodeinfo.go | 53 ------ internal/api/s2s/user/user.go | 89 ---------- internal/api/security/flocblock.go | 31 ---- internal/api/security/ratelimit.go | 70 -------- internal/api/security/security.go | 65 -------- internal/api/security/signaturecheck.go | 51 ------ internal/api/security/tokencheck.go | 120 ------------- internal/api/{ => util}/errorhandling.go | 2 +- internal/api/{ => util}/mime.go | 2 +- internal/api/{ => util}/negotiate.go | 2 +- internal/api/util/signaturectx.go | 41 +++++ internal/api/wellknown.go | 55 ++++++ internal/api/wellknown/nodeinfo/nodeinfo.go | 47 ++++++ .../nodeinfo/nodeinfoget.go} | 16 +- .../{s2s => wellknown}/webfinger/webfinger.go | 18 +- .../webfinger/webfinger_test.go | 23 ++- .../webfinger/webfingerget.go | 45 ++--- .../webfinger/webfingerget_test.go | 6 +- internal/config/defaults.go | 2 +- internal/db/db.go | 2 +- .../cachecontrol.go} | 18 +- internal/middleware/cors.go | 86 ++++++++++ .../security => middleware}/extraheaders.go | 19 ++- internal/{router => middleware}/gzip.go | 10 +- internal/middleware/logger.go | 99 +++++++++++ internal/middleware/ratelimit.go | 77 +++++++++ internal/{router => middleware}/session.go | 30 +--- .../{router => middleware}/session_test.go | 16 +- internal/middleware/signaturecheck.go | 93 +++++++++++ internal/middleware/tokencheck.go | 140 ++++++++++++++++ .../auth/util.go => middleware/useragent.go} | 22 ++- internal/oidc/idp.go | 8 - internal/processing/federation.go | 8 +- internal/processing/federation/federation.go | 4 +- internal/processing/federation/getnodeinfo.go | 5 +- internal/processing/fromclientapi_test.go | 4 +- internal/processing/fromfederator_test.go | 8 +- internal/processing/oauth.go | 6 + internal/processing/processor.go | 6 +- internal/processing/status/create_test.go | 52 +++--- internal/processing/status/util.go | 7 +- internal/processing/status/util_test.go | 62 +++---- internal/router/attach.go | 23 +-- internal/router/cors.go | 87 ---------- internal/router/logger.go | 96 ----------- internal/router/router.go | 67 ++++---- internal/router/template.go | 16 +- internal/text/emojify.go | 6 +- internal/transport/deliver.go | 4 +- internal/transport/dereference.go | 4 +- internal/transport/derefinstance.go | 8 +- internal/transport/finger.go | 4 +- internal/typeutils/converter.go | 38 ++--- internal/typeutils/frontendtointernal.go | 14 +- internal/typeutils/internaltofrontend.go | 142 ++++++++-------- internal/typeutils/internaltorss.go | 4 +- internal/web/assets.go | 21 +-- internal/web/base.go | 4 +- internal/web/confirmemail.go | 8 +- internal/web/customcss.go | 14 +- internal/web/profile.go | 22 +-- internal/web/robots.go | 44 ++++- internal/web/rss.go | 18 +- internal/web/settings-panel.go | 4 +- internal/web/thread.go | 26 +-- internal/web/web.go | 101 +++++------ testrig/router.go | 2 +- 229 files changed, 3209 insertions(+), 3068 deletions(-) create mode 100644 internal/api/activitypub.go rename internal/api/{client => activitypub}/emoji/emoji.go (62%) rename internal/api/{s2s => activitypub}/emoji/emojiget.go (54%) rename internal/api/{s2s => activitypub}/emoji/emojiget_test.go (86%) rename internal/api/{s2s/user => activitypub/users}/common.go (77%) rename internal/api/{s2s/user => activitypub/users}/followers.go (69%) rename internal/api/{s2s/user => activitypub/users}/following.go (70%) rename internal/api/{s2s/user => activitypub/users}/inboxpost.go (72%) rename internal/api/{s2s/user => activitypub/users}/inboxpost_test.go (97%) rename internal/api/{s2s/user => activitypub/users}/outboxget.go (79%) rename internal/api/{s2s/user => activitypub/users}/outboxget_test.go (95%) rename internal/api/{s2s/user => activitypub/users}/publickeyget.go (72%) rename internal/api/{s2s/user => activitypub/users}/repliesget.go (78%) rename internal/api/{s2s/user => activitypub/users}/repliesget_test.go (95%) rename internal/api/{s2s/user => activitypub/users}/statusget.go (69%) rename internal/api/{s2s/user => activitypub/users}/statusget_test.go (94%) create mode 100644 internal/api/activitypub/users/user.go rename internal/api/{s2s/user => activitypub/users}/user_test.go (83%) rename internal/api/{s2s/user => activitypub/users}/userget.go (74%) rename internal/api/{s2s/user => activitypub/users}/userget_test.go (95%) delete mode 100644 internal/api/apimodule.go create mode 100644 internal/api/auth.go create mode 100644 internal/api/auth/auth.go rename internal/api/{client => }/auth/auth_test.go (85%) rename internal/api/{client => }/auth/authorize.go (84%) rename internal/api/{client => }/auth/authorize_test.go (91%) rename internal/api/{client => }/auth/callback.go (83%) rename internal/api/{client => }/auth/oob.go (80%) rename internal/api/{client => }/auth/signin.go (85%) rename internal/api/{client => }/auth/token.go (87%) rename internal/api/{client => }/auth/token_test.go (100%) create mode 100644 internal/api/client.go rename internal/api/client/{account => accounts}/account_test.go (97%) rename internal/api/client/{account => accounts}/accountcreate.go (79%) rename internal/api/client/{account => accounts}/accountcreate_test.go (97%) rename internal/api/client/{account => accounts}/accountdelete.go (79%) rename internal/api/client/{account => accounts}/accountdelete_test.go (88%) rename internal/api/client/{account => accounts}/accountget.go (79%) rename internal/api/client/{account/account.go => accounts/accounts.go} (67%) rename internal/api/client/{account => accounts}/accountupdate.go (86%) rename internal/api/client/{account => accounts}/accountupdate_test.go (91%) rename internal/api/client/{account => accounts}/accountverify.go (80%) rename internal/api/client/{account => accounts}/accountverify_test.go (95%) rename internal/api/client/{account => accounts}/block.go (79%) rename internal/api/client/{account => accounts}/block_test.go (91%) rename internal/api/client/{account => accounts}/follow.go (79%) rename internal/api/client/{account => accounts}/follow_test.go (91%) rename internal/api/client/{account => accounts}/followers.go (80%) rename internal/api/client/{account => accounts}/following.go (80%) rename internal/api/client/{account => accounts}/relationships.go (73%) rename internal/api/client/{account => accounts}/statuses.go (83%) rename internal/api/client/{account => accounts}/statuses_test.go (95%) rename internal/api/client/{account => accounts}/unblock.go (80%) rename internal/api/client/{account => accounts}/unfollow.go (80%) delete mode 100644 internal/api/client/app/app_test.go rename internal/api/client/{app => apps}/appcreate.go (74%) rename internal/api/client/{app/app.go => apps/apps.go} (61%) delete mode 100644 internal/api/client/auth/auth.go create mode 100644 internal/api/client/customemojis/customemojis.go create mode 100644 internal/api/client/customemojis/customemojisget.go delete mode 100644 internal/api/client/emoji/emojisget.go rename internal/api/client/{filter => filters}/filter.go (62%) rename internal/api/client/{filter => filters}/filtersget.go (55%) rename internal/api/client/{followrequest => followrequests}/authorize.go (81%) rename internal/api/client/{followrequest => followrequests}/authorize_test.go (96%) rename internal/api/client/{followrequest => followrequests}/followrequest.go (67%) rename internal/api/client/{followrequest => followrequests}/followrequest_test.go (96%) rename internal/api/client/{followrequest => followrequests}/get.go (83%) rename internal/api/client/{followrequest => followrequests}/get_test.go (98%) rename internal/api/client/{followrequest => followrequests}/reject.go (80%) rename internal/api/client/{followrequest => followrequests}/reject_test.go (97%) delete mode 100644 internal/api/client/list/listsgets.go rename internal/api/client/{list => lists}/list.go (62%) rename internal/api/{security/robots.go => client/lists/listsgets.go} (50%) rename internal/api/client/{notification/notification.go => notifications/notifications.go} (68%) rename internal/api/client/{notification => notifications}/notificationsclear.go (80%) rename internal/api/client/{notification => notifications}/notificationsget.go (87%) rename internal/api/client/{status => statuses}/status.go (59%) rename internal/api/client/{status => statuses}/status_test.go (96%) rename internal/api/client/{status => statuses}/statusbookmark.go (80%) rename internal/api/client/{status => statuses}/statusbookmark_test.go (93%) rename internal/api/client/{status => statuses}/statusboost.go (81%) rename internal/api/client/{status => statuses}/statusboost_test.go (91%) rename internal/api/client/{status => statuses}/statusboostedby.go (85%) rename internal/api/client/{status => statuses}/statusboostedby_test.go (92%) rename internal/api/client/{status => statuses}/statuscontext.go (81%) rename internal/api/client/{status => statuses}/statuscreate.go (84%) rename internal/api/client/{status => statuses}/statuscreate_test.go (91%) rename internal/api/client/{status => statuses}/statusdelete.go (81%) rename internal/api/client/{status => statuses}/statusdelete_test.go (90%) rename internal/api/client/{status => statuses}/statusfave.go (80%) rename internal/api/client/{status => statuses}/statusfave_test.go (88%) rename internal/api/client/{status => statuses}/statusfavedby.go (80%) rename internal/api/client/{status => statuses}/statusfavedby_test.go (90%) rename internal/api/client/{status => statuses}/statusget.go (79%) rename internal/api/client/{status => statuses}/statusget_test.go (97%) rename internal/api/client/{status => statuses}/statusunbookmark.go (80%) rename internal/api/client/{status => statuses}/statusunbookmark_test.go (92%) rename internal/api/client/{status => statuses}/statusunboost.go (80%) rename internal/api/client/{status => statuses}/statusunfave.go (80%) rename internal/api/client/{status => statuses}/statusunfave_test.go (87%) rename internal/api/client/{timeline => timelines}/home.go (86%) rename internal/api/client/{timeline => timelines}/public.go (87%) rename internal/api/client/{timeline => timelines}/timeline.go (70%) create mode 100644 internal/api/fileserver.go rename internal/api/{client => }/fileserver/fileserver.go (57%) rename internal/api/{client => }/fileserver/fileserver_test.go (89%) rename internal/api/{client => }/fileserver/servefile.go (75%) rename internal/api/{client => }/fileserver/servefile_test.go (99%) create mode 100644 internal/api/nodeinfo.go create mode 100644 internal/api/nodeinfo/nodeinfo.go rename internal/api/{s2s => }/nodeinfo/nodeinfoget.go (65%) delete mode 100644 internal/api/s2s/emoji/emoji.go delete mode 100644 internal/api/s2s/nodeinfo/nodeinfo.go delete mode 100644 internal/api/s2s/user/user.go delete mode 100644 internal/api/security/flocblock.go delete mode 100644 internal/api/security/ratelimit.go delete mode 100644 internal/api/security/security.go delete mode 100644 internal/api/security/signaturecheck.go delete mode 100644 internal/api/security/tokencheck.go rename internal/api/{ => util}/errorhandling.go (99%) rename internal/api/{ => util}/mime.go (99%) rename internal/api/{ => util}/negotiate.go (99%) create mode 100644 internal/api/util/signaturectx.go create mode 100644 internal/api/wellknown.go create mode 100644 internal/api/wellknown/nodeinfo/nodeinfo.go rename internal/api/{s2s/nodeinfo/wellknownget.go => wellknown/nodeinfo/nodeinfoget.go} (73%) rename internal/api/{s2s => wellknown}/webfinger/webfinger.go (62%) rename internal/api/{s2s => wellknown}/webfinger/webfinger_test.go (89%) rename internal/api/{s2s => wellknown}/webfinger/webfingerget.go (59%) rename internal/api/{s2s => wellknown}/webfinger/webfingerget_test.go (97%) rename internal/{api/security/useragentblock.go => middleware/cachecontrol.go} (65%) create mode 100644 internal/middleware/cors.go rename internal/{api/security => middleware}/extraheaders.go (52%) rename internal/{router => middleware}/gzip.go (80%) create mode 100644 internal/middleware/logger.go create mode 100644 internal/middleware/ratelimit.go rename internal/{router => middleware}/session.go (82%) rename internal/{router => middleware}/session_test.go (86%) create mode 100644 internal/middleware/signaturecheck.go create mode 100644 internal/middleware/tokencheck.go rename internal/{api/client/auth/util.go => middleware/useragent.go} (59%) delete mode 100644 internal/router/cors.go delete mode 100644 internal/router/logger.go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6643713ea..ff8794c6a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -251,7 +251,7 @@ Most of the crucial business logic of the application is found inside the variou `internal/ap` - ActivityPub utility functions and interfaces. -`internal/api` - Models, routers, and utilities for the client and federated (s2s) APIs. This is where routes are attached to the router, and where you want to be if you're adding a route. +`internal/api` - Models, routers, and utilities for the client and federated (ActivityPub) APIs. This is where routes are attached to the router, and where you want to be if you're adding a route. `internal/concurrency` - Worker models used by the processor and other queues. @@ -283,6 +283,8 @@ Most of the crucial business logic of the application is found inside the variou `internal/messages` - Models for wrapping worker messages. +`internal/middleware` - Gin Gonic router middlewares: http signature checking, cache control, token checks, etc. + `internal/netutil` - HTTP / net request validation code. `internal/oauth` - Wrapper code/interfaces for OAuth server implementation. diff --git a/cmd/gotosocial/action/server/server.go b/cmd/gotosocial/action/server/server.go index fbb22bdcf..7e07b76ab 100644 --- a/cmd/gotosocial/action/server/server.go +++ b/cmd/gotosocial/action/server/server.go @@ -20,37 +20,20 @@ package server import ( "context" + "errors" "fmt" + "net/http" "os" "os/signal" "syscall" + "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/cmd/gotosocial/action" "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/api/client/account" - "github.com/superseriousbusiness/gotosocial/internal/api/client/admin" - "github.com/superseriousbusiness/gotosocial/internal/api/client/app" - "github.com/superseriousbusiness/gotosocial/internal/api/client/auth" - "github.com/superseriousbusiness/gotosocial/internal/api/client/blocks" - "github.com/superseriousbusiness/gotosocial/internal/api/client/bookmarks" - "github.com/superseriousbusiness/gotosocial/internal/api/client/emoji" - "github.com/superseriousbusiness/gotosocial/internal/api/client/favourites" - "github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver" - "github.com/superseriousbusiness/gotosocial/internal/api/client/filter" - "github.com/superseriousbusiness/gotosocial/internal/api/client/followrequest" - "github.com/superseriousbusiness/gotosocial/internal/api/client/instance" - "github.com/superseriousbusiness/gotosocial/internal/api/client/list" - mediaModule "github.com/superseriousbusiness/gotosocial/internal/api/client/media" - "github.com/superseriousbusiness/gotosocial/internal/api/client/notification" - "github.com/superseriousbusiness/gotosocial/internal/api/client/search" - "github.com/superseriousbusiness/gotosocial/internal/api/client/status" - "github.com/superseriousbusiness/gotosocial/internal/api/client/streaming" - "github.com/superseriousbusiness/gotosocial/internal/api/client/timeline" - userClient "github.com/superseriousbusiness/gotosocial/internal/api/client/user" - "github.com/superseriousbusiness/gotosocial/internal/api/s2s/nodeinfo" - "github.com/superseriousbusiness/gotosocial/internal/api/s2s/user" - "github.com/superseriousbusiness/gotosocial/internal/api/s2s/webfinger" - "github.com/superseriousbusiness/gotosocial/internal/api/security" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/middleware" + "github.com/superseriousbusiness/gotosocial/internal/concurrency" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db/bundb" @@ -106,11 +89,6 @@ var Start action.GTSAction = func(ctx context.Context) error { federatingDB := federatingdb.New(dbService, fedWorker) - router, err := router.New(ctx, dbService) - if err != nil { - return fmt.Errorf("error creating router: %s", err) - } - // build converters and util typeConverter := typeutils.NewConverter(dbService) @@ -148,85 +126,72 @@ var Start action.GTSAction = func(ctx context.Context) error { } } - // create and start the message processor using the other services we've created so far + // create the message processor using the other services we've created so far processor := processing.NewProcessor(typeConverter, federator, oauthServer, mediaManager, storage, dbService, emailSender, clientWorker, fedWorker) if err := processor.Start(); err != nil { - return fmt.Errorf("error starting processor: %s", err) + return fmt.Errorf("error creating processor: %s", err) } - idp, err := oidc.NewIDP(ctx) + /* + HTTP router initialization + */ + + router, err := router.New(ctx) if err != nil { - return fmt.Errorf("error creating oidc idp: %s", err) + return fmt.Errorf("error creating router: %s", err) } - // build web module - webModule := web.New(processor) + // attach global middlewares which are used for every request + router.AttachGlobalMiddleware( + middleware.Logger(), + middleware.UserAgent(), + middleware.CORS(), + middleware.ExtraHeaders(), + ) - // build client api modules - authModule := auth.New(dbService, idp, processor) - accountModule := account.New(processor) - instanceModule := instance.New(processor) - appsModule := app.New(processor) - followRequestsModule := followrequest.New(processor) - webfingerModule := webfinger.New(processor) - nodeInfoModule := nodeinfo.New(processor) - usersModule := user.New(processor) - timelineModule := timeline.New(processor) - notificationModule := notification.New(processor) - searchModule := search.New(processor) - filtersModule := filter.New(processor) - emojiModule := emoji.New(processor) - listsModule := list.New(processor) - mm := mediaModule.New(processor) - fileServerModule := fileserver.New(processor) - adminModule := admin.New(processor) - statusModule := status.New(processor) - bookmarksModule := bookmarks.New(processor) - securityModule := security.New(dbService, oauthServer) - streamingModule := streaming.New(processor) - favouritesModule := favourites.New(processor) - blocksModule := blocks.New(processor) - userClientModule := userClient.New(processor) + // attach global no route / 404 handler to the router + router.AttachNoRouteHandler(func(c *gin.Context) { + apiutil.ErrorHandler(c, gtserror.NewErrorNotFound(errors.New(http.StatusText(http.StatusNotFound))), processor.InstanceGet) + }) - apis := []api.ClientModule{ - // modules with middleware go first - securityModule, - authModule, - - // now the web module - webModule, - - // now everything else - accountModule, - instanceModule, - appsModule, - followRequestsModule, - mm, - fileServerModule, - adminModule, - statusModule, - bookmarksModule, - webfingerModule, - nodeInfoModule, - usersModule, - timelineModule, - notificationModule, - searchModule, - filtersModule, - emojiModule, - listsModule, - streamingModule, - favouritesModule, - blocksModule, - userClientModule, - } - - for _, m := range apis { - if err := m.Route(router); err != nil { - return fmt.Errorf("routing error: %s", err) + // build router modules + var idp oidc.IDP + if config.GetOIDCEnabled() { + idp, err = oidc.NewIDP(ctx) + if err != nil { + return fmt.Errorf("error creating oidc idp: %w", err) } } + routerSession, err := dbService.GetSession(ctx) + if err != nil { + return fmt.Errorf("error retrieving router session for session middleware: %w", err) + } + + sessionName, err := middleware.SessionName() + if err != nil { + return fmt.Errorf("error generating session name for session middleware: %w", err) + } + + var ( + authModule = api.NewAuth(dbService, processor, idp, routerSession, sessionName) // auth/oauth paths + clientModule = api.NewClient(dbService, processor) // api client endpoints + fileserverModule = api.NewFileserver(processor) // fileserver endpoints + wellKnownModule = api.NewWellKnown(processor) // .well-known endpoints + nodeInfoModule = api.NewNodeInfo(processor) // nodeinfo endpoint + activityPubModule = api.NewActivityPub(dbService, processor) // ActivityPub endpoints + webModule = web.New(processor) // web pages + user profiles + settings panels etc + ) + + // these should be routed in order + authModule.Route(router) + clientModule.Route(router) + fileserverModule.Route(router) + wellKnownModule.Route(router) + nodeInfoModule.Route(router) + activityPubModule.Route(router) + webModule.Route(router) + gts, err := gotosocial.NewServer(dbService, router, federator, mediaManager) if err != nil { return fmt.Errorf("error creating gotosocial service: %s", err) diff --git a/cmd/gotosocial/action/testrig/testrig.go b/cmd/gotosocial/action/testrig/testrig.go index 88ac369de..f13845b20 100644 --- a/cmd/gotosocial/action/testrig/testrig.go +++ b/cmd/gotosocial/action/testrig/testrig.go @@ -21,6 +21,7 @@ package testrig import ( "bytes" "context" + "errors" "fmt" "io" "net/http" @@ -28,35 +29,17 @@ import ( "os/signal" "syscall" + "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/cmd/gotosocial/action" "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/api/client/account" - "github.com/superseriousbusiness/gotosocial/internal/api/client/admin" - "github.com/superseriousbusiness/gotosocial/internal/api/client/app" - "github.com/superseriousbusiness/gotosocial/internal/api/client/auth" - "github.com/superseriousbusiness/gotosocial/internal/api/client/blocks" - "github.com/superseriousbusiness/gotosocial/internal/api/client/emoji" - "github.com/superseriousbusiness/gotosocial/internal/api/client/favourites" - "github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver" - "github.com/superseriousbusiness/gotosocial/internal/api/client/filter" - "github.com/superseriousbusiness/gotosocial/internal/api/client/followrequest" - "github.com/superseriousbusiness/gotosocial/internal/api/client/instance" - "github.com/superseriousbusiness/gotosocial/internal/api/client/list" - mediaModule "github.com/superseriousbusiness/gotosocial/internal/api/client/media" - "github.com/superseriousbusiness/gotosocial/internal/api/client/notification" - "github.com/superseriousbusiness/gotosocial/internal/api/client/search" - "github.com/superseriousbusiness/gotosocial/internal/api/client/status" - "github.com/superseriousbusiness/gotosocial/internal/api/client/streaming" - "github.com/superseriousbusiness/gotosocial/internal/api/client/timeline" - userClient "github.com/superseriousbusiness/gotosocial/internal/api/client/user" - "github.com/superseriousbusiness/gotosocial/internal/api/s2s/nodeinfo" - "github.com/superseriousbusiness/gotosocial/internal/api/s2s/user" - "github.com/superseriousbusiness/gotosocial/internal/api/s2s/webfinger" - "github.com/superseriousbusiness/gotosocial/internal/api/security" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/concurrency" + "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gotosocial" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/messages" + "github.com/superseriousbusiness/gotosocial/internal/middleware" "github.com/superseriousbusiness/gotosocial/internal/oidc" "github.com/superseriousbusiness/gotosocial/internal/storage" "github.com/superseriousbusiness/gotosocial/internal/web" @@ -70,7 +53,6 @@ var Start action.GTSAction = func(ctx context.Context) error { dbService := testrig.NewTestDB() testrig.StandardDBSetup(dbService, nil) - router := testrig.NewTestRouter(dbService) var storageBackend *storage.Driver if os.Getenv("GTS_STORAGE_BACKEND") == "s3" { storageBackend, _ = storage.NewS3Storage() @@ -84,7 +66,6 @@ var Start action.GTSAction = func(ctx context.Context) error { fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) // build backend handlers - oauthServer := testrig.NewTestOauthServer(dbService) transportController := testrig.NewTestTransportController(testrig.NewMockHTTPClient(func(req *http.Request) (*http.Response, error) { r := io.NopCloser(bytes.NewReader([]byte{})) return &http.Response{ @@ -102,77 +83,64 @@ var Start action.GTSAction = func(ctx context.Context) error { return fmt.Errorf("error starting processor: %s", err) } - idp, err := oidc.NewIDP(ctx) - if err != nil { - return fmt.Errorf("error creating oidc idp: %s", err) - } + /* + HTTP router initialization + */ - // build web module - webModule := web.New(processor) + router := testrig.NewTestRouter(dbService) - // build client api modules - authModule := auth.New(dbService, idp, processor) - accountModule := account.New(processor) - instanceModule := instance.New(processor) - appsModule := app.New(processor) - followRequestsModule := followrequest.New(processor) - webfingerModule := webfinger.New(processor) - nodeInfoModule := nodeinfo.New(processor) - usersModule := user.New(processor) - timelineModule := timeline.New(processor) - notificationModule := notification.New(processor) - searchModule := search.New(processor) - filtersModule := filter.New(processor) - emojiModule := emoji.New(processor) - listsModule := list.New(processor) - mm := mediaModule.New(processor) - fileServerModule := fileserver.New(processor) - adminModule := admin.New(processor) - statusModule := status.New(processor) - securityModule := security.New(dbService, oauthServer) - streamingModule := streaming.New(processor) - favouritesModule := favourites.New(processor) - blocksModule := blocks.New(processor) - userClientModule := userClient.New(processor) + // attach global middlewares which are used for every request + router.AttachGlobalMiddleware( + middleware.Logger(), + middleware.UserAgent(), + middleware.CORS(), + middleware.ExtraHeaders(), + ) - apis := []api.ClientModule{ - // modules with middleware go first - securityModule, - authModule, + // attach global no route / 404 handler to the router + router.AttachNoRouteHandler(func(c *gin.Context) { + apiutil.ErrorHandler(c, gtserror.NewErrorNotFound(errors.New(http.StatusText(http.StatusNotFound))), processor.InstanceGet) + }) - // now the web module - webModule, - - // now everything else - accountModule, - instanceModule, - appsModule, - followRequestsModule, - mm, - fileServerModule, - adminModule, - statusModule, - webfingerModule, - nodeInfoModule, - usersModule, - timelineModule, - notificationModule, - searchModule, - filtersModule, - emojiModule, - listsModule, - streamingModule, - favouritesModule, - blocksModule, - userClientModule, - } - - for _, m := range apis { - if err := m.Route(router); err != nil { - return fmt.Errorf("routing error: %s", err) + // build router modules + var idp oidc.IDP + var err error + if config.GetOIDCEnabled() { + idp, err = oidc.NewIDP(ctx) + if err != nil { + return fmt.Errorf("error creating oidc idp: %w", err) } } + routerSession, err := dbService.GetSession(ctx) + if err != nil { + return fmt.Errorf("error retrieving router session for session middleware: %w", err) + } + + sessionName, err := middleware.SessionName() + if err != nil { + return fmt.Errorf("error generating session name for session middleware: %w", err) + } + + var ( + authModule = api.NewAuth(dbService, processor, idp, routerSession, sessionName) // auth/oauth paths + clientModule = api.NewClient(dbService, processor) // api client endpoints + fileserverModule = api.NewFileserver(processor) // fileserver endpoints + wellKnownModule = api.NewWellKnown(processor) // .well-known endpoints + nodeInfoModule = api.NewNodeInfo(processor) // nodeinfo endpoint + activityPubModule = api.NewActivityPub(dbService, processor) // ActivityPub endpoints + webModule = web.New(processor) // web pages + user profiles + settings panels etc + ) + + // these should be routed in order + authModule.Route(router) + clientModule.Route(router) + fileserverModule.Route(router) + wellKnownModule.Route(router) + nodeInfoModule.Route(router) + activityPubModule.Route(router) + webModule.Route(router) + gts, err := gotosocial.NewServer(dbService, router, federator, mediaManager) if err != nil { return fmt.Errorf("error creating gotosocial service: %s", err) diff --git a/docs/api/ratelimiting.md b/docs/api/ratelimiting.md index a9ca07390..09b554093 100644 --- a/docs/api/ratelimiting.md +++ b/docs/api/ratelimiting.md @@ -1,10 +1,16 @@ # Rate Limit -To mitigate abuse + scraping of your instance, an IP-based HTTP rate limit is in place. +To mitigate abuse + scraping of your instance, IP-based HTTP rate limiting is in place. -This rate limit applies not just to the API, but to all requests (web, federation, etc). +There are separate rate limiters configured for different groupings of endpoints. In other words, being rate limited for one part of the API doesn't necessarily mean you will be rate limited for other parts. Each entry in the following list has a separate rate limiter: -By default, a maximum of 1000 requests in a 5 minute time window are allowed. +- `/users/*` and `/emoji/*` - ActivityPub (s2s) endpoints. +- `/auth/*` and `/oauth/*` - Sign in + OAUTH token requests. +- `/fileserver/*` - Media attachments, emojis, etc. +- `/nodeinfo/*` - NodeInfo endpoint(s). +- `/.well-known/*` - webfinger + nodeinfo requests. + +By default, each rate limiter allows a maximum of 300 requests in a 5 minute time window: 1 request per second per client IP address. Every response will include the current status of the rate limit with the following headers: diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml index b790cb71b..a900a31d5 100644 --- a/docs/api/swagger.yaml +++ b/docs/api/swagger.yaml @@ -1914,7 +1914,7 @@ definitions: title: SwaggerCollection represents an activitypub collection. type: object x-go-name: SwaggerCollection - x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/s2s/user + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users swaggerCollectionPage: properties: id: @@ -1949,7 +1949,7 @@ definitions: title: SwaggerCollectionPage represents one page of a collection. type: object x-go-name: SwaggerCollectionPage - x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/s2s/user + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users tag: properties: name: @@ -2049,9 +2049,9 @@ paths: description: "" schema: $ref: '#/definitions/wellKnownResponse' - summary: Directs callers to /nodeinfo/2.0. + summary: Returns a well-known response which redirects callers to `/nodeinfo/2.0`. tags: - - nodeinfo + - .well-known /.well-known/webfinger: get: description: |- @@ -2074,7 +2074,7 @@ paths: $ref: '#/definitions/wellKnownResponse' summary: Handles webfinger account lookup requests. tags: - - webfinger + - .well-known /api/{api_version}/media: post: consumes: diff --git a/example/config.yaml b/example/config.yaml index 37ee84c09..03c9c74cd 100644 --- a/example/config.yaml +++ b/example/config.yaml @@ -641,9 +641,11 @@ syslog-address: "localhost:514" # Default: "lax" advanced-cookies-samesite: "lax" -# Int. Amount of requests to permit from a single IP address within a span of 5 minutes. -# If this amount is exceeded, a 429 HTTP error code will be returned. -# See https://docs.gotosocial.org/en/latest/api/swagger/#rate-limit. +# Int. Amount of requests to permit per router grouping from a single IP address within +# a span of 5 minutes. If this amount is exceeded, a 429 HTTP error code will be returned. +# +# Router groupings and rate limit headers are described here: +# https://docs.gotosocial.org/en/latest/api/swagger/#rate-limit. # # If you find yourself adjusting this limit because it's regularly being exceeded, # you should first verify that your settings for `trusted-proxies` (above) are correct. @@ -655,5 +657,5 @@ advanced-cookies-samesite: "lax" # If you set this to 0 or less, rate limiting will be disabled entirely. # # Examples: [1000, 500, 0] -# Default: 1000 -advanced-rate-limit-requests: 1000 +# Default: 300 +advanced-rate-limit-requests: 300 diff --git a/internal/api/activitypub.go b/internal/api/activitypub.go new file mode 100644 index 000000000..68c3b81e0 --- /dev/null +++ b/internal/api/activitypub.go @@ -0,0 +1,66 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + 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 api + +import ( + "context" + "net/url" + + "github.com/superseriousbusiness/gotosocial/internal/api/activitypub/emoji" + "github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/middleware" + "github.com/superseriousbusiness/gotosocial/internal/processing" + "github.com/superseriousbusiness/gotosocial/internal/router" +) + +type ActivityPub struct { + emoji *emoji.Module + users *users.Module + + isURIBlocked func(context.Context, *url.URL) (bool, db.Error) +} + +func (a *ActivityPub) Route(r router.Router) { + // create groupings for the 'emoji' and 'users' prefixes + emojiGroup := r.AttachGroup("emoji") + usersGroup := r.AttachGroup("users") + + // instantiate + attach shared, non-global middlewares to both of these groups + var ( + rateLimitMiddleware = middleware.RateLimit() // nolint:contextcheck + signatureCheckMiddleware = middleware.SignatureCheck(a.isURIBlocked) + gzipMiddleware = middleware.Gzip() + cacheControlMiddleware = middleware.CacheControl("no-store") + ) + emojiGroup.Use(rateLimitMiddleware, signatureCheckMiddleware, gzipMiddleware, cacheControlMiddleware) + usersGroup.Use(rateLimitMiddleware, signatureCheckMiddleware, gzipMiddleware, cacheControlMiddleware) + + a.emoji.Route(emojiGroup.Handle) + a.users.Route(usersGroup.Handle) +} + +func NewActivityPub(db db.DB, p processing.Processor) *ActivityPub { + return &ActivityPub{ + emoji: emoji.New(p), + users: users.New(p), + + isURIBlocked: db.IsURIBlocked, + } +} diff --git a/internal/api/client/emoji/emoji.go b/internal/api/activitypub/emoji/emoji.go similarity index 62% rename from internal/api/client/emoji/emoji.go rename to internal/api/activitypub/emoji/emoji.go index 871a12854..f0eab0f02 100644 --- a/internal/api/client/emoji/emoji.go +++ b/internal/api/activitypub/emoji/emoji.go @@ -21,30 +21,27 @@ package emoji import ( "net/http" - "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" ) const ( - // BasePath is the base path for serving the emoji API - BasePath = "/api/v1/custom_emojis" + // EmojiIDKey is for emoji IDs + EmojiIDKey = "id" + // EmojiBasePath is the base path for serving AP Emojis, minus the "emoji" prefix + EmojiWithIDPath = "/:" + EmojiIDKey ) -// Module implements the ClientAPIModule interface for everything related to emoji type Module struct { processor processing.Processor } -// New returns a new emoji module -func New(processor processing.Processor) api.ClientModule { +func New(processor processing.Processor) *Module { return &Module{ processor: processor, } } -// Route attaches all routes from this module to the given router -func (m *Module) Route(r router.Router) error { - r.AttachHandler(http.MethodGet, BasePath, m.EmojisGETHandler) - return nil +func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { + attachHandler(http.MethodGet, EmojiWithIDPath, m.EmojiGetHandler) } diff --git a/internal/api/s2s/emoji/emojiget.go b/internal/api/activitypub/emoji/emojiget.go similarity index 54% rename from internal/api/s2s/emoji/emojiget.go rename to internal/api/activitypub/emoji/emojiget.go index 28a737f9a..4e5ca25f4 100644 --- a/internal/api/s2s/emoji/emojiget.go +++ b/internal/api/activitypub/emoji/emojiget.go @@ -19,54 +19,39 @@ package emoji import ( - "context" "encoding/json" "errors" "net/http" "strings" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/ap" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) -// EmojiGetHandler func (m *Module) EmojiGetHandler(c *gin.Context) { - // usernames on our instance are always lowercase requestedEmojiID := strings.ToUpper(c.Param(EmojiIDKey)) if requestedEmojiID == "" { err := errors.New("no emoji id specified in request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...) + format, err := apiutil.NegotiateAccept(c, apiutil.ActivityPubAcceptHeaders...) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - ctx := c.Request.Context() - verifier, signed := c.Get(string(ap.ContextRequestingPublicKeyVerifier)) - if signed { - ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeyVerifier, verifier) - } - - signature, signed := c.Get(string(ap.ContextRequestingPublicKeySignature)) - if signed { - ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeySignature, signature) - } - - resp, errWithCode := m.processor.GetFediEmoji(ctx, requestedEmojiID, c.Request.URL) + resp, errWithCode := m.processor.GetFediEmoji(apiutil.TransferSignatureContext(c), requestedEmojiID, c.Request.URL) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } b, err := json.Marshal(resp) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } diff --git a/internal/api/s2s/emoji/emojiget_test.go b/internal/api/activitypub/emoji/emojiget_test.go similarity index 86% rename from internal/api/s2s/emoji/emojiget_test.go rename to internal/api/activitypub/emoji/emojiget_test.go index 16bc9dc1a..5b06f7a33 100644 --- a/internal/api/s2s/emoji/emojiget_test.go +++ b/internal/api/activitypub/emoji/emojiget_test.go @@ -26,8 +26,7 @@ import ( "github.com/gin-gonic/gin" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/s2s/emoji" - "github.com/superseriousbusiness/gotosocial/internal/api/security" + "github.com/superseriousbusiness/gotosocial/internal/api/activitypub/emoji" "github.com/superseriousbusiness/gotosocial/internal/concurrency" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/email" @@ -35,7 +34,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/messages" - "github.com/superseriousbusiness/gotosocial/internal/oauth" + "github.com/superseriousbusiness/gotosocial/internal/middleware" "github.com/superseriousbusiness/gotosocial/internal/processing" "github.com/superseriousbusiness/gotosocial/internal/storage" "github.com/superseriousbusiness/gotosocial/internal/typeutils" @@ -44,20 +43,20 @@ import ( type EmojiGetTestSuite struct { suite.Suite - db db.DB - tc typeutils.TypeConverter - mediaManager media.Manager - federator federation.Federator - emailSender email.Sender - processor processing.Processor - storage *storage.Driver - oauthServer oauth.Server - securityModule *security.Module + db db.DB + tc typeutils.TypeConverter + mediaManager media.Manager + federator federation.Federator + emailSender email.Sender + processor processing.Processor + storage *storage.Driver testEmojis map[string]*gtsmodel.Emoji testAccounts map[string]*gtsmodel.Account emojiModule *emoji.Module + + signatureCheck gin.HandlerFunc } func (suite *EmojiGetTestSuite) SetupSuite() { @@ -79,12 +78,12 @@ func (suite *EmojiGetTestSuite) SetupTest() { suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker), suite.storage, suite.mediaManager, fedWorker) suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil) suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker) - suite.emojiModule = emoji.New(suite.processor).(*emoji.Module) - suite.oauthServer = testrig.NewTestOauthServer(suite.db) - suite.securityModule = security.New(suite.db, suite.oauthServer).(*security.Module) + suite.emojiModule = emoji.New(suite.processor) testrig.StandardDBSetup(suite.db, suite.testAccounts) testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") + suite.signatureCheck = middleware.SignatureCheck(suite.db.IsURIBlocked) + suite.NoError(suite.processor.Start()) } @@ -108,7 +107,7 @@ func (suite *EmojiGetTestSuite) TestGetEmoji() { ctx.Request.Header.Set("Date", signedRequest.DateHeader) // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) + suite.signatureCheck(ctx) // normally the router would populate these params from the path values, // but because we're calling the function directly, we need to set them manually. diff --git a/internal/api/s2s/user/common.go b/internal/api/activitypub/users/common.go similarity index 77% rename from internal/api/s2s/user/common.go rename to internal/api/activitypub/users/common.go index 0a215ebbd..bf7623774 100644 --- a/internal/api/s2s/user/common.go +++ b/internal/api/activitypub/users/common.go @@ -16,31 +16,7 @@ along with this program. If not, see . */ -package user - -import ( - "context" - - "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/ap" -) - -// transferContext transfers the signature verifier and signature from the gin context to the request context -func transferContext(c *gin.Context) context.Context { - ctx := c.Request.Context() - - verifier, signed := c.Get(string(ap.ContextRequestingPublicKeyVerifier)) - if signed { - ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeyVerifier, verifier) - } - - signature, signed := c.Get(string(ap.ContextRequestingPublicKeySignature)) - if signed { - ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeySignature, signature) - } - - return ctx -} +package users // SwaggerCollection represents an activitypub collection. // swagger:model swaggerCollection diff --git a/internal/api/s2s/user/followers.go b/internal/api/activitypub/users/followers.go similarity index 69% rename from internal/api/s2s/user/followers.go rename to internal/api/activitypub/users/followers.go index 675688311..69b9f52fd 100644 --- a/internal/api/s2s/user/followers.go +++ b/internal/api/activitypub/users/followers.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package user +package users import ( "encoding/json" @@ -25,7 +25,7 @@ import ( "strings" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) @@ -35,31 +35,31 @@ func (m *Module) FollowersGETHandler(c *gin.Context) { requestedUsername := strings.ToLower(c.Param(UsernameKey)) if requestedUsername == "" { err := errors.New("no username specified in request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) + format, err := apiutil.NegotiateAccept(c, apiutil.HTMLOrActivityPubHeaders...) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - if format == string(api.TextHTML) { + if format == string(apiutil.TextHTML) { // redirect to the user's profile c.Redirect(http.StatusSeeOther, "/@"+requestedUsername) return } - resp, errWithCode := m.processor.GetFediFollowers(transferContext(c), requestedUsername, c.Request.URL) + resp, errWithCode := m.processor.GetFediFollowers(apiutil.TransferSignatureContext(c), requestedUsername, c.Request.URL) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } b, err := json.Marshal(resp) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } diff --git a/internal/api/s2s/user/following.go b/internal/api/activitypub/users/following.go similarity index 70% rename from internal/api/s2s/user/following.go rename to internal/api/activitypub/users/following.go index d4404ea08..64337d4cd 100644 --- a/internal/api/s2s/user/following.go +++ b/internal/api/activitypub/users/following.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package user +package users import ( "encoding/json" @@ -25,7 +25,7 @@ import ( "strings" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) @@ -35,31 +35,31 @@ func (m *Module) FollowingGETHandler(c *gin.Context) { requestedUsername := strings.ToLower(c.Param(UsernameKey)) if requestedUsername == "" { err := errors.New("no username specified in request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) + format, err := apiutil.NegotiateAccept(c, apiutil.HTMLOrActivityPubHeaders...) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - if format == string(api.TextHTML) { + if format == string(apiutil.TextHTML) { // redirect to the user's profile c.Redirect(http.StatusSeeOther, "/@"+requestedUsername) return } - resp, errWithCode := m.processor.GetFediFollowing(transferContext(c), requestedUsername, c.Request.URL) + resp, errWithCode := m.processor.GetFediFollowing(apiutil.TransferSignatureContext(c), requestedUsername, c.Request.URL) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } b, err := json.Marshal(resp) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } diff --git a/internal/api/s2s/user/inboxpost.go b/internal/api/activitypub/users/inboxpost.go similarity index 72% rename from internal/api/s2s/user/inboxpost.go rename to internal/api/activitypub/users/inboxpost.go index fa6792cd2..1d8f9f923 100644 --- a/internal/api/s2s/user/inboxpost.go +++ b/internal/api/activitypub/users/inboxpost.go @@ -16,14 +16,14 @@ along with this program. If not, see . */ -package user +package users import ( "errors" "strings" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" //nolint:typecheck ) @@ -34,18 +34,18 @@ func (m *Module) InboxPOSTHandler(c *gin.Context) { requestedUsername := strings.ToLower(c.Param(UsernameKey)) if requestedUsername == "" { err := errors.New("no username specified in request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - if posted, err := m.processor.InboxPost(transferContext(c), c.Writer, c.Request); err != nil { + if posted, err := m.processor.InboxPost(apiutil.TransferSignatureContext(c), c.Writer, c.Request); err != nil { if withCode, ok := err.(gtserror.WithCode); ok { - api.ErrorHandler(c, withCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, withCode, m.processor.InstanceGet) } else { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) } } else if !posted { err := errors.New("unable to process request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) } } diff --git a/internal/api/s2s/user/inboxpost_test.go b/internal/api/activitypub/users/inboxpost_test.go similarity index 97% rename from internal/api/s2s/user/inboxpost_test.go rename to internal/api/activitypub/users/inboxpost_test.go index 3821406be..53b6b8aac 100644 --- a/internal/api/s2s/user/inboxpost_test.go +++ b/internal/api/activitypub/users/inboxpost_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package user_test +package users_test import ( "bytes" @@ -32,7 +32,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/activity/pub" "github.com/superseriousbusiness/activity/streams" - "github.com/superseriousbusiness/gotosocial/internal/api/s2s/user" + "github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users" "github.com/superseriousbusiness/gotosocial/internal/concurrency" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" @@ -92,7 +92,7 @@ func (suite *InboxPostTestSuite) TestPostBlock() { federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager, fedWorker) emailSender := testrig.NewEmailSender("../../../../web/template/", nil) processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker) - userModule := user.New(processor).(*user.Module) + userModule := users.New(processor) suite.NoError(processor.Start()) // setup request @@ -105,13 +105,13 @@ func (suite *InboxPostTestSuite) TestPostBlock() { ctx.Request.Header.Set("Content-Type", "application/activity+json") // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) + suite.signatureCheck(ctx) // normally the router would populate these params from the path values, // but because we're calling the function directly, we need to set them manually. ctx.Params = gin.Params{ gin.Param{ - Key: user.UsernameKey, + Key: users.UsernameKey, Value: blockedAccount.Username, }, } @@ -196,7 +196,7 @@ func (suite *InboxPostTestSuite) TestPostUnblock() { federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager, fedWorker) emailSender := testrig.NewEmailSender("../../../../web/template/", nil) processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker) - userModule := user.New(processor).(*user.Module) + userModule := users.New(processor) suite.NoError(processor.Start()) // setup request @@ -209,13 +209,13 @@ func (suite *InboxPostTestSuite) TestPostUnblock() { ctx.Request.Header.Set("Content-Type", "application/activity+json") // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) + suite.signatureCheck(ctx) // normally the router would populate these params from the path values, // but because we're calling the function directly, we need to set them manually. ctx.Params = gin.Params{ gin.Param{ - Key: user.UsernameKey, + Key: users.UsernameKey, Value: blockedAccount.Username, }, } @@ -298,7 +298,7 @@ func (suite *InboxPostTestSuite) TestPostUpdate() { federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager, fedWorker) emailSender := testrig.NewEmailSender("../../../../web/template/", nil) processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker) - userModule := user.New(processor).(*user.Module) + userModule := users.New(processor) suite.NoError(processor.Start()) // setup request @@ -311,13 +311,13 @@ func (suite *InboxPostTestSuite) TestPostUpdate() { ctx.Request.Header.Set("Content-Type", "application/activity+json") // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) + suite.signatureCheck(ctx) // normally the router would populate these params from the path values, // but because we're calling the function directly, we need to set them manually. ctx.Params = gin.Params{ gin.Param{ - Key: user.UsernameKey, + Key: users.UsernameKey, Value: receivingAccount.Username, }, } @@ -431,7 +431,7 @@ func (suite *InboxPostTestSuite) TestPostDelete() { emailSender := testrig.NewEmailSender("../../../../web/template/", nil) processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker) suite.NoError(processor.Start()) - userModule := user.New(processor).(*user.Module) + userModule := users.New(processor) // setup request recorder := httptest.NewRecorder() @@ -443,13 +443,13 @@ func (suite *InboxPostTestSuite) TestPostDelete() { ctx.Request.Header.Set("Content-Type", "application/activity+json") // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) + suite.signatureCheck(ctx) // normally the router would populate these params from the path values, // but because we're calling the function directly, we need to set them manually. ctx.Params = gin.Params{ gin.Param{ - Key: user.UsernameKey, + Key: users.UsernameKey, Value: receivingAccount.Username, }, } diff --git a/internal/api/s2s/user/outboxget.go b/internal/api/activitypub/users/outboxget.go similarity index 79% rename from internal/api/s2s/user/outboxget.go rename to internal/api/activitypub/users/outboxget.go index 726f86237..8e5d1a751 100644 --- a/internal/api/s2s/user/outboxget.go +++ b/internal/api/activitypub/users/outboxget.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package user +package users import ( "encoding/json" @@ -27,7 +27,7 @@ import ( "strings" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) @@ -90,17 +90,17 @@ func (m *Module) OutboxGETHandler(c *gin.Context) { requestedUsername := strings.ToLower(c.Param(UsernameKey)) if requestedUsername == "" { err := errors.New("no username specified in request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) + format, err := apiutil.NegotiateAccept(c, apiutil.HTMLOrActivityPubHeaders...) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - if format == string(api.TextHTML) { + if format == string(apiutil.TextHTML) { // redirect to the user's profile c.Redirect(http.StatusSeeOther, "/@"+requestedUsername) return @@ -111,7 +111,7 @@ func (m *Module) OutboxGETHandler(c *gin.Context) { i, err := strconv.ParseBool(pageString) if err != nil { err := fmt.Errorf("error parsing %s: %s", PageKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } page = i @@ -129,15 +129,15 @@ func (m *Module) OutboxGETHandler(c *gin.Context) { maxID = maxIDString } - resp, errWithCode := m.processor.GetFediOutbox(transferContext(c), requestedUsername, page, maxID, minID, c.Request.URL) + resp, errWithCode := m.processor.GetFediOutbox(apiutil.TransferSignatureContext(c), requestedUsername, page, maxID, minID, c.Request.URL) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } b, err := json.Marshal(resp) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } diff --git a/internal/api/s2s/user/outboxget_test.go b/internal/api/activitypub/users/outboxget_test.go similarity index 95% rename from internal/api/s2s/user/outboxget_test.go rename to internal/api/activitypub/users/outboxget_test.go index 35a048323..cd0518350 100644 --- a/internal/api/s2s/user/outboxget_test.go +++ b/internal/api/activitypub/users/outboxget_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package user_test +package users_test import ( "context" @@ -30,7 +30,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/activity/streams" "github.com/superseriousbusiness/activity/streams/vocab" - "github.com/superseriousbusiness/gotosocial/internal/api/s2s/user" + "github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users" "github.com/superseriousbusiness/gotosocial/internal/concurrency" "github.com/superseriousbusiness/gotosocial/internal/messages" "github.com/superseriousbusiness/gotosocial/testrig" @@ -55,13 +55,13 @@ func (suite *OutboxGetTestSuite) TestGetOutbox() { ctx.Request.Header.Set("Date", signedRequest.DateHeader) // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) + suite.signatureCheck(ctx) // normally the router would populate these params from the path values, // but because we're calling the function directly, we need to set them manually. ctx.Params = gin.Params{ gin.Param{ - Key: user.UsernameKey, + Key: users.UsernameKey, Value: targetAccount.Username, }, } @@ -102,7 +102,7 @@ func (suite *OutboxGetTestSuite) TestGetOutboxFirstPage() { federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager, fedWorker) emailSender := testrig.NewEmailSender("../../../../web/template/", nil) processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker) - userModule := user.New(processor).(*user.Module) + userModule := users.New(processor) suite.NoError(processor.Start()) // setup request @@ -114,13 +114,13 @@ func (suite *OutboxGetTestSuite) TestGetOutboxFirstPage() { ctx.Request.Header.Set("Date", signedRequest.DateHeader) // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) + suite.signatureCheck(ctx) // normally the router would populate these params from the path values, // but because we're calling the function directly, we need to set them manually. ctx.Params = gin.Params{ gin.Param{ - Key: user.UsernameKey, + Key: users.UsernameKey, Value: targetAccount.Username, }, } @@ -161,7 +161,7 @@ func (suite *OutboxGetTestSuite) TestGetOutboxNextPage() { federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager, fedWorker) emailSender := testrig.NewEmailSender("../../../../web/template/", nil) processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker) - userModule := user.New(processor).(*user.Module) + userModule := users.New(processor) suite.NoError(processor.Start()) // setup request @@ -173,17 +173,17 @@ func (suite *OutboxGetTestSuite) TestGetOutboxNextPage() { ctx.Request.Header.Set("Date", signedRequest.DateHeader) // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) + suite.signatureCheck(ctx) // normally the router would populate these params from the path values, // but because we're calling the function directly, we need to set them manually. ctx.Params = gin.Params{ gin.Param{ - Key: user.UsernameKey, + Key: users.UsernameKey, Value: targetAccount.Username, }, gin.Param{ - Key: user.MaxIDKey, + Key: users.MaxIDKey, Value: "01F8MHAMCHF6Y650WCRSCP4WMY", }, } diff --git a/internal/api/s2s/user/publickeyget.go b/internal/api/activitypub/users/publickeyget.go similarity index 72% rename from internal/api/s2s/user/publickeyget.go rename to internal/api/activitypub/users/publickeyget.go index 265c01ee5..d57aa2f9d 100644 --- a/internal/api/s2s/user/publickeyget.go +++ b/internal/api/activitypub/users/publickeyget.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package user +package users import ( "encoding/json" @@ -25,7 +25,7 @@ import ( "strings" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) @@ -39,31 +39,31 @@ func (m *Module) PublicKeyGETHandler(c *gin.Context) { requestedUsername := strings.ToLower(c.Param(UsernameKey)) if requestedUsername == "" { err := errors.New("no username specified in request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) + format, err := apiutil.NegotiateAccept(c, apiutil.HTMLOrActivityPubHeaders...) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - if format == string(api.TextHTML) { + if format == string(apiutil.TextHTML) { // redirect to the user's profile c.Redirect(http.StatusSeeOther, "/@"+requestedUsername) return } - resp, errWithCode := m.processor.GetFediUser(transferContext(c), requestedUsername, c.Request.URL) + resp, errWithCode := m.processor.GetFediUser(apiutil.TransferSignatureContext(c), requestedUsername, c.Request.URL) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } b, err := json.Marshal(resp) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } diff --git a/internal/api/s2s/user/repliesget.go b/internal/api/activitypub/users/repliesget.go similarity index 78% rename from internal/api/s2s/user/repliesget.go rename to internal/api/activitypub/users/repliesget.go index b3b20d0c2..253166446 100644 --- a/internal/api/s2s/user/repliesget.go +++ b/internal/api/activitypub/users/repliesget.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package user +package users import ( "encoding/json" @@ -27,7 +27,7 @@ import ( "strings" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) @@ -97,7 +97,7 @@ func (m *Module) StatusRepliesGETHandler(c *gin.Context) { requestedUsername := strings.ToLower(c.Param(UsernameKey)) if requestedUsername == "" { err := errors.New("no username specified in request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } @@ -105,17 +105,17 @@ func (m *Module) StatusRepliesGETHandler(c *gin.Context) { requestedStatusID := strings.ToUpper(c.Param(StatusIDKey)) if requestedStatusID == "" { err := errors.New("no status id specified in request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) + format, err := apiutil.NegotiateAccept(c, apiutil.HTMLOrActivityPubHeaders...) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - if format == string(api.TextHTML) { + if format == string(apiutil.TextHTML) { // redirect to the status c.Redirect(http.StatusSeeOther, "/@"+requestedUsername+"/statuses/"+requestedStatusID) return @@ -126,7 +126,7 @@ func (m *Module) StatusRepliesGETHandler(c *gin.Context) { i, err := strconv.ParseBool(pageString) if err != nil { err := fmt.Errorf("error parsing %s: %s", PageKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } page = i @@ -138,7 +138,7 @@ func (m *Module) StatusRepliesGETHandler(c *gin.Context) { i, err := strconv.ParseBool(onlyOtherAccountsString) if err != nil { err := fmt.Errorf("error parsing %s: %s", OnlyOtherAccountsKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } onlyOtherAccounts = i @@ -150,15 +150,15 @@ func (m *Module) StatusRepliesGETHandler(c *gin.Context) { minID = minIDString } - resp, errWithCode := m.processor.GetFediStatusReplies(transferContext(c), requestedUsername, requestedStatusID, page, onlyOtherAccounts, minID, c.Request.URL) + resp, errWithCode := m.processor.GetFediStatusReplies(apiutil.TransferSignatureContext(c), requestedUsername, requestedStatusID, page, onlyOtherAccounts, minID, c.Request.URL) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } b, err := json.Marshal(resp) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } diff --git a/internal/api/s2s/user/repliesget_test.go b/internal/api/activitypub/users/repliesget_test.go similarity index 95% rename from internal/api/s2s/user/repliesget_test.go rename to internal/api/activitypub/users/repliesget_test.go index 5ab7f1ebd..3d8e0b12a 100644 --- a/internal/api/s2s/user/repliesget_test.go +++ b/internal/api/activitypub/users/repliesget_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package user_test +package users_test import ( "context" @@ -32,7 +32,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/activity/streams" "github.com/superseriousbusiness/activity/streams/vocab" - "github.com/superseriousbusiness/gotosocial/internal/api/s2s/user" + "github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users" "github.com/superseriousbusiness/gotosocial/internal/concurrency" "github.com/superseriousbusiness/gotosocial/internal/messages" "github.com/superseriousbusiness/gotosocial/testrig" @@ -58,17 +58,17 @@ func (suite *RepliesGetTestSuite) TestGetReplies() { ctx.Request.Header.Set("Date", signedRequest.DateHeader) // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) + suite.signatureCheck(ctx) // normally the router would populate these params from the path values, // but because we're calling the function directly, we need to set them manually. ctx.Params = gin.Params{ gin.Param{ - Key: user.UsernameKey, + Key: users.UsernameKey, Value: targetAccount.Username, }, gin.Param{ - Key: user.StatusIDKey, + Key: users.StatusIDKey, Value: targetStatus.ID, }, } @@ -111,7 +111,7 @@ func (suite *RepliesGetTestSuite) TestGetRepliesNext() { federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager, fedWorker) emailSender := testrig.NewEmailSender("../../../../web/template/", nil) processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker) - userModule := user.New(processor).(*user.Module) + userModule := users.New(processor) suite.NoError(processor.Start()) // setup request @@ -123,17 +123,17 @@ func (suite *RepliesGetTestSuite) TestGetRepliesNext() { ctx.Request.Header.Set("Date", signedRequest.DateHeader) // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) + suite.signatureCheck(ctx) // normally the router would populate these params from the path values, // but because we're calling the function directly, we need to set them manually. ctx.Params = gin.Params{ gin.Param{ - Key: user.UsernameKey, + Key: users.UsernameKey, Value: targetAccount.Username, }, gin.Param{ - Key: user.StatusIDKey, + Key: users.StatusIDKey, Value: targetStatus.ID, }, } @@ -179,7 +179,7 @@ func (suite *RepliesGetTestSuite) TestGetRepliesLast() { federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager, fedWorker) emailSender := testrig.NewEmailSender("../../../../web/template/", nil) processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker) - userModule := user.New(processor).(*user.Module) + userModule := users.New(processor) suite.NoError(processor.Start()) // setup request @@ -191,17 +191,17 @@ func (suite *RepliesGetTestSuite) TestGetRepliesLast() { ctx.Request.Header.Set("Date", signedRequest.DateHeader) // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) + suite.signatureCheck(ctx) // normally the router would populate these params from the path values, // but because we're calling the function directly, we need to set them manually. ctx.Params = gin.Params{ gin.Param{ - Key: user.UsernameKey, + Key: users.UsernameKey, Value: targetAccount.Username, }, gin.Param{ - Key: user.StatusIDKey, + Key: users.StatusIDKey, Value: targetStatus.ID, }, } diff --git a/internal/api/s2s/user/statusget.go b/internal/api/activitypub/users/statusget.go similarity index 69% rename from internal/api/s2s/user/statusget.go rename to internal/api/activitypub/users/statusget.go index 3e3d6ea56..a72f8fd16 100644 --- a/internal/api/s2s/user/statusget.go +++ b/internal/api/activitypub/users/statusget.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package user +package users import ( "encoding/json" @@ -25,7 +25,7 @@ import ( "strings" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) @@ -35,7 +35,7 @@ func (m *Module) StatusGETHandler(c *gin.Context) { requestedUsername := strings.ToLower(c.Param(UsernameKey)) if requestedUsername == "" { err := errors.New("no username specified in request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } @@ -43,31 +43,31 @@ func (m *Module) StatusGETHandler(c *gin.Context) { requestedStatusID := strings.ToUpper(c.Param(StatusIDKey)) if requestedStatusID == "" { err := errors.New("no status id specified in request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) + format, err := apiutil.NegotiateAccept(c, apiutil.HTMLOrActivityPubHeaders...) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - if format == string(api.TextHTML) { + if format == string(apiutil.TextHTML) { // redirect to the status c.Redirect(http.StatusSeeOther, "/@"+requestedUsername+"/statuses/"+requestedStatusID) return } - resp, errWithCode := m.processor.GetFediStatus(transferContext(c), requestedUsername, requestedStatusID, c.Request.URL) + resp, errWithCode := m.processor.GetFediStatus(apiutil.TransferSignatureContext(c), requestedUsername, requestedStatusID, c.Request.URL) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } b, err := json.Marshal(resp) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } diff --git a/internal/api/s2s/user/statusget_test.go b/internal/api/activitypub/users/statusget_test.go similarity index 94% rename from internal/api/s2s/user/statusget_test.go rename to internal/api/activitypub/users/statusget_test.go index 42f7dbb1b..37d311e7b 100644 --- a/internal/api/s2s/user/statusget_test.go +++ b/internal/api/activitypub/users/statusget_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package user_test +package users_test import ( "context" @@ -31,7 +31,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/activity/streams" "github.com/superseriousbusiness/activity/streams/vocab" - "github.com/superseriousbusiness/gotosocial/internal/api/s2s/user" + "github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users" "github.com/superseriousbusiness/gotosocial/testrig" ) @@ -55,17 +55,17 @@ func (suite *StatusGetTestSuite) TestGetStatus() { ctx.Request.Header.Set("Date", signedRequest.DateHeader) // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) + suite.signatureCheck(ctx) // normally the router would populate these params from the path values, // but because we're calling the function directly, we need to set them manually. ctx.Params = gin.Params{ gin.Param{ - Key: user.UsernameKey, + Key: users.UsernameKey, Value: targetAccount.Username, }, gin.Param{ - Key: user.StatusIDKey, + Key: users.StatusIDKey, Value: targetStatus.ID, }, } @@ -114,17 +114,17 @@ func (suite *StatusGetTestSuite) TestGetStatusLowercase() { ctx.Request.Header.Set("Date", signedRequest.DateHeader) // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) + suite.signatureCheck(ctx) // normally the router would populate these params from the path values, // but because we're calling the function directly, we need to set them manually. ctx.Params = gin.Params{ gin.Param{ - Key: user.UsernameKey, + Key: users.UsernameKey, Value: strings.ToLower(targetAccount.Username), }, gin.Param{ - Key: user.StatusIDKey, + Key: users.StatusIDKey, Value: strings.ToLower(targetStatus.ID), }, } diff --git a/internal/api/activitypub/users/user.go b/internal/api/activitypub/users/user.go new file mode 100644 index 000000000..d2e02fb08 --- /dev/null +++ b/internal/api/activitypub/users/user.go @@ -0,0 +1,80 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + 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 users + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/processing" + "github.com/superseriousbusiness/gotosocial/internal/uris" +) + +const ( + // UsernameKey is for account usernames. + UsernameKey = "username" + // StatusIDKey is for status IDs + StatusIDKey = "status" + // OnlyOtherAccountsKey is for filtering status responses. + OnlyOtherAccountsKey = "only_other_accounts" + // MinIDKey is for filtering status responses. + MinIDKey = "min_id" + // MaxIDKey is for filtering status responses. + MaxIDKey = "max_id" + // PageKey is for filtering status responses. + PageKey = "page" + + // BasePath is the base path for serving AP 'users' requests, minus the 'users' prefix. + BasePath = "/:" + UsernameKey + // PublicKeyPath is a path to a user's public key, for serving bare minimum AP representations. + PublicKeyPath = BasePath + "/" + uris.PublicKeyPath + // InboxPath is for serving POST requests to a user's inbox with the given username key. + InboxPath = BasePath + "/" + uris.InboxPath + // OutboxPath is for serving GET requests to a user's outbox with the given username key. + OutboxPath = BasePath + "/" + uris.OutboxPath + // FollowersPath is for serving GET request's to a user's followers list, with the given username key. + FollowersPath = BasePath + "/" + uris.FollowersPath + // FollowingPath is for serving GET request's to a user's following list, with the given username key. + FollowingPath = BasePath + "/" + uris.FollowingPath + // StatusPath is for serving GET requests to a particular status by a user, with the given username key and status ID + StatusPath = BasePath + "/" + uris.StatusesPath + "/:" + StatusIDKey + // StatusRepliesPath is for serving the replies collection of a status. + StatusRepliesPath = StatusPath + "/replies" +) + +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, BasePath, m.UsersGETHandler) + attachHandler(http.MethodPost, InboxPath, m.InboxPOSTHandler) + attachHandler(http.MethodGet, FollowersPath, m.FollowersGETHandler) + attachHandler(http.MethodGet, FollowingPath, m.FollowingGETHandler) + attachHandler(http.MethodGet, StatusPath, m.StatusGETHandler) + attachHandler(http.MethodGet, PublicKeyPath, m.PublicKeyGETHandler) + attachHandler(http.MethodGet, StatusRepliesPath, m.StatusRepliesGETHandler) + attachHandler(http.MethodGet, OutboxPath, m.OutboxGETHandler) +} diff --git a/internal/api/s2s/user/user_test.go b/internal/api/activitypub/users/user_test.go similarity index 83% rename from internal/api/s2s/user/user_test.go rename to internal/api/activitypub/users/user_test.go index 444e9cab5..0f08366f0 100644 --- a/internal/api/s2s/user/user_test.go +++ b/internal/api/activitypub/users/user_test.go @@ -16,12 +16,12 @@ along with this program. If not, see . */ -package user_test +package users_test import ( + "github.com/gin-gonic/gin" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/s2s/user" - "github.com/superseriousbusiness/gotosocial/internal/api/security" + "github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users" "github.com/superseriousbusiness/gotosocial/internal/concurrency" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/email" @@ -29,7 +29,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/messages" - "github.com/superseriousbusiness/gotosocial/internal/oauth" + "github.com/superseriousbusiness/gotosocial/internal/middleware" "github.com/superseriousbusiness/gotosocial/internal/processing" "github.com/superseriousbusiness/gotosocial/internal/storage" "github.com/superseriousbusiness/gotosocial/internal/typeutils" @@ -39,15 +39,13 @@ import ( type UserStandardTestSuite struct { // standard suite interfaces suite.Suite - db db.DB - tc typeutils.TypeConverter - mediaManager media.Manager - federator federation.Federator - emailSender email.Sender - processor processing.Processor - storage *storage.Driver - oauthServer oauth.Server - securityModule *security.Module + db db.DB + tc typeutils.TypeConverter + mediaManager media.Manager + federator federation.Federator + emailSender email.Sender + processor processing.Processor + storage *storage.Driver // standard suite models testTokens map[string]*gtsmodel.Token @@ -60,7 +58,9 @@ type UserStandardTestSuite struct { testBlocks map[string]*gtsmodel.Block // module being tested - userModule *user.Module + userModule *users.Module + + signatureCheck gin.HandlerFunc } func (suite *UserStandardTestSuite) SetupSuite() { @@ -88,12 +88,12 @@ func (suite *UserStandardTestSuite) SetupTest() { suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker), suite.storage, suite.mediaManager, fedWorker) suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil) suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker) - suite.userModule = user.New(suite.processor).(*user.Module) - suite.oauthServer = testrig.NewTestOauthServer(suite.db) - suite.securityModule = security.New(suite.db, suite.oauthServer).(*security.Module) + suite.userModule = users.New(suite.processor) testrig.StandardDBSetup(suite.db, suite.testAccounts) testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") + suite.signatureCheck = middleware.SignatureCheck(suite.db.IsURIBlocked) + suite.NoError(suite.processor.Start()) } diff --git a/internal/api/s2s/user/userget.go b/internal/api/activitypub/users/userget.go similarity index 74% rename from internal/api/s2s/user/userget.go rename to internal/api/activitypub/users/userget.go index 508c8be7d..51b0ebd72 100644 --- a/internal/api/s2s/user/userget.go +++ b/internal/api/activitypub/users/userget.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package user +package users import ( "encoding/json" @@ -25,7 +25,7 @@ import ( "strings" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) @@ -43,31 +43,31 @@ func (m *Module) UsersGETHandler(c *gin.Context) { requestedUsername := strings.ToLower(c.Param(UsernameKey)) if requestedUsername == "" { err := errors.New("no username specified in request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) + format, err := apiutil.NegotiateAccept(c, apiutil.HTMLOrActivityPubHeaders...) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - if format == string(api.TextHTML) { + if format == string(apiutil.TextHTML) { // redirect to the user's profile c.Redirect(http.StatusSeeOther, "/@"+requestedUsername) return } - resp, errWithCode := m.processor.GetFediUser(transferContext(c), requestedUsername, c.Request.URL) + resp, errWithCode := m.processor.GetFediUser(apiutil.TransferSignatureContext(c), requestedUsername, c.Request.URL) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } b, err := json.Marshal(resp) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } diff --git a/internal/api/s2s/user/userget_test.go b/internal/api/activitypub/users/userget_test.go similarity index 95% rename from internal/api/s2s/user/userget_test.go rename to internal/api/activitypub/users/userget_test.go index c656911d7..511634fc7 100644 --- a/internal/api/s2s/user/userget_test.go +++ b/internal/api/activitypub/users/userget_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package user_test +package users_test import ( "context" @@ -30,8 +30,8 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/activity/streams" "github.com/superseriousbusiness/activity/streams/vocab" + "github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/api/s2s/user" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/testrig" ) @@ -55,13 +55,13 @@ func (suite *UserGetTestSuite) TestGetUser() { ctx.Request.Header.Set("Date", signedRequest.DateHeader) // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) + suite.signatureCheck(ctx) // normally the router would populate these params from the path values, // but because we're calling the function directly, we need to set them manually. ctx.Params = gin.Params{ gin.Param{ - Key: user.UsernameKey, + Key: users.UsernameKey, Value: targetAccount.Username, }, } @@ -98,7 +98,7 @@ func (suite *UserGetTestSuite) TestGetUser() { // TestGetUserPublicKeyDeleted checks whether the public key of a deleted account can still be dereferenced. // This is needed by remote instances for authenticating delete requests and stuff like that. func (suite *UserGetTestSuite) TestGetUserPublicKeyDeleted() { - userModule := user.New(suite.processor).(*user.Module) + userModule := users.New(suite.processor) targetAccount := suite.testAccounts["local_account_1"] // first delete the account, as though zork had deleted himself @@ -133,13 +133,13 @@ func (suite *UserGetTestSuite) TestGetUserPublicKeyDeleted() { ctx.Request.Header.Set("Date", signedRequest.DateHeader) // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) + suite.signatureCheck(ctx) // normally the router would populate these params from the path values, // but because we're calling the function directly, we need to set them manually. ctx.Params = gin.Params{ gin.Param{ - Key: user.UsernameKey, + Key: users.UsernameKey, Value: targetAccount.Username, }, } diff --git a/internal/api/apimodule.go b/internal/api/apimodule.go deleted file mode 100644 index 67dc3a850..000000000 --- a/internal/api/apimodule.go +++ /dev/null @@ -1,37 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org - - 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 api - -import ( - "github.com/superseriousbusiness/gotosocial/internal/router" -) - -// ClientModule represents a chunk of code (usually contained in a single package) that adds a set -// of functionalities and/or side effects to a router, by mapping routes and/or middlewares onto it--in other words, a REST API ;) -// A ClientAPIMpdule with routes corresponds roughly to one main path of the gotosocial REST api, for example /api/v1/accounts/ or /oauth/ -type ClientModule interface { - Route(s router.Router) error -} - -// FederationModule represents a chunk of code (usually contained in a single package) that adds a set -// of functionalities and/or side effects to a router, by mapping routes and/or middlewares onto it--in other words, a REST API ;) -// Unlike ClientAPIModule, federation API module is not intended to be interacted with by clients directly -- it is primarily a server-to-server interface. -type FederationModule interface { - Route(s router.Router) error -} diff --git a/internal/api/auth.go b/internal/api/auth.go new file mode 100644 index 000000000..472c6922a --- /dev/null +++ b/internal/api/auth.go @@ -0,0 +1,64 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + 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 api + +import ( + "github.com/superseriousbusiness/gotosocial/internal/api/auth" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/middleware" + "github.com/superseriousbusiness/gotosocial/internal/oidc" + "github.com/superseriousbusiness/gotosocial/internal/processing" + "github.com/superseriousbusiness/gotosocial/internal/router" +) + +type Auth struct { + routerSession *gtsmodel.RouterSession + sessionName string + + auth *auth.Module +} + +// Route attaches 'auth' and 'oauth' groups to the given router. +func (a *Auth) Route(r router.Router) { + // create groupings for the 'auth' and 'oauth' prefixes + authGroup := r.AttachGroup("auth") + oauthGroup := r.AttachGroup("oauth") + + // instantiate + attach shared, non-global middlewares to both of these groups + var ( + rateLimitMiddleware = middleware.RateLimit() // nolint:contextcheck + gzipMiddleware = middleware.Gzip() + cacheControlMiddleware = middleware.CacheControl("private", "max-age=120") + sessionMiddleware = middleware.Session(a.sessionName, a.routerSession.Auth, a.routerSession.Crypt) + ) + authGroup.Use(rateLimitMiddleware, gzipMiddleware, cacheControlMiddleware, sessionMiddleware) + oauthGroup.Use(rateLimitMiddleware, gzipMiddleware, cacheControlMiddleware, sessionMiddleware) + + a.auth.RouteAuth(authGroup.Handle) + a.auth.RouteOauth(oauthGroup.Handle) +} + +func NewAuth(db db.DB, p processing.Processor, idp oidc.IDP, routerSession *gtsmodel.RouterSession, sessionName string) *Auth { + return &Auth{ + routerSession: routerSession, + sessionName: sessionName, + auth: auth.New(db, p, idp), + } +} diff --git a/internal/api/auth/auth.go b/internal/api/auth/auth.go new file mode 100644 index 000000000..7ce992466 --- /dev/null +++ b/internal/api/auth/auth.go @@ -0,0 +1,117 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + 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 auth + +import ( + "net/http" + + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/oidc" + "github.com/superseriousbusiness/gotosocial/internal/processing" +) + +const ( + /* + paths prefixed with 'auth' + */ + + // AuthSignInPath is the API path for users to sign in through + AuthSignInPath = "/sign_in" + // AuthCheckYourEmailPath users land here after registering a new account, instructs them to confirm their email + AuthCheckYourEmailPath = "/check_your_email" + // AuthWaitForApprovalPath users land here after confirming their email + // but before an admin approves their account (if such is required) + AuthWaitForApprovalPath = "/wait_for_approval" + // AuthAccountDisabledPath users land here when their account is suspended by an admin + AuthAccountDisabledPath = "/account_disabled" + // AuthCallbackPath is the API path for receiving callback tokens from external OIDC providers + AuthCallbackPath = "/callback" + + /* + paths prefixed with 'oauth' + */ + + // OauthTokenPath is the API path to use for granting token requests to users with valid credentials + OauthTokenPath = "/token" // #nosec G101 else we get a hardcoded credentials warning + // OauthAuthorizePath is the API path for authorization requests (eg., authorize this app to act on my behalf as a user) + OauthAuthorizePath = "/authorize" + // OauthFinalizePath is the API path for completing user registration with additional user details + OauthFinalizePath = "/finalize" + // OauthOobTokenPath is the path for serving an html representation of an oob token page. + OauthOobTokenPath = "/oob" // #nosec G101 else we get a hardcoded credentials warning + + /* + params / session keys + */ + + callbackStateParam = "state" + callbackCodeParam = "code" + sessionUserID = "userid" + sessionClientID = "client_id" + sessionRedirectURI = "redirect_uri" + sessionForceLogin = "force_login" + sessionResponseType = "response_type" + sessionScope = "scope" + sessionInternalState = "internal_state" + sessionClientState = "client_state" + sessionClaims = "claims" + sessionAppID = "app_id" +) + +type Module struct { + db db.DB + processor processing.Processor + idp oidc.IDP +} + +// New returns an Auth module which provides both 'oauth' and 'auth' endpoints. +// +// It is safe to pass a nil idp if oidc is disabled. +func New(db db.DB, processor processing.Processor, idp oidc.IDP) *Module { + return &Module{ + db: db, + processor: processor, + idp: idp, + } +} + +// RouteAuth routes all paths that should have an 'auth' prefix +func (m *Module) RouteAuth(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { + attachHandler(http.MethodGet, AuthSignInPath, m.SignInGETHandler) + attachHandler(http.MethodPost, AuthSignInPath, m.SignInPOSTHandler) + attachHandler(http.MethodGet, AuthCallbackPath, m.CallbackGETHandler) +} + +// RouteOauth routes all paths that should have an 'oauth' prefix +func (m *Module) RouteOauth(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { + attachHandler(http.MethodPost, OauthTokenPath, m.TokenPOSTHandler) + attachHandler(http.MethodGet, OauthAuthorizePath, m.AuthorizeGETHandler) + attachHandler(http.MethodPost, OauthAuthorizePath, m.AuthorizePOSTHandler) + attachHandler(http.MethodPost, OauthFinalizePath, m.FinalizePOSTHandler) + attachHandler(http.MethodGet, OauthOobTokenPath, m.OobHandler) +} + +func (m *Module) clearSession(s sessions.Session) { + s.Clear() + if err := s.Save(); err != nil { + panic(err) + } +} diff --git a/internal/api/client/auth/auth_test.go b/internal/api/auth/auth_test.go similarity index 85% rename from internal/api/client/auth/auth_test.go rename to internal/api/auth/auth_test.go index 75e958418..cb92850d0 100644 --- a/internal/api/client/auth/auth_test.go +++ b/internal/api/auth/auth_test.go @@ -20,7 +20,6 @@ package auth_test import ( "bytes" - "context" "fmt" "net/http/httptest" @@ -28,7 +27,7 @@ import ( "github.com/gin-contrib/sessions/memstore" "github.com/gin-gonic/gin" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/client/auth" + "github.com/superseriousbusiness/gotosocial/internal/api/auth" "github.com/superseriousbusiness/gotosocial/internal/concurrency" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" @@ -37,10 +36,9 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/messages" - "github.com/superseriousbusiness/gotosocial/internal/oauth" + "github.com/superseriousbusiness/gotosocial/internal/middleware" "github.com/superseriousbusiness/gotosocial/internal/oidc" "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" "github.com/superseriousbusiness/gotosocial/internal/storage" "github.com/superseriousbusiness/gotosocial/testrig" ) @@ -54,7 +52,6 @@ type AuthStandardTestSuite struct { processor processing.Processor emailSender email.Sender idp oidc.IDP - oauthServer oauth.Server // standard suite models testTokens map[string]*gtsmodel.Token @@ -90,17 +87,10 @@ func (suite *AuthStandardTestSuite) SetupTest() { suite.db = testrig.NewTestDB() suite.storage = testrig.NewInMemoryStorage() suite.mediaManager = testrig.NewTestMediaManager(suite.db, suite.storage) - suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker), suite.storage, suite.mediaManager, fedWorker) - suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil) + suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../testrig/media"), suite.db, fedWorker), suite.storage, suite.mediaManager, fedWorker) + suite.emailSender = testrig.NewEmailSender("../../../web/template/", nil) suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker) - - suite.oauthServer = testrig.NewTestOauthServer(suite.db) - var err error - suite.idp, err = oidc.NewIDP(context.Background()) - if err != nil { - panic(err) - } - suite.authModule = auth.New(suite.db, suite.idp, suite.processor).(*auth.Module) + suite.authModule = auth.New(suite.db, suite.processor, suite.idp) testrig.StandardDBSetup(suite.db, suite.testAccounts) } @@ -114,7 +104,7 @@ func (suite *AuthStandardTestSuite) newContext(requestMethod string, requestPath ctx, engine := testrig.CreateGinTestContext(recorder, nil) // load templates into the engine - testrig.ConfigureTemplatesWithGin(engine, "../../../../web/template") + testrig.ConfigureTemplatesWithGin(engine, "../../../web/template") // create the request protocol := config.GetProtocol() @@ -131,7 +121,7 @@ func (suite *AuthStandardTestSuite) newContext(requestMethod string, requestPath // trigger the session middleware on the context store := memstore.NewStore(make([]byte, 32), make([]byte, 32)) - store.Options(router.SessionOptions()) + store.Options(middleware.SessionOptions()) sessionMiddleware := sessions.Sessions("gotosocial-localhost", store) sessionMiddleware(ctx) diff --git a/internal/api/client/auth/authorize.go b/internal/api/auth/authorize.go similarity index 84% rename from internal/api/client/auth/authorize.go rename to internal/api/auth/authorize.go index f28d1dfc9..e504f6be2 100644 --- a/internal/api/client/auth/authorize.go +++ b/internal/api/auth/authorize.go @@ -27,8 +27,8 @@ import ( "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" "github.com/google/uuid" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" @@ -42,8 +42,8 @@ import ( func (m *Module) AuthorizeGETHandler(c *gin.Context) { s := sessions.Default(c) - if _, err := api.NegotiateAccept(c, api.HTMLAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.HTMLAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } @@ -51,20 +51,20 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) { // If it's not set, then we don't know yet who the user is, so we need to redirect them to the sign in page. userID, ok := s.Get(sessionUserID).(string) if !ok || userID == "" { - form := &model.OAuthAuthorize{} + form := &apimodel.OAuthAuthorize{} if err := c.ShouldBind(form); err != nil { m.clearSession(s) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGet) return } if errWithCode := saveAuthFormToSession(s, form); errWithCode != nil { m.clearSession(s) - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } - c.Redirect(http.StatusSeeOther, AuthSignInPath) + c.Redirect(http.StatusSeeOther, "/auth"+AuthSignInPath) return } @@ -73,7 +73,7 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) { if !ok || clientID == "" { m.clearSession(s) err := fmt.Errorf("key %s was not found in session", sessionClientID) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGet) return } @@ -87,7 +87,7 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) { } else { errWithCode = gtserror.NewErrorInternalError(err, safe, oauth.HelpfulAdvice) } - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } @@ -101,7 +101,7 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) { } else { errWithCode = gtserror.NewErrorInternalError(err, safe, oauth.HelpfulAdvice) } - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } @@ -115,7 +115,7 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) { } else { errWithCode = gtserror.NewErrorInternalError(err, safe, oauth.HelpfulAdvice) } - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } @@ -128,7 +128,7 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) { if !ok || redirect == "" { m.clearSession(s) err := fmt.Errorf("key %s was not found in session", sessionRedirectURI) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGet) return } @@ -136,13 +136,13 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) { if !ok || scope == "" { m.clearSession(s) err := fmt.Errorf("key %s was not found in session", sessionScope) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGet) return } instance, errWithCode := m.processor.InstanceGet(c.Request.Context(), config.GetHost()) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } @@ -206,7 +206,7 @@ func (m *Module) AuthorizePOSTHandler(c *gin.Context) { if len(errs) != 0 { errs = append(errs, oauth.HelpfulAdvice) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(errors.New("one or more missing keys on session during AuthorizePOSTHandler"), errs...), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(errors.New("one or more missing keys on session during AuthorizePOSTHandler"), errs...), m.processor.InstanceGet) return } @@ -220,7 +220,7 @@ func (m *Module) AuthorizePOSTHandler(c *gin.Context) { } else { errWithCode = gtserror.NewErrorInternalError(err, safe, oauth.HelpfulAdvice) } - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } @@ -234,7 +234,7 @@ func (m *Module) AuthorizePOSTHandler(c *gin.Context) { } else { errWithCode = gtserror.NewErrorInternalError(err, safe, oauth.HelpfulAdvice) } - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } @@ -263,13 +263,13 @@ func (m *Module) AuthorizePOSTHandler(c *gin.Context) { } if errWithCode := m.processor.OAuthHandleAuthorizeRequest(c.Writer, c.Request); errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) } } // saveAuthFormToSession checks the given OAuthAuthorize form, // and stores the values in the form into the session. -func saveAuthFormToSession(s sessions.Session, form *model.OAuthAuthorize) gtserror.WithCode { +func saveAuthFormToSession(s sessions.Session, form *apimodel.OAuthAuthorize) gtserror.WithCode { if form == nil { err := errors.New("OAuthAuthorize form was nil") return gtserror.NewErrorBadRequest(err, err.Error(), oauth.HelpfulAdvice) @@ -314,19 +314,19 @@ func saveAuthFormToSession(s sessions.Session, form *model.OAuthAuthorize) gtser func ensureUserIsAuthorizedOrRedirect(ctx *gin.Context, user *gtsmodel.User, account *gtsmodel.Account) (redirected bool) { if user.ConfirmedAt.IsZero() { - ctx.Redirect(http.StatusSeeOther, CheckYourEmailPath) + ctx.Redirect(http.StatusSeeOther, "/auth"+AuthCheckYourEmailPath) redirected = true return } if !*user.Approved { - ctx.Redirect(http.StatusSeeOther, WaitForApprovalPath) + ctx.Redirect(http.StatusSeeOther, "/auth"+AuthWaitForApprovalPath) redirected = true return } if *user.Disabled || !account.SuspendedAt.IsZero() { - ctx.Redirect(http.StatusSeeOther, AccountDisabledPath) + ctx.Redirect(http.StatusSeeOther, "/auth"+AuthAccountDisabledPath) redirected = true return } diff --git a/internal/api/client/auth/authorize_test.go b/internal/api/auth/authorize_test.go similarity index 91% rename from internal/api/client/auth/authorize_test.go rename to internal/api/auth/authorize_test.go index 738b3b910..ff65d041b 100644 --- a/internal/api/client/auth/authorize_test.go +++ b/internal/api/auth/authorize_test.go @@ -9,7 +9,7 @@ import ( "github.com/gin-contrib/sessions" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/client/auth" + "github.com/superseriousbusiness/gotosocial/internal/api/auth" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/testrig" ) @@ -34,7 +34,7 @@ func (suite *AuthAuthorizeTestSuite) TestAccountAuthorizeHandler() { return []string{"confirmed_at"} }, expectedStatusCode: http.StatusSeeOther, - expectedLocationHeader: auth.CheckYourEmailPath, + expectedLocationHeader: "/auth" + auth.AuthCheckYourEmailPath, }, { description: "user has their email confirmed but is not approved", @@ -44,7 +44,7 @@ func (suite *AuthAuthorizeTestSuite) TestAccountAuthorizeHandler() { return []string{"confirmed_at", "email"} }, expectedStatusCode: http.StatusSeeOther, - expectedLocationHeader: auth.WaitForApprovalPath, + expectedLocationHeader: "/auth" + auth.AuthWaitForApprovalPath, }, { description: "user has their email confirmed and is approved, but User entity has been disabled", @@ -56,7 +56,7 @@ func (suite *AuthAuthorizeTestSuite) TestAccountAuthorizeHandler() { return []string{"confirmed_at", "email", "approved", "disabled"} }, expectedStatusCode: http.StatusSeeOther, - expectedLocationHeader: auth.AccountDisabledPath, + expectedLocationHeader: "/auth" + auth.AuthAccountDisabledPath, }, { description: "user has their email confirmed and is approved, but Account entity has been suspended", @@ -69,7 +69,7 @@ func (suite *AuthAuthorizeTestSuite) TestAccountAuthorizeHandler() { return []string{"confirmed_at", "email", "approved", "disabled"} }, expectedStatusCode: http.StatusSeeOther, - expectedLocationHeader: auth.AccountDisabledPath, + expectedLocationHeader: "/auth" + auth.AuthAccountDisabledPath, }, } diff --git a/internal/api/client/auth/callback.go b/internal/api/auth/callback.go similarity index 83% rename from internal/api/client/auth/callback.go rename to internal/api/auth/callback.go index c97abf7aa..d344b5d5f 100644 --- a/internal/api/client/auth/callback.go +++ b/internal/api/auth/callback.go @@ -29,7 +29,7 @@ import ( "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" "github.com/google/uuid" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" @@ -47,6 +47,12 @@ type extraInfo struct { // CallbackGETHandler parses a token from an external auth provider. func (m *Module) CallbackGETHandler(c *gin.Context) { + if !config.GetOIDCEnabled() { + err := errors.New("oidc is not enabled for this server") + apiutil.ErrorHandler(c, gtserror.NewErrorNotFound(err, err.Error()), m.processor.InstanceGet) + return + } + s := sessions.Default(c) // check the query vs session state parameter to mitigate csrf @@ -56,7 +62,7 @@ func (m *Module) CallbackGETHandler(c *gin.Context) { if returnedInternalState == "" { m.clearSession(s) err := fmt.Errorf("%s parameter not found on callback query", callbackStateParam) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } @@ -65,14 +71,14 @@ func (m *Module) CallbackGETHandler(c *gin.Context) { if !ok { m.clearSession(s) err := fmt.Errorf("key %s was not found in session", sessionInternalState) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } if returnedInternalState != savedInternalState { m.clearSession(s) err := errors.New("mismatch between callback state and saved state") - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } @@ -81,14 +87,14 @@ func (m *Module) CallbackGETHandler(c *gin.Context) { if code == "" { m.clearSession(s) err := fmt.Errorf("%s parameter not found on callback query", callbackCodeParam) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } claims, errWithCode := m.idp.HandleCallback(c.Request.Context(), code) if errWithCode != nil { m.clearSession(s) - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } @@ -98,7 +104,7 @@ func (m *Module) CallbackGETHandler(c *gin.Context) { if !ok || clientID == "" { m.clearSession(s) err := fmt.Errorf("key %s was not found in session", sessionClientID) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGet) return } @@ -112,21 +118,21 @@ func (m *Module) CallbackGETHandler(c *gin.Context) { } else { errWithCode = gtserror.NewErrorInternalError(err, safe, oauth.HelpfulAdvice) } - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } user, errWithCode := m.fetchUserForClaims(c.Request.Context(), claims, net.IP(c.ClientIP()), app.ID) if errWithCode != nil { m.clearSession(s) - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } if user == nil { // no user exists yet - let's ask them for their preferred username instance, errWithCode := m.processor.InstanceGet(c.Request.Context(), config.GetHost()) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } @@ -135,7 +141,7 @@ func (m *Module) CallbackGETHandler(c *gin.Context) { s.Set(sessionAppID, app.ID) if err := s.Save(); err != nil { m.clearSession(s) - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } c.HTML(http.StatusOK, "finalize.tmpl", gin.H{ @@ -148,10 +154,10 @@ func (m *Module) CallbackGETHandler(c *gin.Context) { s.Set(sessionUserID, user.ID) if err := s.Save(); err != nil { m.clearSession(s) - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } - c.Redirect(http.StatusFound, OauthAuthorizePath) + c.Redirect(http.StatusFound, "/oauth"+OauthAuthorizePath) } // FinalizePOSTHandler registers the user after additional data has been provided @@ -161,7 +167,7 @@ func (m *Module) FinalizePOSTHandler(c *gin.Context) { form := &extraInfo{} if err := c.ShouldBind(form); err != nil { m.clearSession(s) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGet) return } @@ -169,7 +175,7 @@ func (m *Module) FinalizePOSTHandler(c *gin.Context) { validationError := func(err error) { instance, errWithCode := m.processor.InstanceGet(c.Request.Context(), config.GetHost()) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } c.HTML(http.StatusOK, "finalize.tmpl", gin.H{ @@ -189,7 +195,7 @@ func (m *Module) FinalizePOSTHandler(c *gin.Context) { // see if the username is still available usernameAvailable, err := m.db.IsUsernameAvailable(c.Request.Context(), form.Username) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGet) return } if !usernameAvailable { @@ -201,7 +207,7 @@ func (m *Module) FinalizePOSTHandler(c *gin.Context) { appID, ok := s.Get(sessionAppID).(string) if !ok { err := fmt.Errorf("key %s was not found in session", sessionAppID) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGet) return } @@ -209,7 +215,7 @@ func (m *Module) FinalizePOSTHandler(c *gin.Context) { claims, ok := s.Get(sessionClaims).(*oidc.Claims) if !ok { err := fmt.Errorf("key %s was not found in session", sessionClaims) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGet) return } @@ -217,7 +223,7 @@ func (m *Module) FinalizePOSTHandler(c *gin.Context) { user, errWithCode := m.createUserFromOIDC(c.Request.Context(), claims, form, net.IP(c.ClientIP()), appID) if errWithCode != nil { m.clearSession(s) - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } s.Delete(sessionClaims) @@ -225,10 +231,10 @@ func (m *Module) FinalizePOSTHandler(c *gin.Context) { s.Set(sessionUserID, user.ID) if err := s.Save(); err != nil { m.clearSession(s) - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } - c.Redirect(http.StatusFound, OauthAuthorizePath) + c.Redirect(http.StatusFound, "/oauth"+OauthAuthorizePath) } func (m *Module) fetchUserForClaims(ctx context.Context, claims *oidc.Claims, ip net.IP, appID string) (*gtsmodel.User, gtserror.WithCode) { diff --git a/internal/api/client/auth/oob.go b/internal/api/auth/oob.go similarity index 80% rename from internal/api/client/auth/oob.go rename to internal/api/auth/oob.go index 92e49d328..97f9c0f8c 100644 --- a/internal/api/client/auth/oob.go +++ b/internal/api/auth/oob.go @@ -26,8 +26,8 @@ import ( "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" @@ -38,16 +38,18 @@ func (m *Module) OobHandler(c *gin.Context) { host := config.GetHost() instance, errWithCode := m.processor.InstanceGet(c.Request.Context(), host) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } - instanceGet := func(ctx context.Context, domain string) (*model.Instance, gtserror.WithCode) { return instance, nil } + instanceGet := func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode) { + return instance, nil + } oobToken := c.Query("code") if oobToken == "" { err := errors.New("no 'code' query value provided in callback redirect") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error(), oauth.HelpfulAdvice), instanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error(), oauth.HelpfulAdvice), instanceGet) return } @@ -67,7 +69,7 @@ func (m *Module) OobHandler(c *gin.Context) { if len(errs) != 0 { errs = append(errs, oauth.HelpfulAdvice) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(errors.New("one or more missing keys on session during OobHandler"), errs...), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(errors.New("one or more missing keys on session during OobHandler"), errs...), m.processor.InstanceGet) return } @@ -81,7 +83,7 @@ func (m *Module) OobHandler(c *gin.Context) { } else { errWithCode = gtserror.NewErrorInternalError(err, safe, oauth.HelpfulAdvice) } - api.ErrorHandler(c, errWithCode, instanceGet) + apiutil.ErrorHandler(c, errWithCode, instanceGet) return } @@ -95,7 +97,7 @@ func (m *Module) OobHandler(c *gin.Context) { } else { errWithCode = gtserror.NewErrorInternalError(err, safe, oauth.HelpfulAdvice) } - api.ErrorHandler(c, errWithCode, instanceGet) + apiutil.ErrorHandler(c, errWithCode, instanceGet) return } diff --git a/internal/api/client/auth/signin.go b/internal/api/auth/signin.go similarity index 85% rename from internal/api/client/auth/signin.go rename to internal/api/auth/signin.go index 73a5de398..bae33a43b 100644 --- a/internal/api/client/auth/signin.go +++ b/internal/api/auth/signin.go @@ -26,7 +26,7 @@ import ( "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -44,15 +44,15 @@ type login struct { // The form will then POST to the sign in page, which will be handled by SignInPOSTHandler. // If an idp provider is set, then the user will be redirected to that to do their sign in. func (m *Module) SignInGETHandler(c *gin.Context) { - if _, err := api.NegotiateAccept(c, api.HTMLAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.HTMLAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - if m.idp == nil { + if !config.GetOIDCEnabled() { instance, errWithCode := m.processor.InstanceGet(c.Request.Context(), config.GetHost()) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } @@ -71,7 +71,7 @@ func (m *Module) SignInGETHandler(c *gin.Context) { if !ok { m.clearSession(s) err := fmt.Errorf("key %s was not found in session", sessionInternalState) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } @@ -87,7 +87,7 @@ func (m *Module) SignInPOSTHandler(c *gin.Context) { form := &login{} if err := c.ShouldBind(form); err != nil { m.clearSession(s) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGet) return } @@ -95,17 +95,17 @@ func (m *Module) SignInPOSTHandler(c *gin.Context) { if errWithCode != nil { // don't clear session here, so the user can just press back and try again // if they accidentally gave the wrong password or something - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } s.Set(sessionUserID, userid) if err := s.Save(); err != nil { err := fmt.Errorf("error saving user id onto session: %s", err) - api.ErrorHandler(c, gtserror.NewErrorInternalError(err, oauth.HelpfulAdvice), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err, oauth.HelpfulAdvice), m.processor.InstanceGet) } - c.Redirect(http.StatusFound, OauthAuthorizePath) + c.Redirect(http.StatusFound, "/oauth"+OauthAuthorizePath) } // ValidatePassword takes an email address and a password. diff --git a/internal/api/client/auth/token.go b/internal/api/auth/token.go similarity index 87% rename from internal/api/client/auth/token.go rename to internal/api/auth/token.go index fbbd08404..17c4d8d8b 100644 --- a/internal/api/client/auth/token.go +++ b/internal/api/auth/token.go @@ -22,7 +22,7 @@ import ( "net/http" "net/url" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -41,8 +41,8 @@ type tokenRequestForm struct { // TokenPOSTHandler should be served as a POST at https://example.org/oauth/token // The idea here is to serve an oauth access token to a user, which can be used for authorizing against non-public APIs. func (m *Module) TokenPOSTHandler(c *gin.Context) { - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } @@ -50,7 +50,7 @@ func (m *Module) TokenPOSTHandler(c *gin.Context) { form := &tokenRequestForm{} if err := c.ShouldBind(form); err != nil { - api.OAuthErrorHandler(c, gtserror.NewErrorBadRequest(oauth.InvalidRequest(), err.Error())) + apiutil.OAuthErrorHandler(c, gtserror.NewErrorBadRequest(oauth.InvalidRequest(), err.Error())) return } @@ -99,13 +99,13 @@ func (m *Module) TokenPOSTHandler(c *gin.Context) { } if len(help) != 0 { - api.OAuthErrorHandler(c, gtserror.NewErrorBadRequest(oauth.InvalidRequest(), help...)) + apiutil.OAuthErrorHandler(c, gtserror.NewErrorBadRequest(oauth.InvalidRequest(), help...)) return } token, errWithCode := m.processor.OAuthHandleTokenRequest(c.Request) if errWithCode != nil { - api.OAuthErrorHandler(c, errWithCode) + apiutil.OAuthErrorHandler(c, errWithCode) return } diff --git a/internal/api/client/auth/token_test.go b/internal/api/auth/token_test.go similarity index 100% rename from internal/api/client/auth/token_test.go rename to internal/api/auth/token_test.go diff --git a/internal/api/client.go b/internal/api/client.go new file mode 100644 index 000000000..7736a99f4 --- /dev/null +++ b/internal/api/client.go @@ -0,0 +1,129 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + 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 api + +import ( + "github.com/superseriousbusiness/gotosocial/internal/api/client/accounts" + "github.com/superseriousbusiness/gotosocial/internal/api/client/admin" + "github.com/superseriousbusiness/gotosocial/internal/api/client/apps" + "github.com/superseriousbusiness/gotosocial/internal/api/client/blocks" + "github.com/superseriousbusiness/gotosocial/internal/api/client/bookmarks" + "github.com/superseriousbusiness/gotosocial/internal/api/client/customemojis" + "github.com/superseriousbusiness/gotosocial/internal/api/client/favourites" + filter "github.com/superseriousbusiness/gotosocial/internal/api/client/filters" + "github.com/superseriousbusiness/gotosocial/internal/api/client/followrequests" + "github.com/superseriousbusiness/gotosocial/internal/api/client/instance" + "github.com/superseriousbusiness/gotosocial/internal/api/client/lists" + "github.com/superseriousbusiness/gotosocial/internal/api/client/media" + "github.com/superseriousbusiness/gotosocial/internal/api/client/notifications" + "github.com/superseriousbusiness/gotosocial/internal/api/client/search" + "github.com/superseriousbusiness/gotosocial/internal/api/client/statuses" + "github.com/superseriousbusiness/gotosocial/internal/api/client/streaming" + "github.com/superseriousbusiness/gotosocial/internal/api/client/timelines" + "github.com/superseriousbusiness/gotosocial/internal/api/client/user" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/middleware" + "github.com/superseriousbusiness/gotosocial/internal/processing" + "github.com/superseriousbusiness/gotosocial/internal/router" +) + +type Client struct { + processor processing.Processor + db db.DB + + accounts *accounts.Module // api/v1/accounts + admin *admin.Module // api/v1/admin + apps *apps.Module // api/v1/apps + blocks *blocks.Module // api/v1/blocks + bookmarks *bookmarks.Module // api/v1/bookmarks + customEmojis *customemojis.Module // api/v1/custom_emojis + favourites *favourites.Module // api/v1/favourites + filters *filter.Module // api/v1/filters + followRequests *followrequests.Module // api/v1/follow_requests + instance *instance.Module // api/v1/instance + lists *lists.Module // api/v1/lists + media *media.Module // api/v1/media, api/v2/media + notifications *notifications.Module // api/v1/notifications + search *search.Module // api/v1/search, api/v2/search + statuses *statuses.Module // api/v1/statuses + streaming *streaming.Module // api/v1/streaming + timelines *timelines.Module // api/v1/timelines + user *user.Module // api/v1/user +} + +func (c *Client) Route(r router.Router) { + // create a new group on the top level client 'api' prefix + apiGroup := r.AttachGroup("api") + + // attach non-global middlewares appropriate to the client api + apiGroup.Use( + middleware.TokenCheck(c.db, c.processor.OAuthValidateBearerToken), + middleware.RateLimit(), + middleware.Gzip(), + middleware.CacheControl("no-store"), // never cache api responses + ) + + // for each client api module, pass it the Handle function + // so that the module can attach its routes to this group + h := apiGroup.Handle + c.accounts.Route(h) + c.admin.Route(h) + c.apps.Route(h) + c.blocks.Route(h) + c.bookmarks.Route(h) + c.customEmojis.Route(h) + c.favourites.Route(h) + c.filters.Route(h) + c.followRequests.Route(h) + c.instance.Route(h) + c.lists.Route(h) + c.media.Route(h) + c.notifications.Route(h) + c.search.Route(h) + c.statuses.Route(h) + c.streaming.Route(h) + c.timelines.Route(h) + c.user.Route(h) +} + +func NewClient(db db.DB, p processing.Processor) *Client { + return &Client{ + processor: p, + db: db, + + accounts: accounts.New(p), + admin: admin.New(p), + apps: apps.New(p), + blocks: blocks.New(p), + bookmarks: bookmarks.New(p), + customEmojis: customemojis.New(p), + favourites: favourites.New(p), + filters: filter.New(p), + followRequests: followrequests.New(p), + instance: instance.New(p), + lists: lists.New(p), + media: media.New(p), + notifications: notifications.New(p), + search: search.New(p), + statuses: statuses.New(p), + streaming: streaming.New(p), + timelines: timelines.New(p), + user: user.New(p), + } +} diff --git a/internal/api/client/account/account_test.go b/internal/api/client/accounts/account_test.go similarity index 97% rename from internal/api/client/account/account_test.go rename to internal/api/client/accounts/account_test.go index 90dbd6249..57d1e6c04 100644 --- a/internal/api/client/account/account_test.go +++ b/internal/api/client/accounts/account_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package account_test +package accounts_test import ( "bytes" @@ -26,7 +26,7 @@ import ( "github.com/gin-gonic/gin" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/client/account" + "github.com/superseriousbusiness/gotosocial/internal/api/client/accounts" "github.com/superseriousbusiness/gotosocial/internal/concurrency" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" @@ -62,7 +62,7 @@ type AccountStandardTestSuite struct { testStatuses map[string]*gtsmodel.Status // module being tested - accountModule *account.Module + accountsModule *accounts.Module } func (suite *AccountStandardTestSuite) SetupSuite() { @@ -89,7 +89,7 @@ func (suite *AccountStandardTestSuite) SetupTest() { suite.sentEmails = make(map[string]string) suite.emailSender = testrig.NewEmailSender("../../../../web/template/", suite.sentEmails) suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker) - suite.accountModule = account.New(suite.processor).(*account.Module) + suite.accountsModule = accounts.New(suite.processor) testrig.StandardDBSetup(suite.db, nil) testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") diff --git a/internal/api/client/account/accountcreate.go b/internal/api/client/accounts/accountcreate.go similarity index 79% rename from internal/api/client/account/accountcreate.go rename to internal/api/client/accounts/accountcreate.go index e7b6c642d..041ca7fc4 100644 --- a/internal/api/client/account/accountcreate.go +++ b/internal/api/client/accounts/accountcreate.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package account +package accounts import ( "errors" @@ -24,8 +24,8 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -73,23 +73,23 @@ import ( func (m *Module) AccountCreatePOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, false, false) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - form := &model.AccountCreateRequest{} + form := &apimodel.AccountCreateRequest{} if err := c.ShouldBind(form); err != nil { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } if err := validateCreateAccount(form); err != nil { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } @@ -97,14 +97,14 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) { signUpIP := net.ParseIP(clientIP) if signUpIP == nil { err := errors.New("ip address could not be parsed from request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } form.IP = signUpIP ti, errWithCode := m.processor.AccountCreate(c.Request.Context(), authed, form) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } @@ -113,7 +113,7 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) { // validateCreateAccount checks through all the necessary prerequisites for creating a new account, // according to the provided account create request. If the account isn't eligible, an error will be returned. -func validateCreateAccount(form *model.AccountCreateRequest) error { +func validateCreateAccount(form *apimodel.AccountCreateRequest) error { if form == nil { return errors.New("form was nil") } diff --git a/internal/api/client/account/accountcreate_test.go b/internal/api/client/accounts/accountcreate_test.go similarity index 97% rename from internal/api/client/account/accountcreate_test.go rename to internal/api/client/accounts/accountcreate_test.go index a4fc165bf..b2b8c715f 100644 --- a/internal/api/client/account/accountcreate_test.go +++ b/internal/api/client/accounts/accountcreate_test.go @@ -16,4 +16,4 @@ // along with this program. If not, see . // */ -package account_test +package accounts_test diff --git a/internal/api/client/account/accountdelete.go b/internal/api/client/accounts/accountdelete.go similarity index 79% rename from internal/api/client/account/accountdelete.go rename to internal/api/client/accounts/accountdelete.go index 53bdedd0f..f1b95e95a 100644 --- a/internal/api/client/account/accountdelete.go +++ b/internal/api/client/accounts/accountdelete.go @@ -16,15 +16,15 @@ along with this program. If not, see . */ -package account +package accounts import ( "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + 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" ) @@ -68,26 +68,26 @@ import ( func (m *Module) AccountDeletePOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - form := &model.AccountDeleteRequest{} + form := &apimodel.AccountDeleteRequest{} if err := c.ShouldBind(&form); err != nil { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } if form.Password == "" { err = errors.New("no password provided in account delete request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } form.DeleteOriginID = authed.Account.ID if errWithCode := m.processor.AccountDeleteLocal(c.Request.Context(), authed, form); errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/account/accountdelete_test.go b/internal/api/client/accounts/accountdelete_test.go similarity index 88% rename from internal/api/client/account/accountdelete_test.go rename to internal/api/client/accounts/accountdelete_test.go index 78348eabc..31559d59a 100644 --- a/internal/api/client/account/accountdelete_test.go +++ b/internal/api/client/accounts/accountdelete_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package account_test +package accounts_test import ( "net/http" @@ -24,7 +24,7 @@ import ( "testing" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/client/account" + "github.com/superseriousbusiness/gotosocial/internal/api/client/accounts" "github.com/superseriousbusiness/gotosocial/testrig" ) @@ -45,10 +45,10 @@ func (suite *AccountDeleteTestSuite) TestAccountDeletePOSTHandler() { } bodyBytes := requestBody.Bytes() recorder := httptest.NewRecorder() - ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, account.DeleteAccountPath, w.FormDataContentType()) + ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, accounts.DeleteAccountPath, w.FormDataContentType()) // call the handler - suite.accountModule.AccountDeletePOSTHandler(ctx) + suite.accountsModule.AccountDeletePOSTHandler(ctx) // 1. we should have Accepted because our request was valid suite.Equal(http.StatusAccepted, recorder.Code) @@ -67,10 +67,10 @@ func (suite *AccountDeleteTestSuite) TestAccountDeletePOSTHandlerWrongPassword() } bodyBytes := requestBody.Bytes() recorder := httptest.NewRecorder() - ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, account.DeleteAccountPath, w.FormDataContentType()) + ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, accounts.DeleteAccountPath, w.FormDataContentType()) // call the handler - suite.accountModule.AccountDeletePOSTHandler(ctx) + suite.accountsModule.AccountDeletePOSTHandler(ctx) // 1. we should have Forbidden because we supplied the wrong password suite.Equal(http.StatusForbidden, recorder.Code) @@ -87,10 +87,10 @@ func (suite *AccountDeleteTestSuite) TestAccountDeletePOSTHandlerNoPassword() { } bodyBytes := requestBody.Bytes() recorder := httptest.NewRecorder() - ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, account.DeleteAccountPath, w.FormDataContentType()) + ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, accounts.DeleteAccountPath, w.FormDataContentType()) // call the handler - suite.accountModule.AccountDeletePOSTHandler(ctx) + suite.accountsModule.AccountDeletePOSTHandler(ctx) // 1. we should have StatusBadRequest because our request was invalid suite.Equal(http.StatusBadRequest, recorder.Code) diff --git a/internal/api/client/account/accountget.go b/internal/api/client/accounts/accountget.go similarity index 79% rename from internal/api/client/account/accountget.go rename to internal/api/client/accounts/accountget.go index c9aae5b2b..1a6354490 100644 --- a/internal/api/client/account/accountget.go +++ b/internal/api/client/accounts/accountget.go @@ -16,14 +16,14 @@ along with this program. If not, see . */ -package account +package accounts import ( "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -69,25 +69,25 @@ import ( func (m *Module) AccountGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetAcctID := c.Param(IDKey) if targetAcctID == "" { err := errors.New("no account id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } acctInfo, errWithCode := m.processor.AccountGet(c.Request.Context(), authed, targetAcctID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/account/account.go b/internal/api/client/accounts/accounts.go similarity index 67% rename from internal/api/client/account/account.go rename to internal/api/client/accounts/accounts.go index 4205baa2c..54c6c5f22 100644 --- a/internal/api/client/account/account.go +++ b/internal/api/client/accounts/accounts.go @@ -16,17 +16,13 @@ along with this program. If not, see . */ -package account +package accounts import ( "net/http" - "strings" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/processing" - - "github.com/superseriousbusiness/gotosocial/internal/router" ) const ( @@ -49,8 +45,8 @@ const ( // IDKey is the key to use for retrieving account ID in requests IDKey = "id" - // BasePath is the base API path for this module - BasePath = "/api/v1/accounts" + // BasePath is the base API path for this module, excluding the 'api' prefix + BasePath = "/v1/accounts" // BasePathWithID is the base path for this module with the ID key BasePathWithID = BasePath + "/:" + IDKey // VerifyPath is for verifying account credentials @@ -77,65 +73,47 @@ const ( DeleteAccountPath = BasePath + "/delete" ) -// Module implements the ClientAPIModule interface for account-related actions type Module struct { processor processing.Processor } -// New returns a new account module -func New(processor processing.Processor) api.ClientModule { +func New(processor processing.Processor) *Module { return &Module{ processor: processor, } } -// Route attaches all routes from this module to the given router -func (m *Module) Route(r router.Router) error { +func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { // create account - r.AttachHandler(http.MethodPost, BasePath, m.AccountCreatePOSTHandler) - - // delete account - r.AttachHandler(http.MethodPost, DeleteAccountPath, m.AccountDeletePOSTHandler) + attachHandler(http.MethodPost, BasePath, m.AccountCreatePOSTHandler) // get account - r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler) + attachHandler(http.MethodGet, BasePathWithID, m.AccountGETHandler) + + // delete account + attachHandler(http.MethodPost, DeleteAccountPath, m.AccountDeletePOSTHandler) + + // verify account + attachHandler(http.MethodGet, VerifyPath, m.AccountVerifyGETHandler) // modify account - r.AttachHandler(http.MethodPatch, BasePathWithID, m.muxHandler) + attachHandler(http.MethodPatch, UpdateCredentialsPath, m.AccountUpdateCredentialsPATCHHandler) // get account's statuses - r.AttachHandler(http.MethodGet, GetStatusesPath, m.AccountStatusesGETHandler) + attachHandler(http.MethodGet, GetStatusesPath, m.AccountStatusesGETHandler) // get following or followers - r.AttachHandler(http.MethodGet, GetFollowersPath, m.AccountFollowersGETHandler) - r.AttachHandler(http.MethodGet, GetFollowingPath, m.AccountFollowingGETHandler) + attachHandler(http.MethodGet, GetFollowersPath, m.AccountFollowersGETHandler) + attachHandler(http.MethodGet, GetFollowingPath, m.AccountFollowingGETHandler) // get relationship with account - r.AttachHandler(http.MethodGet, GetRelationshipsPath, m.AccountRelationshipsGETHandler) + attachHandler(http.MethodGet, GetRelationshipsPath, m.AccountRelationshipsGETHandler) // follow or unfollow account - r.AttachHandler(http.MethodPost, FollowPath, m.AccountFollowPOSTHandler) - r.AttachHandler(http.MethodPost, UnfollowPath, m.AccountUnfollowPOSTHandler) + attachHandler(http.MethodPost, FollowPath, m.AccountFollowPOSTHandler) + attachHandler(http.MethodPost, UnfollowPath, m.AccountUnfollowPOSTHandler) // block or unblock account - r.AttachHandler(http.MethodPost, BlockPath, m.AccountBlockPOSTHandler) - r.AttachHandler(http.MethodPost, UnblockPath, m.AccountUnblockPOSTHandler) - - return nil -} - -func (m *Module) muxHandler(c *gin.Context) { - ru := c.Request.RequestURI - switch c.Request.Method { - case http.MethodGet: - if strings.HasPrefix(ru, VerifyPath) { - m.AccountVerifyGETHandler(c) - } else { - m.AccountGETHandler(c) - } - case http.MethodPatch: - if strings.HasPrefix(ru, UpdateCredentialsPath) { - m.AccountUpdateCredentialsPATCHHandler(c) - } - } + attachHandler(http.MethodPost, BlockPath, m.AccountBlockPOSTHandler) + attachHandler(http.MethodPost, UnblockPath, m.AccountUnblockPOSTHandler) } diff --git a/internal/api/client/account/accountupdate.go b/internal/api/client/accounts/accountupdate.go similarity index 86% rename from internal/api/client/account/accountupdate.go rename to internal/api/client/accounts/accountupdate.go index f89259a96..5dbf0ce46 100644 --- a/internal/api/client/account/accountupdate.go +++ b/internal/api/client/accounts/accountupdate.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package account +package accounts import ( "errors" @@ -25,8 +25,8 @@ import ( "strconv" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + 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" ) @@ -138,33 +138,33 @@ import ( func (m *Module) AccountUpdateCredentialsPATCHHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } form, err := parseUpdateAccountForm(c) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } acctSensitive, errWithCode := m.processor.AccountUpdate(c.Request.Context(), authed, form) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } c.JSON(http.StatusOK, acctSensitive) } -func parseUpdateAccountForm(c *gin.Context) (*model.UpdateCredentialsRequest, error) { - form := &model.UpdateCredentialsRequest{ - Source: &model.UpdateSource{}, +func parseUpdateAccountForm(c *gin.Context) (*apimodel.UpdateCredentialsRequest, error) { + form := &apimodel.UpdateCredentialsRequest{ + Source: &apimodel.UpdateSource{}, } if err := c.ShouldBind(&form); err != nil { diff --git a/internal/api/client/account/accountupdate_test.go b/internal/api/client/accounts/accountupdate_test.go similarity index 91% rename from internal/api/client/account/accountupdate_test.go rename to internal/api/client/accounts/accountupdate_test.go index 259bb69e9..45a287ec8 100644 --- a/internal/api/client/account/accountupdate_test.go +++ b/internal/api/client/accounts/accountupdate_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package account_test +package accounts_test import ( "context" @@ -27,7 +27,7 @@ import ( "testing" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/client/account" + "github.com/superseriousbusiness/gotosocial/internal/api/client/accounts" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/testrig" ) @@ -50,10 +50,10 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandler() } bodyBytes := requestBody.Bytes() recorder := httptest.NewRecorder() - ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, account.UpdateCredentialsPath, w.FormDataContentType()) + ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, accounts.UpdateCredentialsPath, w.FormDataContentType()) // call the handler - suite.accountModule.AccountUpdateCredentialsPATCHHandler(ctx) + suite.accountsModule.AccountUpdateCredentialsPATCHHandler(ctx) // 1. we should have OK because our request was valid suite.Equal(http.StatusOK, recorder.Code) @@ -89,10 +89,10 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUnl } bodyBytes1 := requestBody1.Bytes() recorder1 := httptest.NewRecorder() - ctx1 := suite.newContext(recorder1, http.MethodPatch, bodyBytes1, account.UpdateCredentialsPath, w1.FormDataContentType()) + ctx1 := suite.newContext(recorder1, http.MethodPatch, bodyBytes1, accounts.UpdateCredentialsPath, w1.FormDataContentType()) // call the handler - suite.accountModule.AccountUpdateCredentialsPATCHHandler(ctx1) + suite.accountsModule.AccountUpdateCredentialsPATCHHandler(ctx1) // 1. we should have OK because our request was valid suite.Equal(http.StatusOK, recorder1.Code) @@ -125,10 +125,10 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUnl } bodyBytes2 := requestBody2.Bytes() recorder2 := httptest.NewRecorder() - ctx2 := suite.newContext(recorder2, http.MethodPatch, bodyBytes2, account.UpdateCredentialsPath, w2.FormDataContentType()) + ctx2 := suite.newContext(recorder2, http.MethodPatch, bodyBytes2, accounts.UpdateCredentialsPath, w2.FormDataContentType()) // call the handler - suite.accountModule.AccountUpdateCredentialsPATCHHandler(ctx2) + suite.accountsModule.AccountUpdateCredentialsPATCHHandler(ctx2) // 1. we should have OK because our request was valid suite.Equal(http.StatusOK, recorder1.Code) @@ -170,10 +170,10 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerGet } bodyBytes := requestBody.Bytes() recorder := httptest.NewRecorder() - ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, account.UpdateCredentialsPath, w.FormDataContentType()) + ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, accounts.UpdateCredentialsPath, w.FormDataContentType()) // call the handler - suite.accountModule.AccountUpdateCredentialsPATCHHandler(ctx) + suite.accountsModule.AccountUpdateCredentialsPATCHHandler(ctx) // 1. we should have OK because our request was valid suite.Equal(http.StatusOK, recorder.Code) @@ -212,10 +212,10 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerTwo } bodyBytes := requestBody.Bytes() recorder := httptest.NewRecorder() - ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, account.UpdateCredentialsPath, w.FormDataContentType()) + ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, accounts.UpdateCredentialsPath, w.FormDataContentType()) // call the handler - suite.accountModule.AccountUpdateCredentialsPATCHHandler(ctx) + suite.accountsModule.AccountUpdateCredentialsPATCHHandler(ctx) // 1. we should have OK because our request was valid suite.Equal(http.StatusOK, recorder.Code) @@ -266,10 +266,10 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerWit } bodyBytes := requestBody.Bytes() recorder := httptest.NewRecorder() - ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, account.UpdateCredentialsPath, w.FormDataContentType()) + ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, accounts.UpdateCredentialsPath, w.FormDataContentType()) // call the handler - suite.accountModule.AccountUpdateCredentialsPATCHHandler(ctx) + suite.accountsModule.AccountUpdateCredentialsPATCHHandler(ctx) // 1. we should have OK because our request was valid suite.Equal(http.StatusOK, recorder.Code) @@ -308,10 +308,10 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerEmp // set up the request bodyBytes := []byte{} recorder := httptest.NewRecorder() - ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, account.UpdateCredentialsPath, "") + ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, accounts.UpdateCredentialsPath, "") // call the handler - suite.accountModule.AccountUpdateCredentialsPATCHHandler(ctx) + suite.accountsModule.AccountUpdateCredentialsPATCHHandler(ctx) // 1. we should have OK because our request was valid suite.Equal(http.StatusBadRequest, recorder.Code) @@ -343,10 +343,10 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpd } bodyBytes := requestBody.Bytes() recorder := httptest.NewRecorder() - ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, account.UpdateCredentialsPath, w.FormDataContentType()) + ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, accounts.UpdateCredentialsPath, w.FormDataContentType()) // call the handler - suite.accountModule.AccountUpdateCredentialsPATCHHandler(ctx) + suite.accountsModule.AccountUpdateCredentialsPATCHHandler(ctx) // 1. we should have OK because our request was valid suite.Equal(http.StatusOK, recorder.Code) @@ -385,10 +385,10 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpd } bodyBytes := requestBody.Bytes() recorder := httptest.NewRecorder() - ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, account.UpdateCredentialsPath, w.FormDataContentType()) + ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, accounts.UpdateCredentialsPath, w.FormDataContentType()) // call the handler - suite.accountModule.AccountUpdateCredentialsPATCHHandler(ctx) + suite.accountsModule.AccountUpdateCredentialsPATCHHandler(ctx) // 1. we should have OK because our request was valid suite.Equal(http.StatusOK, recorder.Code) @@ -430,10 +430,10 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpd } bodyBytes := requestBody.Bytes() recorder := httptest.NewRecorder() - ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, account.UpdateCredentialsPath, w.FormDataContentType()) + ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, accounts.UpdateCredentialsPath, w.FormDataContentType()) // call the handler - suite.accountModule.AccountUpdateCredentialsPATCHHandler(ctx) + suite.accountsModule.AccountUpdateCredentialsPATCHHandler(ctx) suite.Equal(http.StatusBadRequest, recorder.Code) diff --git a/internal/api/client/account/accountverify.go b/internal/api/client/accounts/accountverify.go similarity index 80% rename from internal/api/client/account/accountverify.go rename to internal/api/client/accounts/accountverify.go index 916d0a322..2b39d5ab2 100644 --- a/internal/api/client/account/accountverify.go +++ b/internal/api/client/accounts/accountverify.go @@ -16,13 +16,13 @@ along with this program. If not, see . */ -package account +package accounts import ( "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -59,18 +59,18 @@ import ( func (m *Module) AccountVerifyGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } acctSensitive, errWithCode := m.processor.AccountGet(c.Request.Context(), authed, authed.Account.ID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/account/accountverify_test.go b/internal/api/client/accounts/accountverify_test.go similarity index 95% rename from internal/api/client/account/accountverify_test.go rename to internal/api/client/accounts/accountverify_test.go index 886272865..e74c30aba 100644 --- a/internal/api/client/account/accountverify_test.go +++ b/internal/api/client/accounts/accountverify_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package account_test +package accounts_test import ( "encoding/json" @@ -28,7 +28,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/client/account" + "github.com/superseriousbusiness/gotosocial/internal/api/client/accounts" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) @@ -42,10 +42,10 @@ func (suite *AccountVerifyTestSuite) TestAccountVerifyGet() { // set up the request recorder := httptest.NewRecorder() - ctx := suite.newContext(recorder, http.MethodGet, nil, account.VerifyPath, "") + ctx := suite.newContext(recorder, http.MethodGet, nil, accounts.VerifyPath, "") // call the handler - suite.accountModule.AccountVerifyGETHandler(ctx) + suite.accountsModule.AccountVerifyGETHandler(ctx) // 1. we should have OK because our request was valid suite.Equal(http.StatusOK, recorder.Code) diff --git a/internal/api/client/account/block.go b/internal/api/client/accounts/block.go similarity index 79% rename from internal/api/client/account/block.go rename to internal/api/client/accounts/block.go index 9840c96ab..9e14ecb6e 100644 --- a/internal/api/client/account/block.go +++ b/internal/api/client/accounts/block.go @@ -16,14 +16,14 @@ along with this program. If not, see . */ -package account +package accounts import ( "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -69,25 +69,25 @@ import ( func (m *Module) AccountBlockPOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetAcctID := c.Param(IDKey) if targetAcctID == "" { err := errors.New("no account id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } relationship, errWithCode := m.processor.AccountBlockCreate(c.Request.Context(), authed, targetAcctID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/account/block_test.go b/internal/api/client/accounts/block_test.go similarity index 91% rename from internal/api/client/account/block_test.go rename to internal/api/client/accounts/block_test.go index 9c75330aa..474a53eb8 100644 --- a/internal/api/client/account/block_test.go +++ b/internal/api/client/accounts/block_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package account_test +package accounts_test import ( "fmt" @@ -29,7 +29,7 @@ import ( "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/client/account" + "github.com/superseriousbusiness/gotosocial/internal/api/client/accounts" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/testrig" ) @@ -46,16 +46,16 @@ func (suite *BlockTestSuite) TestBlockSelf() { 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"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(account.BlockPath, ":id", testAcct.ID, 1)), nil) + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(accounts.BlockPath, ":id", testAcct.ID, 1)), nil) ctx.Params = gin.Params{ gin.Param{ - Key: account.IDKey, + Key: accounts.IDKey, Value: testAcct.ID, }, } - suite.accountModule.AccountBlockPOSTHandler(ctx) + suite.accountsModule.AccountBlockPOSTHandler(ctx) // 1. status should be Not Acceptable due to attempted self-block suite.Equal(http.StatusNotAcceptable, recorder.Code) diff --git a/internal/api/client/account/follow.go b/internal/api/client/accounts/follow.go similarity index 79% rename from internal/api/client/account/follow.go rename to internal/api/client/accounts/follow.go index cc523a7f8..d2a8af886 100644 --- a/internal/api/client/account/follow.go +++ b/internal/api/client/accounts/follow.go @@ -16,15 +16,15 @@ along with this program. If not, see . */ -package account +package accounts import ( "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + 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" ) @@ -91,32 +91,32 @@ import ( func (m *Module) AccountFollowPOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetAcctID := c.Param(IDKey) if targetAcctID == "" { err := errors.New("no account id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - form := &model.AccountFollowRequest{} + form := &apimodel.AccountFollowRequest{} if err := c.ShouldBind(form); err != nil { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } form.ID = targetAcctID relationship, errWithCode := m.processor.AccountFollowCreate(c.Request.Context(), authed, form) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/account/follow_test.go b/internal/api/client/accounts/follow_test.go similarity index 91% rename from internal/api/client/account/follow_test.go rename to internal/api/client/accounts/follow_test.go index fad67b185..fd15c3734 100644 --- a/internal/api/client/account/follow_test.go +++ b/internal/api/client/accounts/follow_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package account_test +package accounts_test import ( "fmt" @@ -29,7 +29,7 @@ import ( "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/client/account" + "github.com/superseriousbusiness/gotosocial/internal/api/client/accounts" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/testrig" ) @@ -46,17 +46,17 @@ func (suite *FollowTestSuite) TestFollowSelf() { 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"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(account.FollowPath, ":id", testAcct.ID, 1)), nil) + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(accounts.FollowPath, ":id", testAcct.ID, 1)), nil) ctx.Params = gin.Params{ gin.Param{ - Key: account.IDKey, + Key: accounts.IDKey, Value: testAcct.ID, }, } // call the handler - suite.accountModule.AccountFollowPOSTHandler(ctx) + suite.accountsModule.AccountFollowPOSTHandler(ctx) // 1. status should be Not Acceptable due to self-follow attempt suite.Equal(http.StatusNotAcceptable, recorder.Code) diff --git a/internal/api/client/account/followers.go b/internal/api/client/accounts/followers.go similarity index 80% rename from internal/api/client/account/followers.go rename to internal/api/client/accounts/followers.go index cb2f4bfa6..b464a5ad6 100644 --- a/internal/api/client/account/followers.go +++ b/internal/api/client/accounts/followers.go @@ -16,14 +16,14 @@ along with this program. If not, see . */ -package account +package accounts import ( "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -72,25 +72,25 @@ import ( func (m *Module) AccountFollowersGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetAcctID := c.Param(IDKey) if targetAcctID == "" { err := errors.New("no account id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } followers, errWithCode := m.processor.AccountFollowersGet(c.Request.Context(), authed, targetAcctID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/account/following.go b/internal/api/client/accounts/following.go similarity index 80% rename from internal/api/client/account/following.go rename to internal/api/client/accounts/following.go index 3d69739c3..4589ad07a 100644 --- a/internal/api/client/account/following.go +++ b/internal/api/client/accounts/following.go @@ -16,14 +16,14 @@ along with this program. If not, see . */ -package account +package accounts import ( "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -72,25 +72,25 @@ import ( func (m *Module) AccountFollowingGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetAcctID := c.Param(IDKey) if targetAcctID == "" { err := errors.New("no account id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } following, errWithCode := m.processor.AccountFollowingGet(c.Request.Context(), authed, targetAcctID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/account/relationships.go b/internal/api/client/accounts/relationships.go similarity index 73% rename from internal/api/client/account/relationships.go rename to internal/api/client/accounts/relationships.go index 56159d48e..60e7b517c 100644 --- a/internal/api/client/account/relationships.go +++ b/internal/api/client/accounts/relationships.go @@ -1,12 +1,12 @@ -package account +package accounts import ( "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + 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" ) @@ -57,12 +57,12 @@ import ( func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } @@ -72,18 +72,18 @@ func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) { id := c.Query("id") if id == "" { err = errors.New("no account id(s) specified in query") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } targetAccountIDs = append(targetAccountIDs, id) } - relationships := []model.Relationship{} + relationships := []apimodel.Relationship{} for _, targetAccountID := range targetAccountIDs { r, errWithCode := m.processor.AccountRelationshipGet(c.Request.Context(), authed, targetAccountID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } relationships = append(relationships, *r) diff --git a/internal/api/client/account/statuses.go b/internal/api/client/accounts/statuses.go similarity index 83% rename from internal/api/client/account/statuses.go rename to internal/api/client/accounts/statuses.go index 7ecf3ba9f..a04517feb 100644 --- a/internal/api/client/account/statuses.go +++ b/internal/api/client/accounts/statuses.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package account +package accounts import ( "errors" @@ -25,7 +25,7 @@ import ( "strconv" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -133,19 +133,19 @@ import ( func (m *Module) AccountStatusesGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, false, false, false, false) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetAcctID := c.Param(IDKey) if targetAcctID == "" { err := errors.New("no account id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } @@ -155,7 +155,7 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { i, err := strconv.ParseInt(limitString, 10, 32) if err != nil { err := fmt.Errorf("error parsing %s: %s", LimitKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } limit = int(i) @@ -167,7 +167,7 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { i, err := strconv.ParseBool(excludeRepliesString) if err != nil { err := fmt.Errorf("error parsing %s: %s", ExcludeRepliesKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } excludeReplies = i @@ -179,7 +179,7 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { i, err := strconv.ParseBool(excludeReblogsString) if err != nil { err := fmt.Errorf("error parsing %s: %s", ExcludeReblogsKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } excludeReblogs = i @@ -203,7 +203,7 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { i, err := strconv.ParseBool(pinnedString) if err != nil { err := fmt.Errorf("error parsing %s: %s", PinnedKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } pinnedOnly = i @@ -215,7 +215,7 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { i, err := strconv.ParseBool(mediaOnlyString) if err != nil { err := fmt.Errorf("error parsing %s: %s", OnlyMediaKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } mediaOnly = i @@ -227,7 +227,7 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { i, err := strconv.ParseBool(publicOnlyString) if err != nil { err := fmt.Errorf("error parsing %s: %s", OnlyPublicKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } publicOnly = i @@ -235,7 +235,7 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { resp, errWithCode := m.processor.AccountStatusesGet(c.Request.Context(), authed, targetAcctID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/account/statuses_test.go b/internal/api/client/accounts/statuses_test.go similarity index 95% rename from internal/api/client/account/statuses_test.go rename to internal/api/client/accounts/statuses_test.go index 1f935896c..92ca9d925 100644 --- a/internal/api/client/account/statuses_test.go +++ b/internal/api/client/accounts/statuses_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package account_test +package accounts_test import ( "encoding/json" @@ -29,7 +29,7 @@ import ( "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/client/account" + "github.com/superseriousbusiness/gotosocial/internal/api/client/accounts" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" ) @@ -45,13 +45,13 @@ func (suite *AccountStatusesTestSuite) TestGetStatusesPublicOnly() { ctx := suite.newContext(recorder, http.MethodGet, nil, fmt.Sprintf("/api/v1/accounts/%s/statuses?limit=20&only_media=false&only_public=true", targetAccount.ID), "") ctx.Params = gin.Params{ gin.Param{ - Key: account.IDKey, + Key: accounts.IDKey, Value: targetAccount.ID, }, } // call the handler - suite.accountModule.AccountStatusesGETHandler(ctx) + suite.accountsModule.AccountStatusesGETHandler(ctx) // 1. we should have OK because our request was valid suite.Equal(http.StatusOK, recorder.Code) @@ -85,13 +85,13 @@ func (suite *AccountStatusesTestSuite) TestGetStatusesPublicOnlyMediaOnly() { ctx := suite.newContext(recorder, http.MethodGet, nil, fmt.Sprintf("/api/v1/accounts/%s/statuses?limit=20&only_media=true&only_public=true", targetAccount.ID), "") ctx.Params = gin.Params{ gin.Param{ - Key: account.IDKey, + Key: accounts.IDKey, Value: targetAccount.ID, }, } // call the handler - suite.accountModule.AccountStatusesGETHandler(ctx) + suite.accountsModule.AccountStatusesGETHandler(ctx) // 1. we should have OK because our request was valid suite.Equal(http.StatusOK, recorder.Code) diff --git a/internal/api/client/account/unblock.go b/internal/api/client/accounts/unblock.go similarity index 80% rename from internal/api/client/account/unblock.go rename to internal/api/client/accounts/unblock.go index 451b7fd27..e0a0a978e 100644 --- a/internal/api/client/account/unblock.go +++ b/internal/api/client/accounts/unblock.go @@ -16,14 +16,14 @@ along with this program. If not, see . */ -package account +package accounts import ( "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -70,25 +70,25 @@ import ( func (m *Module) AccountUnblockPOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetAcctID := c.Param(IDKey) if targetAcctID == "" { err := errors.New("no account id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } relationship, errWithCode := m.processor.AccountBlockRemove(c.Request.Context(), authed, targetAcctID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/account/unfollow.go b/internal/api/client/accounts/unfollow.go similarity index 80% rename from internal/api/client/account/unfollow.go rename to internal/api/client/accounts/unfollow.go index fafba99fd..95c819903 100644 --- a/internal/api/client/account/unfollow.go +++ b/internal/api/client/accounts/unfollow.go @@ -16,14 +16,14 @@ along with this program. If not, see . */ -package account +package accounts import ( "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -70,25 +70,25 @@ import ( func (m *Module) AccountUnfollowPOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetAcctID := c.Param(IDKey) if targetAcctID == "" { err := errors.New("no account id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } relationship, errWithCode := m.processor.AccountFollowRemove(c.Request.Context(), authed, targetAcctID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/admin/accountaction.go b/internal/api/client/admin/accountaction.go index 2dc84a2d0..d40404b15 100644 --- a/internal/api/client/admin/accountaction.go +++ b/internal/api/client/admin/accountaction.go @@ -24,8 +24,8 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + 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" ) @@ -85,38 +85,38 @@ import ( func (m *Module) AccountActionPOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if !*authed.User.Admin { err := fmt.Errorf("user %s not an admin", authed.User.ID) - api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) return } - form := &model.AdminAccountActionRequest{} + form := &apimodel.AdminAccountActionRequest{} if err := c.ShouldBind(form); err != nil { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } if form.Type == "" { err := errors.New("no type specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } targetAcctID := c.Param(IDKey) if targetAcctID == "" { err := errors.New("no account id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } form.TargetAccountID = targetAcctID if errWithCode := m.processor.AdminAccountAction(c.Request.Context(), authed, form); errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/admin/admin.go b/internal/api/client/admin/admin.go index 569354c96..b4fb4d6d1 100644 --- a/internal/api/client/admin/admin.go +++ b/internal/api/client/admin/admin.go @@ -21,14 +21,13 @@ package admin import ( "net/http" - "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" ) const ( - // BasePath is the base API path for this module. - BasePath = "/api/v1/admin" + // BasePath is the base API path for this module, excluding the api prefix + BasePath = "/v1/admin" // EmojiPath is used for posting/deleting custom emojis. EmojiPath = BasePath + "/custom_emojis" // EmojiPathWithID is used for interacting with a single emoji. @@ -68,32 +67,28 @@ const ( DomainQueryKey = "domain" ) -// Module implements the ClientAPIModule interface for admin-related actions (reports, emojis, etc) type Module struct { processor processing.Processor } -// New returns a new admin module -func New(processor processing.Processor) api.ClientModule { +func New(processor processing.Processor) *Module { return &Module{ processor: processor, } } -// Route attaches all routes from this module to the given router -func (m *Module) Route(r router.Router) error { - r.AttachHandler(http.MethodPost, EmojiPath, m.EmojiCreatePOSTHandler) - r.AttachHandler(http.MethodGet, EmojiPath, m.EmojisGETHandler) - r.AttachHandler(http.MethodDelete, EmojiPathWithID, m.EmojiDELETEHandler) - r.AttachHandler(http.MethodGet, EmojiPathWithID, m.EmojiGETHandler) - r.AttachHandler(http.MethodPatch, EmojiPathWithID, m.EmojiPATCHHandler) - r.AttachHandler(http.MethodPost, DomainBlocksPath, m.DomainBlocksPOSTHandler) - r.AttachHandler(http.MethodGet, DomainBlocksPath, m.DomainBlocksGETHandler) - r.AttachHandler(http.MethodGet, DomainBlocksPathWithID, m.DomainBlockGETHandler) - r.AttachHandler(http.MethodDelete, DomainBlocksPathWithID, m.DomainBlockDELETEHandler) - r.AttachHandler(http.MethodPost, AccountsActionPath, m.AccountActionPOSTHandler) - r.AttachHandler(http.MethodPost, MediaCleanupPath, m.MediaCleanupPOSTHandler) - r.AttachHandler(http.MethodPost, MediaRefetchPath, m.MediaRefetchPOSTHandler) - r.AttachHandler(http.MethodGet, EmojiCategoriesPath, m.EmojiCategoriesGETHandler) - return nil +func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { + attachHandler(http.MethodPost, EmojiPath, m.EmojiCreatePOSTHandler) + attachHandler(http.MethodGet, EmojiPath, m.EmojisGETHandler) + attachHandler(http.MethodDelete, EmojiPathWithID, m.EmojiDELETEHandler) + attachHandler(http.MethodGet, EmojiPathWithID, m.EmojiGETHandler) + attachHandler(http.MethodPatch, EmojiPathWithID, m.EmojiPATCHHandler) + attachHandler(http.MethodPost, DomainBlocksPath, m.DomainBlocksPOSTHandler) + attachHandler(http.MethodGet, DomainBlocksPath, m.DomainBlocksGETHandler) + attachHandler(http.MethodGet, DomainBlocksPathWithID, m.DomainBlockGETHandler) + attachHandler(http.MethodDelete, DomainBlocksPathWithID, m.DomainBlockDELETEHandler) + attachHandler(http.MethodPost, AccountsActionPath, m.AccountActionPOSTHandler) + attachHandler(http.MethodPost, MediaCleanupPath, m.MediaCleanupPOSTHandler) + attachHandler(http.MethodPost, MediaRefetchPath, m.MediaRefetchPOSTHandler) + attachHandler(http.MethodGet, EmojiCategoriesPath, m.EmojiCategoriesGETHandler) } diff --git a/internal/api/client/admin/admin_test.go b/internal/api/client/admin/admin_test.go index 52c2630d9..ac3bbcb98 100644 --- a/internal/api/client/admin/admin_test.go +++ b/internal/api/client/admin/admin_test.go @@ -93,7 +93,7 @@ func (suite *AdminStandardTestSuite) SetupTest() { suite.sentEmails = make(map[string]string) suite.emailSender = testrig.NewEmailSender("../../../../web/template/", suite.sentEmails) suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker) - suite.adminModule = admin.New(suite.processor).(*admin.Module) + suite.adminModule = admin.New(suite.processor) testrig.StandardDBSetup(suite.db, nil) testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") } diff --git a/internal/api/client/admin/domainblockcreate.go b/internal/api/client/admin/domainblockcreate.go index 034ea8682..44410abe3 100644 --- a/internal/api/client/admin/domainblockcreate.go +++ b/internal/api/client/admin/domainblockcreate.go @@ -25,8 +25,8 @@ import ( "strconv" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + 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" ) @@ -126,18 +126,18 @@ import ( func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if !*authed.User.Admin { err := fmt.Errorf("user %s not an admin", authed.User.ID) - api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } @@ -147,21 +147,21 @@ func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) { i, err := strconv.ParseBool(importString) if err != nil { err := fmt.Errorf("error parsing %s: %s", ImportQueryKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } imp = i } - form := &model.DomainBlockCreateRequest{} + form := &apimodel.DomainBlockCreateRequest{} if err := c.ShouldBind(form); err != nil { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } if err := validateCreateDomainBlock(form, imp); err != nil { err := fmt.Errorf("error validating form: %s", err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } @@ -169,7 +169,7 @@ func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) { // we're importing multiple blocks domainBlocks, errWithCode := m.processor.AdminDomainBlocksImport(c.Request.Context(), authed, form) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } c.JSON(http.StatusOK, domainBlocks) @@ -179,13 +179,13 @@ func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) { // we're just creating one block domainBlock, errWithCode := m.processor.AdminDomainBlockCreate(c.Request.Context(), authed, form) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } c.JSON(http.StatusOK, domainBlock) } -func validateCreateDomainBlock(form *model.DomainBlockCreateRequest, imp bool) error { +func validateCreateDomainBlock(form *apimodel.DomainBlockCreateRequest, imp bool) error { if imp { if form.Domains.Size == 0 { return errors.New("import was specified but list of domains is empty") diff --git a/internal/api/client/admin/domainblockdelete.go b/internal/api/client/admin/domainblockdelete.go index 6f3684418..ddb07e6f6 100644 --- a/internal/api/client/admin/domainblockdelete.go +++ b/internal/api/client/admin/domainblockdelete.go @@ -24,7 +24,7 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -72,31 +72,31 @@ import ( func (m *Module) DomainBlockDELETEHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if !*authed.User.Admin { err := fmt.Errorf("user %s not an admin", authed.User.ID) - api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } domainBlockID := c.Param(IDKey) if domainBlockID == "" { err := errors.New("no domain block id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } domainBlock, errWithCode := m.processor.AdminDomainBlockDelete(c.Request.Context(), authed, domainBlockID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/admin/domainblockget.go b/internal/api/client/admin/domainblockget.go index 3d27b585e..b9d365caa 100644 --- a/internal/api/client/admin/domainblockget.go +++ b/internal/api/client/admin/domainblockget.go @@ -25,7 +25,7 @@ import ( "strconv" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -73,25 +73,25 @@ import ( func (m *Module) DomainBlockGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if !*authed.User.Admin { err := fmt.Errorf("user %s not an admin", authed.User.ID) - api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } domainBlockID := c.Param(IDKey) if domainBlockID == "" { err := errors.New("no domain block id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } @@ -101,7 +101,7 @@ func (m *Module) DomainBlockGETHandler(c *gin.Context) { i, err := strconv.ParseBool(exportString) if err != nil { err := fmt.Errorf("error parsing %s: %s", ExportQueryKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } export = i @@ -109,7 +109,7 @@ func (m *Module) DomainBlockGETHandler(c *gin.Context) { domainBlock, errWithCode := m.processor.AdminDomainBlockGet(c.Request.Context(), authed, domainBlockID, export) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/admin/domainblocksget.go b/internal/api/client/admin/domainblocksget.go index a4ab4ac1c..fea0ca35e 100644 --- a/internal/api/client/admin/domainblocksget.go +++ b/internal/api/client/admin/domainblocksget.go @@ -24,7 +24,7 @@ import ( "strconv" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -78,18 +78,18 @@ import ( func (m *Module) DomainBlocksGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if !*authed.User.Admin { err := fmt.Errorf("user %s not an admin", authed.User.ID) - api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } @@ -99,7 +99,7 @@ func (m *Module) DomainBlocksGETHandler(c *gin.Context) { i, err := strconv.ParseBool(exportString) if err != nil { err := fmt.Errorf("error parsing %s: %s", ExportQueryKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } export = i @@ -107,7 +107,7 @@ func (m *Module) DomainBlocksGETHandler(c *gin.Context) { domainBlocks, errWithCode := m.processor.AdminDomainBlocksGet(c.Request.Context(), authed, export) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/admin/emojicategoriesget.go b/internal/api/client/admin/emojicategoriesget.go index d8b379674..e69506413 100644 --- a/internal/api/client/admin/emojicategoriesget.go +++ b/internal/api/client/admin/emojicategoriesget.go @@ -23,7 +23,7 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -69,24 +69,24 @@ import ( func (m *Module) EmojiCategoriesGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if !*authed.User.Admin { err := fmt.Errorf("user %s not an admin", authed.User.ID) - api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } categories, errWithCode := m.processor.AdminEmojiCategoriesGet(c.Request.Context()) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/admin/emojicreate.go b/internal/api/client/admin/emojicreate.go index 2a075708f..8368a12b0 100644 --- a/internal/api/client/admin/emojicreate.go +++ b/internal/api/client/admin/emojicreate.go @@ -24,8 +24,8 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -100,42 +100,42 @@ import ( func (m *Module) EmojiCreatePOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if !*authed.User.Admin { err := fmt.Errorf("user %s not an admin", authed.User.ID) - api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - form := &model.EmojiCreateRequest{} + form := &apimodel.EmojiCreateRequest{} if err := c.ShouldBind(form); err != nil { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } if err := validateCreateEmoji(form); err != nil { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } apiEmoji, errWithCode := m.processor.AdminEmojiCreate(c.Request.Context(), authed, form) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } c.JSON(http.StatusOK, apiEmoji) } -func validateCreateEmoji(form *model.EmojiCreateRequest) error { +func validateCreateEmoji(form *apimodel.EmojiCreateRequest) error { if form.Image == nil || form.Image.Size == 0 { return errors.New("no emoji given") } diff --git a/internal/api/client/admin/emojidelete.go b/internal/api/client/admin/emojidelete.go index 14f3c70ff..b66116b6d 100644 --- a/internal/api/client/admin/emojidelete.go +++ b/internal/api/client/admin/emojidelete.go @@ -24,7 +24,7 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -78,31 +78,31 @@ import ( func (m *Module) EmojiDELETEHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if !*authed.User.Admin { err := fmt.Errorf("user %s not an admin", authed.User.ID) - api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } emojiID := c.Param(IDKey) if emojiID == "" { err := errors.New("no emoji id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } emoji, errWithCode := m.processor.AdminEmojiDelete(c.Request.Context(), authed, emojiID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/admin/emojiget.go b/internal/api/client/admin/emojiget.go index 60f7d5948..49d586756 100644 --- a/internal/api/client/admin/emojiget.go +++ b/internal/api/client/admin/emojiget.go @@ -24,7 +24,7 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -68,31 +68,31 @@ import ( func (m *Module) EmojiGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if !*authed.User.Admin { err := fmt.Errorf("user %s not an admin", authed.User.ID) - api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } emojiID := c.Param(IDKey) if emojiID == "" { err := errors.New("no emoji id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } emoji, errWithCode := m.processor.AdminEmojiGet(c.Request.Context(), authed, emojiID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/admin/emojisget.go b/internal/api/client/admin/emojisget.go index 0b7cfe059..e8b3c0e49 100644 --- a/internal/api/client/admin/emojisget.go +++ b/internal/api/client/admin/emojisget.go @@ -25,7 +25,7 @@ import ( "strings" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" @@ -125,18 +125,18 @@ import ( func (m *Module) EmojisGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if !*authed.User.Admin { err := fmt.Errorf("user %s not an admin", authed.User.ID) - api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } @@ -149,7 +149,7 @@ func (m *Module) EmojisGETHandler(c *gin.Context) { i, err := strconv.ParseInt(limitString, 10, 32) if err != nil { err := fmt.Errorf("error parsing %s: %s", LimitKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } limit = int(i) @@ -177,7 +177,7 @@ func (m *Module) EmojisGETHandler(c *gin.Context) { shortcode = strings.Trim(filter[10:], ":") // remove any errant ":" default: err := fmt.Errorf("filter %s not recognized; accepted values are 'domain:[domain]', 'disabled', 'enabled', 'shortcode:[shortcode]'", filter) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } } @@ -200,7 +200,7 @@ func (m *Module) EmojisGETHandler(c *gin.Context) { resp, errWithCode := m.processor.AdminEmojisGet(c.Request.Context(), authed, domain, includeDisabled, includeEnabled, shortcode, maxShortcodeDomain, minShortcodeDomain, limit) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/admin/emojiupdate.go b/internal/api/client/admin/emojiupdate.go index 695c6bcde..8402b30e9 100644 --- a/internal/api/client/admin/emojiupdate.go +++ b/internal/api/client/admin/emojiupdate.go @@ -25,8 +25,8 @@ import ( "strings" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -123,42 +123,42 @@ import ( func (m *Module) EmojiPATCHHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if !*authed.User.Admin { err := fmt.Errorf("user %s not an admin", authed.User.ID) - api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } emojiID := c.Param(IDKey) if emojiID == "" { err := errors.New("no emoji id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - form := &model.EmojiUpdateRequest{} + form := &apimodel.EmojiUpdateRequest{} if err := c.ShouldBind(form); err != nil { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } if err := validateUpdateEmoji(form); err != nil { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } emoji, errWithCode := m.processor.AdminEmojiUpdate(c.Request.Context(), emojiID, form) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } @@ -166,14 +166,14 @@ func (m *Module) EmojiPATCHHandler(c *gin.Context) { } // do a first pass on the form here -func validateUpdateEmoji(form *model.EmojiUpdateRequest) error { +func validateUpdateEmoji(form *apimodel.EmojiUpdateRequest) error { // check + normalize update type so we don't need // to do this trimming + lowercasing again later switch strings.TrimSpace(strings.ToLower(string(form.Type))) { - case string(model.EmojiUpdateDisable): + case string(apimodel.EmojiUpdateDisable): // no params required for this one, so don't bother checking - form.Type = model.EmojiUpdateDisable - case string(model.EmojiUpdateCopy): + form.Type = apimodel.EmojiUpdateDisable + case string(apimodel.EmojiUpdateCopy): // need at least a valid shortcode when doing a copy if form.Shortcode == nil { return errors.New("emoji action type was 'copy' but no shortcode was provided") @@ -190,8 +190,8 @@ func validateUpdateEmoji(form *model.EmojiUpdateRequest) error { } } - form.Type = model.EmojiUpdateCopy - case string(model.EmojiUpdateModify): + form.Type = apimodel.EmojiUpdateCopy + case string(apimodel.EmojiUpdateModify): // need either image or category name for modify hasImage := form.Image != nil && form.Image.Size != 0 hasCategoryName := form.CategoryName != nil @@ -212,7 +212,7 @@ func validateUpdateEmoji(form *model.EmojiUpdateRequest) error { } } - form.Type = model.EmojiUpdateModify + form.Type = apimodel.EmojiUpdateModify default: return errors.New("emoji action type must be one of 'disable', 'copy', 'modify'") } diff --git a/internal/api/client/admin/mediacleanup.go b/internal/api/client/admin/mediacleanup.go index 157f35ab0..7f3fc11d5 100644 --- a/internal/api/client/admin/mediacleanup.go +++ b/internal/api/client/admin/mediacleanup.go @@ -23,8 +23,8 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -71,19 +71,19 @@ import ( func (m *Module) MediaCleanupPOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if !*authed.User.Admin { err := fmt.Errorf("user %s not an admin", authed.User.ID) - api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) return } - form := &model.MediaCleanupRequest{} + form := &apimodel.MediaCleanupRequest{} if err := c.ShouldBind(form); err != nil { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } @@ -98,7 +98,7 @@ func (m *Module) MediaCleanupPOSTHandler(c *gin.Context) { } if errWithCode := m.processor.AdminMediaPrune(c.Request.Context(), remoteCacheDays); errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/admin/mediarefetch.go b/internal/api/client/admin/mediarefetch.go index 9c8a30c1b..5618843e5 100644 --- a/internal/api/client/admin/mediarefetch.go +++ b/internal/api/client/admin/mediarefetch.go @@ -23,7 +23,7 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -74,18 +74,18 @@ import ( func (m *Module) MediaRefetchPOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if !*authed.User.Admin { err := fmt.Errorf("user %s not an admin", authed.User.ID) - api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) return } if errWithCode := m.processor.AdminMediaRefetch(c.Request.Context(), authed, c.Query(DomainQueryKey)); errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/app/app_test.go b/internal/api/client/app/app_test.go deleted file mode 100644 index 5c1981ba1..000000000 --- a/internal/api/client/app/app_test.go +++ /dev/null @@ -1,21 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org - - 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 app_test - -// TODO: write tests diff --git a/internal/api/client/app/appcreate.go b/internal/api/client/apps/appcreate.go similarity index 74% rename from internal/api/client/app/appcreate.go rename to internal/api/client/apps/appcreate.go index 6060c9480..f381e9954 100644 --- a/internal/api/client/app/appcreate.go +++ b/internal/api/client/apps/appcreate.go @@ -16,15 +16,15 @@ along with this program. If not, see . */ -package app +package apps import ( "fmt" "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + 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" ) @@ -77,48 +77,48 @@ const ( func (m *Module) AppsPOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, false, false, false, false) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - form := &model.ApplicationCreateRequest{} + form := &apimodel.ApplicationCreateRequest{} if err := c.ShouldBind(form); err != nil { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } if len([]rune(form.ClientName)) > formFieldLen { err := fmt.Errorf("client_name must be less than %d characters", formFieldLen) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } if len([]rune(form.RedirectURIs)) > formRedirectLen { err := fmt.Errorf("redirect_uris must be less than %d characters", formRedirectLen) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } if len([]rune(form.Scopes)) > formFieldLen { err := fmt.Errorf("scopes must be less than %d characters", formFieldLen) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } if len([]rune(form.Website)) > formFieldLen { err := fmt.Errorf("website must be less than %d characters", formFieldLen) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } apiApp, errWithCode := m.processor.AppCreate(c.Request.Context(), authed, form) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/app/app.go b/internal/api/client/apps/apps.go similarity index 61% rename from internal/api/client/app/app.go rename to internal/api/client/apps/apps.go index 0bbeb6cc9..264a76f6f 100644 --- a/internal/api/client/app/app.go +++ b/internal/api/client/apps/apps.go @@ -16,33 +16,28 @@ along with this program. If not, see . */ -package app +package apps import ( "net/http" - "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" ) -// BasePath is the base path for this api module -const BasePath = "/api/v1/apps" +// BasePath is the base path for this api module, excluding the api prefix +const BasePath = "/v1/apps" -// Module implements the ClientAPIModule interface for requests relating to registering/removing applications type Module struct { processor processing.Processor } -// New returns a new auth module -func New(processor processing.Processor) api.ClientModule { +func New(processor processing.Processor) *Module { return &Module{ processor: processor, } } -// Route satisfies the RESTAPIModule interface -func (m *Module) Route(s router.Router) error { - s.AttachHandler(http.MethodPost, BasePath, m.AppsPOSTHandler) - return nil +func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { + attachHandler(http.MethodPost, BasePath, m.AppsPOSTHandler) } diff --git a/internal/api/client/auth/auth.go b/internal/api/client/auth/auth.go deleted file mode 100644 index 8a1d9d483..000000000 --- a/internal/api/client/auth/auth.go +++ /dev/null @@ -1,105 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org - - 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 auth - -import ( - "net/http" - - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/oauth" - "github.com/superseriousbusiness/gotosocial/internal/oidc" - "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" -) - -/* #nosec G101 */ -const ( - // AuthSignInPath is the API path for users to sign in through - AuthSignInPath = "/auth/sign_in" - - // CheckYourEmailPath users land here after registering a new account, instructs them to confirm thier email - CheckYourEmailPath = "/check_your_email" - - // WaitForApprovalPath users land here after confirming thier email but before an admin approves thier account - // (if such is required) - WaitForApprovalPath = "/wait_for_approval" - - // AccountDisabledPath users land here when thier account is suspended by an admin - AccountDisabledPath = "/account_disabled" - - // OauthTokenPath is the API path to use for granting token requests to users with valid credentials - OauthTokenPath = "/oauth/token" - - // OauthAuthorizePath is the API path for authorization requests (eg., authorize this app to act on my behalf as a user) - OauthAuthorizePath = "/oauth/authorize" - - // OauthFinalizePath is the API path for completing user registration with additional user details - OauthFinalizePath = "/oauth/finalize" - - // CallbackPath is the API path for receiving callback tokens from external OIDC providers - CallbackPath = oidc.CallbackPath - - callbackStateParam = "state" - callbackCodeParam = "code" - - sessionUserID = "userid" - sessionClientID = "client_id" - sessionRedirectURI = "redirect_uri" - sessionForceLogin = "force_login" - sessionResponseType = "response_type" - sessionScope = "scope" - sessionInternalState = "internal_state" - sessionClientState = "client_state" - sessionClaims = "claims" - sessionAppID = "app_id" -) - -// Module implements the ClientAPIModule interface for -type Module struct { - db db.DB - idp oidc.IDP - processor processing.Processor -} - -// New returns a new auth module -func New(db db.DB, idp oidc.IDP, processor processing.Processor) api.ClientModule { - return &Module{ - db: db, - idp: idp, - processor: processor, - } -} - -// Route satisfies the RESTAPIModule interface -func (m *Module) Route(s router.Router) error { - s.AttachHandler(http.MethodGet, AuthSignInPath, m.SignInGETHandler) - s.AttachHandler(http.MethodPost, AuthSignInPath, m.SignInPOSTHandler) - - s.AttachHandler(http.MethodPost, OauthTokenPath, m.TokenPOSTHandler) - - s.AttachHandler(http.MethodGet, OauthAuthorizePath, m.AuthorizeGETHandler) - s.AttachHandler(http.MethodPost, OauthAuthorizePath, m.AuthorizePOSTHandler) - - s.AttachHandler(http.MethodGet, CallbackPath, m.CallbackGETHandler) - s.AttachHandler(http.MethodPost, OauthFinalizePath, m.FinalizePOSTHandler) - - s.AttachHandler(http.MethodGet, oauth.OOBTokenPath, m.OobHandler) - return nil -} diff --git a/internal/api/client/blocks/blocks.go b/internal/api/client/blocks/blocks.go index 2211a8076..df2ee65bb 100644 --- a/internal/api/client/blocks/blocks.go +++ b/internal/api/client/blocks/blocks.go @@ -21,14 +21,13 @@ package blocks import ( "net/http" - "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" ) const ( - // BasePath is the base URI path for serving favourites - BasePath = "/api/v1/blocks" + // BasePath is the base URI path for serving blocks, minus the api prefix. + BasePath = "/v1/blocks" // MaxIDKey is the url query for setting a max ID to return MaxIDKey = "max_id" @@ -38,20 +37,16 @@ const ( LimitKey = "limit" ) -// Module implements the ClientAPIModule interface for everything relating to viewing blocks type Module struct { processor processing.Processor } -// New returns a new blocks module -func New(processor processing.Processor) api.ClientModule { +func New(processor processing.Processor) *Module { return &Module{ processor: processor, } } -// Route attaches all routes from this module to the given router -func (m *Module) Route(r router.Router) error { - r.AttachHandler(http.MethodGet, BasePath, m.BlocksGETHandler) - return nil +func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { + attachHandler(http.MethodGet, BasePath, m.BlocksGETHandler) } diff --git a/internal/api/client/blocks/blocksget.go b/internal/api/client/blocks/blocksget.go index 98f5ce6ea..290ea6617 100644 --- a/internal/api/client/blocks/blocksget.go +++ b/internal/api/client/blocks/blocksget.go @@ -24,7 +24,7 @@ import ( "strconv" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -96,12 +96,12 @@ import ( func (m *Module) BlocksGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } @@ -123,7 +123,7 @@ func (m *Module) BlocksGETHandler(c *gin.Context) { i, err := strconv.ParseInt(limitString, 10, 32) if err != nil { err := fmt.Errorf("error parsing %s: %s", LimitKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } limit = int(i) @@ -131,7 +131,7 @@ func (m *Module) BlocksGETHandler(c *gin.Context) { resp, errWithCode := m.processor.BlocksGet(c.Request.Context(), authed, maxID, sinceID, limit) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/bookmarks/bookmarks.go b/internal/api/client/bookmarks/bookmarks.go index 492b7364c..d0273321c 100644 --- a/internal/api/client/bookmarks/bookmarks.go +++ b/internal/api/client/bookmarks/bookmarks.go @@ -21,9 +21,8 @@ package bookmarks import ( "net/http" - "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" ) const ( @@ -31,20 +30,16 @@ const ( BasePath = "/api/v1/bookmarks" ) -// Module implements the ClientAPIModule interface for everything related to bookmarks type Module struct { processor processing.Processor } -// New returns a new emoji module -func New(processor processing.Processor) api.ClientModule { +func New(processor processing.Processor) *Module { return &Module{ processor: processor, } } -// Route attaches all routes from this module to the given router -func (m *Module) Route(r router.Router) error { - r.AttachHandler(http.MethodGet, BasePath, m.BookmarksGETHandler) - return nil +func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { + attachHandler(http.MethodGet, BasePath, m.BookmarksGETHandler) } diff --git a/internal/api/client/bookmarks/bookmarks_test.go b/internal/api/client/bookmarks/bookmarks_test.go index b4a4bdfb1..3bd12aee1 100644 --- a/internal/api/client/bookmarks/bookmarks_test.go +++ b/internal/api/client/bookmarks/bookmarks_test.go @@ -29,7 +29,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/api/client/bookmarks" - "github.com/superseriousbusiness/gotosocial/internal/api/client/status" + "github.com/superseriousbusiness/gotosocial/internal/api/client/statuses" "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/concurrency" "github.com/superseriousbusiness/gotosocial/internal/db" @@ -67,7 +67,7 @@ type BookmarkTestSuite struct { testFollows map[string]*gtsmodel.Follow // module being tested - statusModule *status.Module + statusModule *statuses.Module bookmarkModule *bookmarks.Module } @@ -99,8 +99,8 @@ func (suite *BookmarkTestSuite) SetupTest() { suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker), suite.storage, suite.mediaManager, fedWorker) suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil) suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker) - suite.statusModule = status.New(suite.processor).(*status.Module) - suite.bookmarkModule = bookmarks.New(suite.processor).(*bookmarks.Module) + suite.statusModule = statuses.New(suite.processor) + suite.bookmarkModule = bookmarks.New(suite.processor) suite.NoError(suite.processor.Start()) } @@ -123,7 +123,7 @@ func (suite *BookmarkTestSuite) TestGetBookmark() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.BookmarkPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.BookmarkPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting ctx.Request.Header.Set("accept", "application/json") suite.bookmarkModule.BookmarksGETHandler(ctx) diff --git a/internal/api/client/bookmarks/bookmarksget.go b/internal/api/client/bookmarks/bookmarksget.go index dafc896ef..8f587f13d 100644 --- a/internal/api/client/bookmarks/bookmarksget.go +++ b/internal/api/client/bookmarks/bookmarksget.go @@ -6,7 +6,7 @@ import ( "strconv" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -56,12 +56,12 @@ const ( func (m *Module) BookmarksGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } @@ -71,7 +71,7 @@ func (m *Module) BookmarksGETHandler(c *gin.Context) { i, err := strconv.ParseInt(limitString, 10, 64) if err != nil { err := fmt.Errorf("error parsing %s: %s", LimitKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } limit = int(i) @@ -91,12 +91,12 @@ func (m *Module) BookmarksGETHandler(c *gin.Context) { resp, errWithCode := m.processor.BookmarksGet(c.Request.Context(), authed, maxID, minID, limit) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/customemojis/customemojis.go b/internal/api/client/customemojis/customemojis.go new file mode 100644 index 000000000..ab89415d0 --- /dev/null +++ b/internal/api/client/customemojis/customemojis.go @@ -0,0 +1,45 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + 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 customemojis + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/processing" +) + +const ( + // BasePath is the base path for serving custom emojis, minus the 'api' prefix + BasePath = "/v1/custom_emojis" +) + +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, BasePath, m.CustomEmojisGETHandler) +} diff --git a/internal/api/client/customemojis/customemojisget.go b/internal/api/client/customemojis/customemojisget.go new file mode 100644 index 000000000..3428071d0 --- /dev/null +++ b/internal/api/client/customemojis/customemojisget.go @@ -0,0 +1,76 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + 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 customemojis + +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" +) + +// CustomEmojisGETHandler swagger:operation GET /api/v1/custom_emojis customEmojisGet +// +// Get an array of custom emojis available on the instance. +// +// --- +// tags: +// - custom_emojis +// +// produces: +// - application/json +// +// security: +// - OAuth2 Bearer: +// - read:custom_emojis +// +// responses: +// '200': +// description: Array of custom emojis. +// schema: +// type: array +// items: +// "$ref": "#/definitions/emoji" +// '401': +// description: unauthorized +// '406': +// description: not acceptable +// '500': +// description: internal server error +func (m *Module) CustomEmojisGETHandler(c *gin.Context) { + if _, err := oauth.Authed(c, true, true, true, true); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + return + } + + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + return + } + + emojis, errWithCode := m.processor.CustomEmojisGet(c) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + return + } + + c.JSON(http.StatusOK, emojis) +} diff --git a/internal/api/client/emoji/emojisget.go b/internal/api/client/emoji/emojisget.go deleted file mode 100644 index d41e5e7df..000000000 --- a/internal/api/client/emoji/emojisget.go +++ /dev/null @@ -1,58 +0,0 @@ -package emoji - -import ( - "net/http" - - "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/oauth" -) - -// EmojisGETHandler swagger:operation GET /api/v1/custom_emojis customEmojisGet -// -// Get an array of custom emojis available on the instance. -// -// --- -// tags: -// - custom_emojis -// -// produces: -// - application/json -// -// security: -// - OAuth2 Bearer: -// - read:custom_emojis -// -// responses: -// '200': -// description: Array of custom emojis. -// schema: -// type: array -// items: -// "$ref": "#/definitions/emoji" -// '401': -// description: unauthorized -// '406': -// description: not acceptable -// '500': -// description: internal server error -func (m *Module) EmojisGETHandler(c *gin.Context) { - if _, err := oauth.Authed(c, true, true, true, true); err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) - return - } - - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) - return - } - - emojis, errWithCode := m.processor.CustomEmojisGet(c) - if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) - return - } - - c.JSON(http.StatusOK, emojis) -} diff --git a/internal/api/client/favourites/favourites.go b/internal/api/client/favourites/favourites.go index f310d6873..5abc85a27 100644 --- a/internal/api/client/favourites/favourites.go +++ b/internal/api/client/favourites/favourites.go @@ -21,14 +21,13 @@ package favourites import ( "net/http" - "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" ) const ( - // BasePath is the base URI path for serving favourites - BasePath = "/api/v1/favourites" + // BasePath is the base URI path for serving favourites, minus the 'api' prefix + BasePath = "/v1/favourites" // MaxIDKey is the url query for setting a max status ID to return MaxIDKey = "max_id" @@ -42,20 +41,16 @@ const ( LocalKey = "local" ) -// Module implements the ClientAPIModule interface for everything relating to viewing favourites type Module struct { processor processing.Processor } -// New returns a new favourites module -func New(processor processing.Processor) api.ClientModule { +func New(processor processing.Processor) *Module { return &Module{ processor: processor, } } -// Route attaches all routes from this module to the given router -func (m *Module) Route(r router.Router) error { - r.AttachHandler(http.MethodGet, BasePath, m.FavouritesGETHandler) - return nil +func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { + attachHandler(http.MethodGet, BasePath, m.FavouritesGETHandler) } diff --git a/internal/api/client/favourites/favourites_test.go b/internal/api/client/favourites/favourites_test.go index c84da6b32..050b72536 100644 --- a/internal/api/client/favourites/favourites_test.go +++ b/internal/api/client/favourites/favourites_test.go @@ -87,7 +87,7 @@ func (suite *FavouritesStandardTestSuite) SetupTest() { suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker), suite.storage, suite.mediaManager, fedWorker) suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil) suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker) - suite.favModule = favourites.New(suite.processor).(*favourites.Module) + suite.favModule = favourites.New(suite.processor) suite.NoError(suite.processor.Start()) } diff --git a/internal/api/client/favourites/favouritesget.go b/internal/api/client/favourites/favouritesget.go index 5ff032b9a..9b6bb715e 100644 --- a/internal/api/client/favourites/favouritesget.go +++ b/internal/api/client/favourites/favouritesget.go @@ -6,7 +6,7 @@ import ( "strconv" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -78,12 +78,12 @@ import ( func (m *Module) FavouritesGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } @@ -105,7 +105,7 @@ func (m *Module) FavouritesGETHandler(c *gin.Context) { i, err := strconv.ParseInt(limitString, 10, 32) if err != nil { err := fmt.Errorf("error parsing %s: %s", LimitKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } limit = int(i) @@ -113,7 +113,7 @@ func (m *Module) FavouritesGETHandler(c *gin.Context) { resp, errWithCode := m.processor.FavedTimelineGet(c.Request.Context(), authed, maxID, minID, limit) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/filter/filter.go b/internal/api/client/filters/filter.go similarity index 62% rename from internal/api/client/filter/filter.go rename to internal/api/client/filters/filter.go index cf801e0a5..bdfd89ffe 100644 --- a/internal/api/client/filter/filter.go +++ b/internal/api/client/filters/filter.go @@ -21,30 +21,25 @@ package filter import ( "net/http" - "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" ) const ( - // BasePath is the base path for serving the filter API - BasePath = "/api/v1/filters" + // BasePath is the base path for serving the filters API, minus the 'api' prefix + BasePath = "/v1/filters" ) -// Module implements the ClientAPIModule interface for every related to filters type Module struct { processor processing.Processor } -// New returns a new filter module -func New(processor processing.Processor) api.ClientModule { +func New(processor processing.Processor) *Module { return &Module{ processor: processor, } } -// Route attaches all routes from this module to the given router -func (m *Module) Route(r router.Router) error { - r.AttachHandler(http.MethodGet, BasePath, m.FiltersGETHandler) - return nil +func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { + attachHandler(http.MethodGet, BasePath, m.FiltersGETHandler) } diff --git a/internal/api/client/filter/filtersget.go b/internal/api/client/filters/filtersget.go similarity index 55% rename from internal/api/client/filter/filtersget.go rename to internal/api/client/filters/filtersget.go index 8e0a0bb34..71d6cac3e 100644 --- a/internal/api/client/filter/filtersget.go +++ b/internal/api/client/filters/filtersget.go @@ -4,7 +4,7 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -12,12 +12,12 @@ import ( // FiltersGETHandler returns a list of filters set by/for the authed account func (m *Module) FiltersGETHandler(c *gin.Context) { if _, err := oauth.Authed(c, true, true, true, true); err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } diff --git a/internal/api/client/followrequest/authorize.go b/internal/api/client/followrequests/authorize.go similarity index 81% rename from internal/api/client/followrequest/authorize.go rename to internal/api/client/followrequests/authorize.go index a5a392f76..d30bb979f 100644 --- a/internal/api/client/followrequest/authorize.go +++ b/internal/api/client/followrequests/authorize.go @@ -16,14 +16,14 @@ along with this program. If not, see . */ -package followrequest +package followrequests import ( "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -72,25 +72,25 @@ import ( func (m *Module) FollowRequestAuthorizePOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } originAccountID := c.Param(IDKey) if originAccountID == "" { err := errors.New("no account id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } relationship, errWithCode := m.processor.FollowRequestAccept(c.Request.Context(), authed, originAccountID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/followrequest/authorize_test.go b/internal/api/client/followrequests/authorize_test.go similarity index 96% rename from internal/api/client/followrequest/authorize_test.go rename to internal/api/client/followrequests/authorize_test.go index 693380d91..048c462c7 100644 --- a/internal/api/client/followrequest/authorize_test.go +++ b/internal/api/client/followrequests/authorize_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package followrequest_test +package followrequests_test import ( "context" @@ -30,7 +30,7 @@ import ( "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/client/followrequest" + "github.com/superseriousbusiness/gotosocial/internal/api/client/followrequests" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) @@ -60,7 +60,7 @@ func (suite *AuthorizeTestSuite) TestAuthorize() { ctx.Params = gin.Params{ gin.Param{ - Key: followrequest.IDKey, + Key: followrequests.IDKey, Value: requestingAccount.ID, }, } @@ -90,7 +90,7 @@ func (suite *AuthorizeTestSuite) TestAuthorizeNoFR() { ctx.Params = gin.Params{ gin.Param{ - Key: followrequest.IDKey, + Key: followrequests.IDKey, Value: requestingAccount.ID, }, } diff --git a/internal/api/client/followrequest/followrequest.go b/internal/api/client/followrequests/followrequest.go similarity index 67% rename from internal/api/client/followrequest/followrequest.go rename to internal/api/client/followrequests/followrequest.go index a511d7226..d9d241e63 100644 --- a/internal/api/client/followrequest/followrequest.go +++ b/internal/api/client/followrequests/followrequest.go @@ -16,21 +16,20 @@ along with this program. If not, see . */ -package followrequest +package followrequests import ( "net/http" - "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" ) const ( // IDKey is for account IDs IDKey = "id" - // BasePath is the base path for serving the follow request API - BasePath = "/api/v1/follow_requests" + // BasePath is the base path for serving the follow request API, minus the 'api' prefix + BasePath = "/v1/follow_requests" // BasePathWithID is just the base path with the ID key in it. // Use this anywhere you need to know the ID of the account that owns the follow request being queried. BasePathWithID = BasePath + "/:" + IDKey @@ -40,22 +39,18 @@ const ( RejectPath = BasePathWithID + "/reject" ) -// Module implements the ClientAPIModule interface type Module struct { processor processing.Processor } -// New returns a new follow request module -func New(processor processing.Processor) api.ClientModule { +func New(processor processing.Processor) *Module { return &Module{ processor: processor, } } -// Route attaches all routes from this module to the given router -func (m *Module) Route(r router.Router) error { - r.AttachHandler(http.MethodGet, BasePath, m.FollowRequestGETHandler) - r.AttachHandler(http.MethodPost, AuthorizePath, m.FollowRequestAuthorizePOSTHandler) - r.AttachHandler(http.MethodPost, RejectPath, m.FollowRequestRejectPOSTHandler) - return nil +func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { + attachHandler(http.MethodGet, BasePath, m.FollowRequestGETHandler) + attachHandler(http.MethodPost, AuthorizePath, m.FollowRequestAuthorizePOSTHandler) + attachHandler(http.MethodPost, RejectPath, m.FollowRequestRejectPOSTHandler) } diff --git a/internal/api/client/followrequest/followrequest_test.go b/internal/api/client/followrequests/followrequest_test.go similarity index 96% rename from internal/api/client/followrequest/followrequest_test.go rename to internal/api/client/followrequests/followrequest_test.go index ca00ea054..c8036cd24 100644 --- a/internal/api/client/followrequest/followrequest_test.go +++ b/internal/api/client/followrequests/followrequest_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package followrequest_test +package followrequests_test import ( "bytes" @@ -25,7 +25,7 @@ import ( "github.com/gin-gonic/gin" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/client/followrequest" + "github.com/superseriousbusiness/gotosocial/internal/api/client/followrequests" "github.com/superseriousbusiness/gotosocial/internal/concurrency" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" @@ -59,7 +59,7 @@ type FollowRequestStandardTestSuite struct { testStatuses map[string]*gtsmodel.Status // module being tested - followRequestModule *followrequest.Module + followRequestModule *followrequests.Module } func (suite *FollowRequestStandardTestSuite) SetupSuite() { @@ -85,7 +85,7 @@ func (suite *FollowRequestStandardTestSuite) SetupTest() { suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker), suite.storage, suite.mediaManager, fedWorker) suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil) suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker) - suite.followRequestModule = followrequest.New(suite.processor).(*followrequest.Module) + suite.followRequestModule = followrequests.New(suite.processor) testrig.StandardDBSetup(suite.db, nil) testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") diff --git a/internal/api/client/followrequest/get.go b/internal/api/client/followrequests/get.go similarity index 83% rename from internal/api/client/followrequest/get.go rename to internal/api/client/followrequests/get.go index 8a2be3686..1153f0f4b 100644 --- a/internal/api/client/followrequest/get.go +++ b/internal/api/client/followrequests/get.go @@ -16,13 +16,13 @@ along with this program. If not, see . */ -package followrequest +package followrequests import ( "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -74,18 +74,18 @@ import ( func (m *Module) FollowRequestGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } accts, errWithCode := m.processor.FollowRequestsGet(c.Request.Context(), authed) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/followrequest/get_test.go b/internal/api/client/followrequests/get_test.go similarity index 98% rename from internal/api/client/followrequest/get_test.go rename to internal/api/client/followrequests/get_test.go index c9b72a35b..d4c9da0a1 100644 --- a/internal/api/client/followrequest/get_test.go +++ b/internal/api/client/followrequests/get_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package followrequest_test +package followrequests_test import ( "context" diff --git a/internal/api/client/followrequest/reject.go b/internal/api/client/followrequests/reject.go similarity index 80% rename from internal/api/client/followrequest/reject.go rename to internal/api/client/followrequests/reject.go index 717dbf4dd..782f932cd 100644 --- a/internal/api/client/followrequest/reject.go +++ b/internal/api/client/followrequests/reject.go @@ -16,14 +16,14 @@ along with this program. If not, see . */ -package followrequest +package followrequests import ( "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -70,25 +70,25 @@ import ( func (m *Module) FollowRequestRejectPOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } originAccountID := c.Param(IDKey) if originAccountID == "" { err := errors.New("no account id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } relationship, errWithCode := m.processor.FollowRequestReject(c.Request.Context(), authed, originAccountID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/followrequest/reject_test.go b/internal/api/client/followrequests/reject_test.go similarity index 97% rename from internal/api/client/followrequest/reject_test.go rename to internal/api/client/followrequests/reject_test.go index 94c646ddc..cea42829d 100644 --- a/internal/api/client/followrequest/reject_test.go +++ b/internal/api/client/followrequests/reject_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package followrequest_test +package followrequests_test import ( "context" @@ -30,7 +30,7 @@ import ( "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/client/followrequest" + "github.com/superseriousbusiness/gotosocial/internal/api/client/followrequests" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) @@ -60,7 +60,7 @@ func (suite *RejectTestSuite) TestReject() { ctx.Params = gin.Params{ gin.Param{ - Key: followrequest.IDKey, + Key: followrequests.IDKey, Value: requestingAccount.ID, }, } diff --git a/internal/api/client/instance/instance.go b/internal/api/client/instance/instance.go index 16ff7c9f9..101e8cea4 100644 --- a/internal/api/client/instance/instance.go +++ b/internal/api/client/instance/instance.go @@ -21,36 +21,31 @@ package instance import ( "net/http" - "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" ) const ( - // InstanceInformationPath is for serving instance info requests - InstanceInformationPath = "api/v1/instance" + // InstanceInformationPath is for serving instance info requests, minus the 'api' prefix. + InstanceInformationPath = "/v1/instance" // InstancePeersPath is for serving instance peers requests. InstancePeersPath = InstanceInformationPath + "/peers" // PeersFilterKey is used to provide filters to /api/v1/instance/peers PeersFilterKey = "filter" ) -// Module implements the ClientModule interface type Module struct { processor processing.Processor } -// New returns a new instance information module -func New(processor processing.Processor) api.ClientModule { +func New(processor processing.Processor) *Module { return &Module{ processor: processor, } } -// Route satisfies the ClientModule interface -func (m *Module) Route(s router.Router) error { - s.AttachHandler(http.MethodGet, InstanceInformationPath, m.InstanceInformationGETHandler) - s.AttachHandler(http.MethodPatch, InstanceInformationPath, m.InstanceUpdatePATCHHandler) - s.AttachHandler(http.MethodGet, InstancePeersPath, m.InstancePeersGETHandler) - return nil +func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { + attachHandler(http.MethodGet, InstanceInformationPath, m.InstanceInformationGETHandler) + attachHandler(http.MethodPatch, InstanceInformationPath, m.InstanceUpdatePATCHHandler) + attachHandler(http.MethodGet, InstancePeersPath, m.InstancePeersGETHandler) } diff --git a/internal/api/client/instance/instance_test.go b/internal/api/client/instance/instance_test.go index 26f29027d..33efbc847 100644 --- a/internal/api/client/instance/instance_test.go +++ b/internal/api/client/instance/instance_test.go @@ -88,7 +88,7 @@ func (suite *InstanceStandardTestSuite) SetupTest() { suite.sentEmails = make(map[string]string) suite.emailSender = testrig.NewEmailSender("../../../../web/template/", suite.sentEmails) suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker) - suite.instanceModule = instance.New(suite.processor).(*instance.Module) + suite.instanceModule = instance.New(suite.processor) testrig.StandardDBSetup(suite.db, nil) testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") } diff --git a/internal/api/client/instance/instanceget.go b/internal/api/client/instance/instanceget.go index bcedf398b..dfb8330ff 100644 --- a/internal/api/client/instance/instanceget.go +++ b/internal/api/client/instance/instanceget.go @@ -21,7 +21,7 @@ package instance import ( "net/http" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" @@ -49,14 +49,14 @@ import ( // '500': // description: internal error func (m *Module) InstanceInformationGETHandler(c *gin.Context) { - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } instance, errWithCode := m.processor.InstanceGet(c.Request.Context(), config.GetHost()) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/instance/instancepatch.go b/internal/api/client/instance/instancepatch.go index d4fa8ca5d..891ce8e38 100644 --- a/internal/api/client/instance/instancepatch.go +++ b/internal/api/client/instance/instancepatch.go @@ -24,8 +24,8 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -130,42 +130,42 @@ import ( func (m *Module) InstanceUpdatePATCHHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } if !*authed.User.Admin { err := errors.New("user is not an admin so cannot update instance settings") - api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) return } - form := &model.InstanceSettingsUpdateRequest{} + form := &apimodel.InstanceSettingsUpdateRequest{} if err := c.ShouldBind(&form); err != nil { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } if err := validateInstanceUpdate(form); err != nil { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } i, errWithCode := m.processor.InstancePatch(c.Request.Context(), form) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } c.JSON(http.StatusOK, i) } -func validateInstanceUpdate(form *model.InstanceSettingsUpdateRequest) error { +func validateInstanceUpdate(form *apimodel.InstanceSettingsUpdateRequest) error { if form.Title == nil && form.ContactUsername == nil && form.ContactEmail == nil && diff --git a/internal/api/client/instance/instancepeersget.go b/internal/api/client/instance/instancepeersget.go index f7d05acdc..de6e40e7c 100644 --- a/internal/api/client/instance/instancepeersget.go +++ b/internal/api/client/instance/instancepeersget.go @@ -23,7 +23,7 @@ import ( "net/http" "strings" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -101,12 +101,12 @@ import ( func (m *Module) InstancePeersGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, false, false, false, false) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } @@ -124,7 +124,7 @@ func (m *Module) InstancePeersGETHandler(c *gin.Context) { includeOpen = true default: err := fmt.Errorf("filter %s not recognized; accepted values are 'open', 'suspended'", trimmed) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } } @@ -138,7 +138,7 @@ func (m *Module) InstancePeersGETHandler(c *gin.Context) { data, errWithCode := m.processor.InstancePeersGet(c.Request.Context(), authed, includeSuspended, includeOpen, flat) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/list/listsgets.go b/internal/api/client/list/listsgets.go deleted file mode 100644 index 246a1216a..000000000 --- a/internal/api/client/list/listsgets.go +++ /dev/null @@ -1,25 +0,0 @@ -package list - -import ( - "net/http" - - "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/oauth" -) - -// ListsGETHandler returns a list of lists created by/for the authed account -func (m *Module) ListsGETHandler(c *gin.Context) { - if _, err := oauth.Authed(c, true, true, true, true); err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) - return - } - - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) - return - } - - c.JSON(http.StatusOK, []string{}) -} diff --git a/internal/api/client/list/list.go b/internal/api/client/lists/list.go similarity index 62% rename from internal/api/client/list/list.go rename to internal/api/client/lists/list.go index c64ada43e..c14917b98 100644 --- a/internal/api/client/list/list.go +++ b/internal/api/client/lists/list.go @@ -16,35 +16,30 @@ along with this program. If not, see . */ -package list +package lists import ( "net/http" - "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" ) const ( - // BasePath is the base path for serving the lists API - BasePath = "/api/v1/lists" + // BasePath is the base path for serving the lists API, minus the 'api' prefix + BasePath = "/v1/lists" ) -// Module implements the ClientAPIModule interface for everything related to lists type Module struct { processor processing.Processor } -// New returns a new list module -func New(processor processing.Processor) api.ClientModule { +func New(processor processing.Processor) *Module { return &Module{ processor: processor, } } -// Route attaches all routes from this module to the given router -func (m *Module) Route(r router.Router) error { - r.AttachHandler(http.MethodGet, BasePath, m.ListsGETHandler) - return nil +func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { + attachHandler(http.MethodGet, BasePath, m.ListsGETHandler) } diff --git a/internal/api/security/robots.go b/internal/api/client/lists/listsgets.go similarity index 50% rename from internal/api/security/robots.go rename to internal/api/client/lists/listsgets.go index 5b8ba3c05..a4e5cbefa 100644 --- a/internal/api/security/robots.go +++ b/internal/api/client/lists/listsgets.go @@ -16,42 +16,29 @@ along with this program. If not, see . */ -package security +package lists 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" ) -const robotsString = `User-agent: * -Crawl-delay: 500 -# api stuff -Disallow: /api/ -# auth/login stuff -Disallow: /auth/ -Disallow: /oauth/ -Disallow: /check_your_email -Disallow: /wait_for_approval -Disallow: /account_disabled -# well known stuff -Disallow: /.well-known/ -# files -Disallow: /fileserver/ -# s2s AP stuff -Disallow: /users/ -Disallow: /emoji/ -# panels -Disallow: /admin -Disallow: /user -Disallow: /settings/ -` +// ListsGETHandler returns a list of lists created by/for the authed account +func (m *Module) ListsGETHandler(c *gin.Context) { + if _, err := oauth.Authed(c, true, true, true, true); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + return + } -// RobotsGETHandler returns a decent robots.txt that prevents crawling -// the api, auth pages, settings pages, etc. -// -// More granular robots meta tags are then applied for web pages -// depending on user preferences (see internal/web). -func (m *Module) RobotsGETHandler(c *gin.Context) { - c.String(http.StatusOK, robotsString) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + return + } + + // todo: implement this; currently it's a no-op + c.JSON(http.StatusOK, []string{}) } diff --git a/internal/api/client/media/media.go b/internal/api/client/media/media.go index 87cc2f091..889a4f3df 100644 --- a/internal/api/client/media/media.go +++ b/internal/api/client/media/media.go @@ -21,34 +21,31 @@ package media import ( "net/http" - "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" ) const ( - IDKey = "id" // IDKey is the key for media attachment IDs - APIVersionKey = "api_version" // APIVersionKey is the key for which version of the API to use (v1 or v2) - BasePathWithAPIVersion = "/api/:" + APIVersionKey + "/media" // BasePathWithAPIVersion is the base API path for making media requests through v1 or v2 of the api (for mastodon API compatibility) - BasePathWithIDV1 = "/api/v1/media/:" + IDKey // BasePathWithID corresponds to a media attachment with the given ID + IDKey = "id" // IDKey is the key for media attachment IDs + APIVersionKey = "api_version" // APIVersionKey is the key for which version of the API to use (v1 or v2) + APIv1 = "v1" // APIV1 corresponds to version 1 of the api + APIv2 = "v2" // APIV2 corresponds to version 2 of the api + BasePath = "/:" + APIVersionKey + "/media" // BasePath is the base API path for making media requests through v1 or v2 of the api (for mastodon API compatibility) + AttachmentWithID = BasePath + "/:" + IDKey // BasePathWithID corresponds to a media attachment with the given ID ) -// Module implements the ClientAPIModule interface for media type Module struct { processor processing.Processor } -// New returns a new auth module -func New(processor processing.Processor) api.ClientModule { +func New(processor processing.Processor) *Module { return &Module{ processor: processor, } } -// Route satisfies the RESTAPIModule interface -func (m *Module) Route(s router.Router) error { - s.AttachHandler(http.MethodPost, BasePathWithAPIVersion, m.MediaCreatePOSTHandler) - s.AttachHandler(http.MethodGet, BasePathWithIDV1, m.MediaGETHandler) - s.AttachHandler(http.MethodPut, BasePathWithIDV1, m.MediaPUTHandler) - return nil +func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { + attachHandler(http.MethodPost, BasePath, m.MediaCreatePOSTHandler) + attachHandler(http.MethodGet, AttachmentWithID, m.MediaGETHandler) + attachHandler(http.MethodPut, AttachmentWithID, m.MediaPUTHandler) } diff --git a/internal/api/client/media/mediacreate.go b/internal/api/client/media/mediacreate.go index db8b2ea56..7e29b2bb3 100644 --- a/internal/api/client/media/mediacreate.go +++ b/internal/api/client/media/mediacreate.go @@ -24,8 +24,8 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -94,42 +94,42 @@ import ( // '500': // description: internal server error func (m *Module) MediaCreatePOSTHandler(c *gin.Context) { + apiVersion := c.Param(APIVersionKey) + if apiVersion != APIv1 && apiVersion != APIv2 { + err := errors.New("api version must be one of v1 or v2 for this path") + apiutil.ErrorHandler(c, gtserror.NewErrorNotFound(err, err.Error()), m.processor.InstanceGet) + return + } + authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - apiVersion := c.Param(APIVersionKey) - if apiVersion != "v1" && apiVersion != "v2" { - err := errors.New("api version must be one of v1 or v2") - api.ErrorHandler(c, gtserror.NewErrorNotFound(err, err.Error()), m.processor.InstanceGet) - return - } - - form := &model.AttachmentRequest{} + form := &apimodel.AttachmentRequest{} if err := c.ShouldBind(&form); err != nil { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } if err := validateCreateMedia(form); err != nil { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } apiAttachment, errWithCode := m.processor.MediaCreate(c.Request.Context(), authed, form) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } - if apiVersion == "v2" { + if apiVersion == APIv2 { // the mastodon v2 media API specifies that the URL should be null // and that the client should call /api/v1/media/:id to get the URL // @@ -141,7 +141,7 @@ func (m *Module) MediaCreatePOSTHandler(c *gin.Context) { c.JSON(http.StatusOK, apiAttachment) } -func validateCreateMedia(form *model.AttachmentRequest) error { +func validateCreateMedia(form *apimodel.AttachmentRequest) error { // check there actually is a file attached and it's not size 0 if form.File == nil { return errors.New("no attachment given") diff --git a/internal/api/client/media/mediacreate_test.go b/internal/api/client/media/mediacreate_test.go index 2f6fb12a4..9e787b4b9 100644 --- a/internal/api/client/media/mediacreate_test.go +++ b/internal/api/client/media/mediacreate_test.go @@ -30,10 +30,9 @@ import ( "net/http/httptest" "testing" - "github.com/gin-gonic/gin" "github.com/stretchr/testify/suite" mediamodule "github.com/superseriousbusiness/gotosocial/internal/api/client/media" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/concurrency" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" @@ -96,7 +95,7 @@ func (suite *MediaCreateTestSuite) SetupSuite() { suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker) // setup module being tested - suite.mediaModule = mediamodule.New(suite.processor).(*mediamodule.Module) + suite.mediaModule = mediamodule.New(suite.processor) } func (suite *MediaCreateTestSuite) TearDownSuite() { @@ -158,12 +157,7 @@ func (suite *MediaCreateTestSuite) TestMediaCreateSuccessful() { ctx.Request = httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/media", bytes.NewReader(buf.Bytes())) // the endpoint we're hitting ctx.Request.Header.Set("Content-Type", w.FormDataContentType()) ctx.Request.Header.Set("accept", "application/json") - ctx.Params = gin.Params{ - gin.Param{ - Key: mediamodule.APIVersionKey, - Value: "v1", - }, - } + ctx.AddParam(mediamodule.APIVersionKey, mediamodule.APIv1) // do the actual request suite.mediaModule.MediaCreatePOSTHandler(ctx) @@ -188,26 +182,26 @@ func (suite *MediaCreateTestSuite) TestMediaCreateSuccessful() { suite.NoError(err) fmt.Println(string(b)) - attachmentReply := &model.Attachment{} + attachmentReply := &apimodel.Attachment{} err = json.Unmarshal(b, attachmentReply) suite.NoError(err) suite.Equal("this is a test image -- a cool background from somewhere", *attachmentReply.Description) suite.Equal("image", attachmentReply.Type) - suite.EqualValues(model.MediaMeta{ - Original: model.MediaDimensions{ + suite.EqualValues(apimodel.MediaMeta{ + Original: apimodel.MediaDimensions{ Width: 1920, Height: 1080, Size: "1920x1080", Aspect: 1.7777778, }, - Small: model.MediaDimensions{ + Small: apimodel.MediaDimensions{ Width: 512, Height: 288, Size: "512x288", Aspect: 1.7777778, }, - Focus: model.MediaFocus{ + Focus: apimodel.MediaFocus{ X: -0.5, Y: 0.5, }, @@ -252,12 +246,7 @@ func (suite *MediaCreateTestSuite) TestMediaCreateSuccessfulV2() { ctx.Request = httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v2/media", bytes.NewReader(buf.Bytes())) // the endpoint we're hitting ctx.Request.Header.Set("Content-Type", w.FormDataContentType()) ctx.Request.Header.Set("accept", "application/json") - ctx.Params = gin.Params{ - gin.Param{ - Key: mediamodule.APIVersionKey, - Value: "v2", - }, - } + ctx.AddParam(mediamodule.APIVersionKey, mediamodule.APIv2) // do the actual request suite.mediaModule.MediaCreatePOSTHandler(ctx) @@ -282,26 +271,26 @@ func (suite *MediaCreateTestSuite) TestMediaCreateSuccessfulV2() { suite.NoError(err) fmt.Println(string(b)) - attachmentReply := &model.Attachment{} + attachmentReply := &apimodel.Attachment{} err = json.Unmarshal(b, attachmentReply) suite.NoError(err) suite.Equal("this is a test image -- a cool background from somewhere", *attachmentReply.Description) suite.Equal("image", attachmentReply.Type) - suite.EqualValues(model.MediaMeta{ - Original: model.MediaDimensions{ + suite.EqualValues(apimodel.MediaMeta{ + Original: apimodel.MediaDimensions{ Width: 1920, Height: 1080, Size: "1920x1080", Aspect: 1.7777778, }, - Small: model.MediaDimensions{ + Small: apimodel.MediaDimensions{ Width: 512, Height: 288, Size: "512x288", Aspect: 1.7777778, }, - Focus: model.MediaFocus{ + Focus: apimodel.MediaFocus{ X: -0.5, Y: 0.5, }, @@ -342,12 +331,7 @@ func (suite *MediaCreateTestSuite) TestMediaCreateLongDescription() { ctx.Request = httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/media", bytes.NewReader(buf.Bytes())) // the endpoint we're hitting ctx.Request.Header.Set("Content-Type", w.FormDataContentType()) ctx.Request.Header.Set("accept", "application/json") - ctx.Params = gin.Params{ - gin.Param{ - Key: mediamodule.APIVersionKey, - Value: "v1", - }, - } + ctx.AddParam(mediamodule.APIVersionKey, mediamodule.APIv1) // do the actual request suite.mediaModule.MediaCreatePOSTHandler(ctx) @@ -388,12 +372,7 @@ func (suite *MediaCreateTestSuite) TestMediaCreateTooShortDescription() { ctx.Request = httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/media", bytes.NewReader(buf.Bytes())) // the endpoint we're hitting ctx.Request.Header.Set("Content-Type", w.FormDataContentType()) ctx.Request.Header.Set("accept", "application/json") - ctx.Params = gin.Params{ - gin.Param{ - Key: mediamodule.APIVersionKey, - Value: "v1", - }, - } + ctx.AddParam(mediamodule.APIVersionKey, mediamodule.APIv1) // do the actual request suite.mediaModule.MediaCreatePOSTHandler(ctx) diff --git a/internal/api/client/media/mediaget.go b/internal/api/client/media/mediaget.go index fd232c4c7..b22c8e79c 100644 --- a/internal/api/client/media/mediaget.go +++ b/internal/api/client/media/mediaget.go @@ -23,7 +23,7 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -67,27 +67,33 @@ import ( // '500': // description: internal server error func (m *Module) MediaGETHandler(c *gin.Context) { - authed, err := oauth.Authed(c, true, true, true, true) - if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + if apiVersion := c.Param(APIVersionKey); apiVersion != APIv1 { + err := errors.New("api version must be one v1 for this path") + apiutil.ErrorHandler(c, gtserror.NewErrorNotFound(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + authed, err := oauth.Authed(c, true, true, true, true) + if err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + return + } + + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } attachmentID := c.Param(IDKey) if attachmentID == "" { err := errors.New("no attachment id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } attachment, errWithCode := m.processor.MediaGet(c.Request.Context(), authed, attachmentID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/media/mediaupdate.go b/internal/api/client/media/mediaupdate.go index 438eaca23..9cfd8a5f1 100644 --- a/internal/api/client/media/mediaupdate.go +++ b/internal/api/client/media/mediaupdate.go @@ -24,8 +24,8 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -99,45 +99,51 @@ import ( // '500': // description: internal server error func (m *Module) MediaPUTHandler(c *gin.Context) { - authed, err := oauth.Authed(c, true, true, true, true) - if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + if apiVersion := c.Param(APIVersionKey); apiVersion != APIv1 { + err := errors.New("api version must be one v1 for this path") + apiutil.ErrorHandler(c, gtserror.NewErrorNotFound(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + authed, err := oauth.Authed(c, true, true, true, true) + if err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + return + } + + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } attachmentID := c.Param(IDKey) if attachmentID == "" { err := errors.New("no attachment id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - form := &model.AttachmentUpdateRequest{} + form := &apimodel.AttachmentUpdateRequest{} if err := c.ShouldBind(form); err != nil { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } if err := validateUpdateMedia(form); err != nil { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } attachment, errWithCode := m.processor.MediaUpdate(c.Request.Context(), authed, attachmentID, form) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } c.JSON(http.StatusOK, attachment) } -func validateUpdateMedia(form *model.AttachmentUpdateRequest) error { +func validateUpdateMedia(form *apimodel.AttachmentUpdateRequest) error { minDescriptionChars := config.GetMediaDescriptionMinChars() maxDescriptionChars := config.GetMediaDescriptionMaxChars() diff --git a/internal/api/client/media/mediaupdate_test.go b/internal/api/client/media/mediaupdate_test.go index e5abb0a91..bcf9a4dfe 100644 --- a/internal/api/client/media/mediaupdate_test.go +++ b/internal/api/client/media/mediaupdate_test.go @@ -28,10 +28,9 @@ import ( "net/http/httptest" "testing" - "github.com/gin-gonic/gin" "github.com/stretchr/testify/suite" mediamodule "github.com/superseriousbusiness/gotosocial/internal/api/client/media" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/concurrency" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" @@ -94,7 +93,7 @@ func (suite *MediaUpdateTestSuite) SetupSuite() { suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker) // setup module being tested - suite.mediaModule = mediamodule.New(suite.processor).(*mediamodule.Module) + suite.mediaModule = mediamodule.New(suite.processor) } func (suite *MediaUpdateTestSuite) TearDownSuite() { @@ -148,12 +147,8 @@ func (suite *MediaUpdateTestSuite) TestUpdateImage() { ctx.Request = httptest.NewRequest(http.MethodPut, fmt.Sprintf("http://localhost:8080/api/v1/media/%s", toUpdate.ID), bytes.NewReader(buf.Bytes())) // the endpoint we're hitting ctx.Request.Header.Set("Content-Type", w.FormDataContentType()) ctx.Request.Header.Set("accept", "application/json") - ctx.Params = gin.Params{ - gin.Param{ - Key: mediamodule.IDKey, - Value: toUpdate.ID, - }, - } + ctx.AddParam(mediamodule.APIVersionKey, mediamodule.APIv1) + ctx.AddParam(mediamodule.IDKey, toUpdate.ID) // do the actual request suite.mediaModule.MediaPUTHandler(ctx) @@ -167,17 +162,17 @@ func (suite *MediaUpdateTestSuite) TestUpdateImage() { suite.NoError(err) // reply should be an attachment - attachmentReply := &model.Attachment{} + attachmentReply := &apimodel.Attachment{} err = json.Unmarshal(b, attachmentReply) suite.NoError(err) // the reply should contain the updated fields suite.Equal("new description!", *attachmentReply.Description) suite.EqualValues("image", attachmentReply.Type) - suite.EqualValues(model.MediaMeta{ - Original: model.MediaDimensions{Width: 800, Height: 450, FrameRate: "", Duration: 0, Bitrate: 0, Size: "800x450", Aspect: 1.7777778}, - Small: model.MediaDimensions{Width: 256, Height: 144, FrameRate: "", Duration: 0, Bitrate: 0, Size: "256x144", Aspect: 1.7777778}, - Focus: model.MediaFocus{X: -0.1, Y: 0.3}, + suite.EqualValues(apimodel.MediaMeta{ + Original: apimodel.MediaDimensions{Width: 800, Height: 450, FrameRate: "", Duration: 0, Bitrate: 0, Size: "800x450", Aspect: 1.7777778}, + Small: apimodel.MediaDimensions{Width: 256, Height: 144, FrameRate: "", Duration: 0, Bitrate: 0, Size: "256x144", Aspect: 1.7777778}, + Focus: apimodel.MediaFocus{X: -0.1, Y: 0.3}, }, attachmentReply.Meta) suite.Equal(toUpdate.Blurhash, attachmentReply.Blurhash) suite.Equal(toUpdate.ID, attachmentReply.ID) @@ -213,12 +208,8 @@ func (suite *MediaUpdateTestSuite) TestUpdateImageShortDescription() { ctx.Request = httptest.NewRequest(http.MethodPut, fmt.Sprintf("http://localhost:8080/api/v1/media/%s", toUpdate.ID), bytes.NewReader(buf.Bytes())) // the endpoint we're hitting ctx.Request.Header.Set("Content-Type", w.FormDataContentType()) ctx.Request.Header.Set("accept", "application/json") - ctx.Params = gin.Params{ - gin.Param{ - Key: mediamodule.IDKey, - Value: toUpdate.ID, - }, - } + ctx.AddParam(mediamodule.APIVersionKey, mediamodule.APIv1) + ctx.AddParam(mediamodule.IDKey, toUpdate.ID) // do the actual request suite.mediaModule.MediaPUTHandler(ctx) diff --git a/internal/api/client/notification/notification.go b/internal/api/client/notifications/notifications.go similarity index 68% rename from internal/api/client/notification/notification.go rename to internal/api/client/notifications/notifications.go index 6ade0b02f..235f0a678 100644 --- a/internal/api/client/notification/notification.go +++ b/internal/api/client/notifications/notifications.go @@ -16,21 +16,20 @@ along with this program. If not, see . */ -package notification +package notifications import ( "net/http" - "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" ) const ( // IDKey is for notification UUIDs IDKey = "id" - // BasePath is the base path for serving the notification API - BasePath = "/api/v1/notifications" + // BasePath is the base path for serving the notification API, minus the 'api' prefix. + BasePath = "/v1/notifications" // BasePathWithID is just the base path with the ID key in it. // Use this anywhere you need to know the ID of the notification being queried. BasePathWithID = BasePath + "/:" + IDKey @@ -46,21 +45,17 @@ const ( SinceIDKey = "since_id" ) -// Module implements the ClientAPIModule interface for every related to posting/deleting/interacting with notifications type Module struct { processor processing.Processor } -// New returns a new notification module -func New(processor processing.Processor) api.ClientModule { +func New(processor processing.Processor) *Module { return &Module{ processor: processor, } } -// Route attaches all routes from this module to the given router -func (m *Module) Route(r router.Router) error { - r.AttachHandler(http.MethodGet, BasePath, m.NotificationsGETHandler) - r.AttachHandler(http.MethodPost, BasePathWithClear, m.NotificationsClearPOSTHandler) - return nil +func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { + attachHandler(http.MethodGet, BasePath, m.NotificationsGETHandler) + attachHandler(http.MethodPost, BasePathWithClear, m.NotificationsClearPOSTHandler) } diff --git a/internal/api/client/notification/notificationsclear.go b/internal/api/client/notifications/notificationsclear.go similarity index 80% rename from internal/api/client/notification/notificationsclear.go rename to internal/api/client/notifications/notificationsclear.go index b97371638..48c074504 100644 --- a/internal/api/client/notification/notificationsclear.go +++ b/internal/api/client/notifications/notificationsclear.go @@ -16,13 +16,13 @@ along with this program. If not, see . */ -package notification +package notifications import ( "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -61,18 +61,18 @@ import ( func (m *Module) NotificationsClearPOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } errWithCode := m.processor.NotificationsClear(c.Request.Context(), authed) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/notification/notificationsget.go b/internal/api/client/notifications/notificationsget.go similarity index 87% rename from internal/api/client/notification/notificationsget.go rename to internal/api/client/notifications/notificationsget.go index d6b3f5162..09000d02a 100644 --- a/internal/api/client/notification/notificationsget.go +++ b/internal/api/client/notifications/notificationsget.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package notification +package notifications import ( "fmt" @@ -24,7 +24,7 @@ import ( "strconv" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -111,12 +111,12 @@ import ( func (m *Module) NotificationsGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } @@ -126,7 +126,7 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) { i, err := strconv.ParseInt(limitString, 10, 32) if err != nil { err := fmt.Errorf("error parsing %s: %s", LimitKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } limit = int(i) @@ -148,7 +148,7 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) { resp, errWithCode := m.processor.NotificationsGet(c.Request.Context(), authed, excludeTypes, limit, maxID, sinceID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/search/search.go b/internal/api/client/search/search.go index 71370a6d5..bebe0bd61 100644 --- a/internal/api/client/search/search.go +++ b/internal/api/client/search/search.go @@ -21,17 +21,16 @@ package search import ( "net/http" - "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" ) const ( - // BasePathV1 is the base path for serving v1 of the search API - BasePathV1 = "/api/v1/search" + // BasePathV1 is the base path for serving v1 of the search API, minus the 'api' prefix + BasePathV1 = "/v1/search" - // BasePathV2 is the base path for serving v2 of the search API - BasePathV2 = "/api/v2/search" + // BasePathV2 is the base path for serving v2 of the search API, minus the 'api' prefix + BasePathV2 = "/v2/search" // AccountIDKey -- If provided, statuses returned will be authored only by this account AccountIDKey = "account_id" @@ -62,21 +61,17 @@ const ( TypeStatuses = "statuses" ) -// Module implements the ClientAPIModule interface for everything related to searching type Module struct { processor processing.Processor } -// New returns a new search module -func New(processor processing.Processor) api.ClientModule { +func New(processor processing.Processor) *Module { return &Module{ processor: processor, } } -// Route attaches all routes from this module to the given router -func (m *Module) Route(r router.Router) error { - r.AttachHandler(http.MethodGet, BasePathV1, m.SearchGETHandler) - r.AttachHandler(http.MethodGet, BasePathV2, m.SearchGETHandler) - return nil +func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { + attachHandler(http.MethodGet, BasePathV1, m.SearchGETHandler) + attachHandler(http.MethodGet, BasePathV2, m.SearchGETHandler) } diff --git a/internal/api/client/search/search_test.go b/internal/api/client/search/search_test.go index 11b5b80b2..3cb5e8377 100644 --- a/internal/api/client/search/search_test.go +++ b/internal/api/client/search/search_test.go @@ -84,7 +84,7 @@ func (suite *SearchStandardTestSuite) SetupTest() { suite.sentEmails = make(map[string]string) suite.emailSender = testrig.NewEmailSender("../../../../web/template/", suite.sentEmails) suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker) - suite.searchModule = search.New(suite.processor).(*search.Module) + suite.searchModule = search.New(suite.processor) testrig.StandardDBSetup(suite.db, nil) testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") diff --git a/internal/api/client/search/searchget.go b/internal/api/client/search/searchget.go index 7026213ac..15786e6e3 100644 --- a/internal/api/client/search/searchget.go +++ b/internal/api/client/search/searchget.go @@ -25,8 +25,8 @@ import ( "strconv" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + 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" ) @@ -66,12 +66,12 @@ import ( func (m *Module) SearchGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } @@ -82,7 +82,7 @@ func (m *Module) SearchGETHandler(c *gin.Context) { excludeUnreviewed, err = strconv.ParseBool(excludeUnreviewedString) if err != nil { err := fmt.Errorf("error parsing %s: %s", ExcludeUnreviewedKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } } @@ -90,7 +90,7 @@ func (m *Module) SearchGETHandler(c *gin.Context) { query := c.Query(QueryKey) if query == "" { err := errors.New("query parameter q was empty") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } @@ -101,7 +101,7 @@ func (m *Module) SearchGETHandler(c *gin.Context) { resolve, err = strconv.ParseBool(resolveString) if err != nil { err := fmt.Errorf("error parsing %s: %s", ResolveKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } } @@ -112,7 +112,7 @@ func (m *Module) SearchGETHandler(c *gin.Context) { i, err := strconv.ParseInt(limitString, 10, 32) if err != nil { err := fmt.Errorf("error parsing %s: %s", LimitKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } limit = int(i) @@ -130,7 +130,7 @@ func (m *Module) SearchGETHandler(c *gin.Context) { i, err := strconv.ParseInt(offsetString, 10, 32) if err != nil { err := fmt.Errorf("error parsing %s: %s", OffsetKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } offset = int(i) @@ -143,12 +143,12 @@ func (m *Module) SearchGETHandler(c *gin.Context) { following, err = strconv.ParseBool(followingString) if err != nil { err := fmt.Errorf("error parsing %s: %s", FollowingKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } } - searchQuery := &model.SearchQuery{ + searchQuery := &apimodel.SearchQuery{ AccountID: c.Query(AccountIDKey), MaxID: c.Query(MaxIDKey), MinID: c.Query(MinIDKey), @@ -163,7 +163,7 @@ func (m *Module) SearchGETHandler(c *gin.Context) { results, errWithCode := m.processor.SearchGet(c.Request.Context(), authed, searchQuery) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/status/status.go b/internal/api/client/statuses/status.go similarity index 59% rename from internal/api/client/status/status.go rename to internal/api/client/statuses/status.go index dc32ae9b5..7f58e8c9d 100644 --- a/internal/api/client/status/status.go +++ b/internal/api/client/statuses/status.go @@ -16,31 +16,24 @@ along with this program. If not, see . */ -package status +package statuses import ( "net/http" - "strings" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" ) const ( // IDKey is for status UUIDs IDKey = "id" - // BasePath is the base path for serving the status API - BasePath = "/api/v1/statuses" + // BasePath is the base path for serving the statuses API, minus the 'api' prefix + BasePath = "/v1/statuses" // BasePathWithID is just the base path with the ID key in it. // Use this anywhere you need to know the ID of the status being queried. BasePathWithID = BasePath + "/:" + IDKey - // ContextPath is used for fetching context of posts - ContextPath = BasePathWithID + "/context" - // FavouritedPath is for seeing who's faved a given status FavouritedPath = BasePathWithID + "/favourited_by" // FavouritePath is for posting a fave on a status @@ -69,55 +62,39 @@ const ( PinPath = BasePathWithID + "/pin" // UnpinPath is for undoing a pin and returning a status to the ever-swirling drain of time and entropy UnpinPath = BasePathWithID + "/unpin" + + // ContextPath is used for fetching context of posts + ContextPath = BasePathWithID + "/context" ) -// Module implements the ClientAPIModule interface for every related to posting/deleting/interacting with statuses type Module struct { processor processing.Processor } -// New returns a new account module -func New(processor processing.Processor) api.ClientModule { +func New(processor processing.Processor) *Module { return &Module{ processor: processor, } } -// Route attaches all routes from this module to the given router -func (m *Module) Route(r router.Router) error { - r.AttachHandler(http.MethodPost, BasePath, m.StatusCreatePOSTHandler) - r.AttachHandler(http.MethodDelete, BasePathWithID, m.StatusDELETEHandler) +func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { + // create / get / delete status + attachHandler(http.MethodPost, BasePath, m.StatusCreatePOSTHandler) + attachHandler(http.MethodGet, BasePathWithID, m.StatusGETHandler) + attachHandler(http.MethodDelete, BasePathWithID, m.StatusDELETEHandler) - r.AttachHandler(http.MethodPost, FavouritePath, m.StatusFavePOSTHandler) - r.AttachHandler(http.MethodPost, UnfavouritePath, m.StatusUnfavePOSTHandler) - r.AttachHandler(http.MethodGet, FavouritedPath, m.StatusFavedByGETHandler) + // fave stuff + attachHandler(http.MethodPost, FavouritePath, m.StatusFavePOSTHandler) + attachHandler(http.MethodPost, UnfavouritePath, m.StatusUnfavePOSTHandler) + attachHandler(http.MethodGet, FavouritedPath, m.StatusFavedByGETHandler) - r.AttachHandler(http.MethodPost, ReblogPath, m.StatusBoostPOSTHandler) - r.AttachHandler(http.MethodPost, UnreblogPath, m.StatusUnboostPOSTHandler) - r.AttachHandler(http.MethodGet, RebloggedPath, m.StatusBoostedByGETHandler) + // reblog stuff + attachHandler(http.MethodPost, ReblogPath, m.StatusBoostPOSTHandler) + attachHandler(http.MethodPost, UnreblogPath, m.StatusUnboostPOSTHandler) + attachHandler(http.MethodGet, RebloggedPath, m.StatusBoostedByGETHandler) + attachHandler(http.MethodPost, BookmarkPath, m.StatusBookmarkPOSTHandler) + attachHandler(http.MethodPost, UnbookmarkPath, m.StatusUnbookmarkPOSTHandler) - r.AttachHandler(http.MethodPost, BookmarkPath, m.StatusBookmarkPOSTHandler) - r.AttachHandler(http.MethodPost, UnbookmarkPath, m.StatusUnbookmarkPOSTHandler) - - r.AttachHandler(http.MethodGet, ContextPath, m.StatusContextGETHandler) - - r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler) - return nil -} - -// muxHandler is a little workaround to overcome the limitations of Gin -func (m *Module) muxHandler(c *gin.Context) { - log.Debug("entering mux handler") - ru := c.Request.RequestURI - - if c.Request.Method == http.MethodGet { - switch { - case strings.HasPrefix(ru, ContextPath): - // TODO - case strings.HasPrefix(ru, FavouritedPath): - m.StatusFavedByGETHandler(c) - default: - m.StatusGETHandler(c) - } - } + // context / status thread + attachHandler(http.MethodGet, ContextPath, m.StatusContextGETHandler) } diff --git a/internal/api/client/status/status_test.go b/internal/api/client/statuses/status_test.go similarity index 96% rename from internal/api/client/status/status_test.go rename to internal/api/client/statuses/status_test.go index 7c3f094f2..0bf824fdb 100644 --- a/internal/api/client/status/status_test.go +++ b/internal/api/client/statuses/status_test.go @@ -16,11 +16,11 @@ along with this program. If not, see . */ -package status_test +package statuses_test import ( "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/client/status" + "github.com/superseriousbusiness/gotosocial/internal/api/client/statuses" "github.com/superseriousbusiness/gotosocial/internal/concurrency" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/email" @@ -56,7 +56,7 @@ type StatusStandardTestSuite struct { testFollows map[string]*gtsmodel.Follow // module being tested - statusModule *status.Module + statusModule *statuses.Module } func (suite *StatusStandardTestSuite) SetupSuite() { @@ -87,7 +87,7 @@ func (suite *StatusStandardTestSuite) SetupTest() { suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker), suite.storage, suite.mediaManager, fedWorker) suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil) suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker) - suite.statusModule = status.New(suite.processor).(*status.Module) + suite.statusModule = statuses.New(suite.processor) suite.NoError(suite.processor.Start()) } diff --git a/internal/api/client/status/statusbookmark.go b/internal/api/client/statuses/statusbookmark.go similarity index 80% rename from internal/api/client/status/statusbookmark.go rename to internal/api/client/statuses/statusbookmark.go index 983becd72..4efa53528 100644 --- a/internal/api/client/status/statusbookmark.go +++ b/internal/api/client/statuses/statusbookmark.go @@ -16,14 +16,14 @@ along with this program. If not, see . */ -package status +package statuses import ( "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -72,25 +72,25 @@ import ( func (m *Module) StatusBookmarkPOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetStatusID := c.Param(IDKey) if targetStatusID == "" { err := errors.New("no status id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } apiStatus, errWithCode := m.processor.StatusBookmark(c.Request.Context(), authed, targetStatusID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/status/statusbookmark_test.go b/internal/api/client/statuses/statusbookmark_test.go similarity index 93% rename from internal/api/client/status/statusbookmark_test.go rename to internal/api/client/statuses/statusbookmark_test.go index d3da4f297..ba2de78e1 100644 --- a/internal/api/client/status/statusbookmark_test.go +++ b/internal/api/client/statuses/statusbookmark_test.go @@ -13,7 +13,7 @@ along with this program. If not, see . */ -package status_test +package statuses_test import ( "encoding/json" @@ -26,7 +26,7 @@ import ( "github.com/gin-gonic/gin" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/client/status" + "github.com/superseriousbusiness/gotosocial/internal/api/client/statuses" "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/testrig" @@ -49,14 +49,14 @@ func (suite *StatusBookmarkTestSuite) TestPostBookmark() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.BookmarkPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.BookmarkPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting ctx.Request.Header.Set("accept", "application/json") // normally the router would populate these params from the path values, // but because we're calling the function directly, we need to set them manually. ctx.Params = gin.Params{ gin.Param{ - Key: status.IDKey, + Key: statuses.IDKey, Value: targetStatus.ID, }, } diff --git a/internal/api/client/status/statusboost.go b/internal/api/client/statuses/statusboost.go similarity index 81% rename from internal/api/client/status/statusboost.go rename to internal/api/client/statuses/statusboost.go index d43bedd6c..c8921b1b6 100644 --- a/internal/api/client/status/statusboost.go +++ b/internal/api/client/statuses/statusboost.go @@ -16,14 +16,14 @@ along with this program. If not, see . */ -package status +package statuses import ( "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -75,25 +75,25 @@ import ( func (m *Module) StatusBoostPOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetStatusID := c.Param(IDKey) if targetStatusID == "" { err := errors.New("no status id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } apiStatus, errWithCode := m.processor.StatusBoost(c.Request.Context(), authed, targetStatusID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/status/statusboost_test.go b/internal/api/client/statuses/statusboost_test.go similarity index 91% rename from internal/api/client/status/statusboost_test.go rename to internal/api/client/statuses/statusboost_test.go index 5b4b1b3cd..13ca2acf2 100644 --- a/internal/api/client/status/statusboost_test.go +++ b/internal/api/client/statuses/statusboost_test.go @@ -13,7 +13,7 @@ along with this program. If not, see . */ -package status_test +package statuses_test import ( "context" @@ -27,8 +27,8 @@ import ( "github.com/gin-gonic/gin" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/client/status" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/api/client/statuses" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/testrig" @@ -51,14 +51,14 @@ func (suite *StatusBoostTestSuite) TestPostBoost() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting ctx.Request.Header.Set("accept", "application/json") // normally the router would populate these params from the path values, // but because we're calling the function directly, we need to set them manually. ctx.Params = gin.Params{ gin.Param{ - Key: status.IDKey, + Key: statuses.IDKey, Value: targetStatus.ID, }, } @@ -73,12 +73,12 @@ func (suite *StatusBoostTestSuite) TestPostBoost() { b, err := ioutil.ReadAll(result.Body) suite.NoError(err) - statusReply := &model.Status{} + statusReply := &apimodel.Status{} err = json.Unmarshal(b, statusReply) suite.NoError(err) suite.False(statusReply.Sensitive) - suite.Equal(model.VisibilityPublic, statusReply.Visibility) + suite.Equal(apimodel.VisibilityPublic, statusReply.Visibility) suite.Equal(targetStatus.ContentWarning, statusReply.SpoilerText) suite.Equal(targetStatus.Content, statusReply.Content) @@ -117,12 +117,12 @@ func (suite *StatusBoostTestSuite) TestPostBoostOwnFollowersOnly() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, testUser) ctx.Set(oauth.SessionAuthorizedAccount, testAccount) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.ReblogPath, ":id", testStatus.ID, 1)), nil) + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.ReblogPath, ":id", testStatus.ID, 1)), nil) ctx.Request.Header.Set("accept", "application/json") ctx.Params = gin.Params{ gin.Param{ - Key: status.IDKey, + Key: statuses.IDKey, Value: testStatus.ID, }, } @@ -137,7 +137,7 @@ func (suite *StatusBoostTestSuite) TestPostBoostOwnFollowersOnly() { b, err := ioutil.ReadAll(result.Body) suite.NoError(err) - responseStatus := &model.Status{} + responseStatus := &apimodel.Status{} err = json.Unmarshal(b, responseStatus) suite.NoError(err) @@ -182,14 +182,14 @@ func (suite *StatusBoostTestSuite) TestPostUnboostable() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting ctx.Request.Header.Set("accept", "application/json") // normally the router would populate these params from the path values, // but because we're calling the function directly, we need to set them manually. ctx.Params = gin.Params{ gin.Param{ - Key: status.IDKey, + Key: statuses.IDKey, Value: targetStatus.ID, }, } @@ -224,14 +224,14 @@ func (suite *StatusBoostTestSuite) TestPostNotVisible() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_2"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_2"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting ctx.Request.Header.Set("accept", "application/json") // normally the router would populate these params from the path values, // but because we're calling the function directly, we need to set them manually. ctx.Params = gin.Params{ gin.Param{ - Key: status.IDKey, + Key: statuses.IDKey, Value: targetStatus.ID, }, } diff --git a/internal/api/client/status/statusboostedby.go b/internal/api/client/statuses/statusboostedby.go similarity index 85% rename from internal/api/client/status/statusboostedby.go rename to internal/api/client/statuses/statusboostedby.go index 4a175f6e9..dc1567dba 100644 --- a/internal/api/client/status/statusboostedby.go +++ b/internal/api/client/statuses/statusboostedby.go @@ -16,14 +16,14 @@ along with this program. If not, see . */ -package status +package statuses import ( "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -68,20 +68,20 @@ import ( func (m *Module) StatusBoostedByGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } targetStatusID := c.Param(IDKey) if targetStatusID == "" { err := errors.New("no status id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } apiAccounts, errWithCode := m.processor.StatusBoostedBy(c.Request.Context(), authed, targetStatusID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/status/statusboostedby_test.go b/internal/api/client/statuses/statusboostedby_test.go similarity index 92% rename from internal/api/client/status/statusboostedby_test.go rename to internal/api/client/statuses/statusboostedby_test.go index 0d7c9f7ab..576dee369 100644 --- a/internal/api/client/status/statusboostedby_test.go +++ b/internal/api/client/statuses/statusboostedby_test.go @@ -13,7 +13,7 @@ along with this program. If not, see . */ -package status_test +package statuses_test import ( "encoding/json" @@ -25,7 +25,7 @@ import ( "testing" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/client/status" + "github.com/superseriousbusiness/gotosocial/internal/api/client/statuses" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/testrig" @@ -46,7 +46,7 @@ func (suite *StatusBoostedByTestSuite) TestRebloggedByOK() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) - ctx.Request = httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.RebloggedPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.RebloggedPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting ctx.Request.Header.Set("accept", "application/json") ctx.AddParam("id", targetStatus.ID) @@ -82,7 +82,7 @@ func (suite *StatusBoostedByTestSuite) TestRebloggedByUseBoostWrapperID() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) - ctx.Request = httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.RebloggedPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.RebloggedPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting ctx.Request.Header.Set("accept", "application/json") ctx.AddParam("id", targetStatus.ID) diff --git a/internal/api/client/status/statuscontext.go b/internal/api/client/statuses/statuscontext.go similarity index 81% rename from internal/api/client/status/statuscontext.go rename to internal/api/client/statuses/statuscontext.go index 632a151d5..9a6ac9f7f 100644 --- a/internal/api/client/status/statuscontext.go +++ b/internal/api/client/statuses/statuscontext.go @@ -16,14 +16,14 @@ along with this program. If not, see . */ -package status +package statuses import ( "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -74,25 +74,25 @@ import ( func (m *Module) StatusContextGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetStatusID := c.Param(IDKey) if targetStatusID == "" { err := errors.New("no status id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } statusContext, errWithCode := m.processor.StatusGetContext(c.Request.Context(), authed, targetStatusID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/status/statuscreate.go b/internal/api/client/statuses/statuscreate.go similarity index 84% rename from internal/api/client/status/statuscreate.go rename to internal/api/client/statuses/statuscreate.go index c1427411d..d36c93e77 100644 --- a/internal/api/client/status/statuscreate.go +++ b/internal/api/client/statuses/statuscreate.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package status +package statuses import ( "errors" @@ -24,8 +24,8 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -75,18 +75,18 @@ import ( func (m *Module) StatusCreatePOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - form := &model.AdvancedStatusCreateForm{} + form := &apimodel.AdvancedStatusCreateForm{} if err := c.ShouldBind(form); err != nil { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } @@ -100,20 +100,20 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) { // form.Status += "\n\nsent from " + user + "'s iphone\n" if err := validateCreateStatus(form); err != nil { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } apiStatus, errWithCode := m.processor.StatusCreate(c.Request.Context(), authed, form) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } c.JSON(http.StatusOK, apiStatus) } -func validateCreateStatus(form *model.AdvancedStatusCreateForm) error { +func validateCreateStatus(form *apimodel.AdvancedStatusCreateForm) error { hasStatus := form.Status != "" hasMedia := len(form.MediaIDs) != 0 hasPoll := form.Poll != nil diff --git a/internal/api/client/status/statuscreate_test.go b/internal/api/client/statuses/statuscreate_test.go similarity index 91% rename from internal/api/client/status/statuscreate_test.go rename to internal/api/client/statuses/statuscreate_test.go index c143489f3..3648d7520 100644 --- a/internal/api/client/status/statuscreate_test.go +++ b/internal/api/client/statuses/statuscreate_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package status_test +package statuses_test import ( "context" @@ -29,8 +29,8 @@ import ( "testing" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/client/status" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/api/client/statuses" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -59,13 +59,13 @@ func (suite *StatusCreateTestSuite) TestPostNewStatus() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", status.BasePath), nil) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", statuses.BasePath), nil) // the endpoint we're hitting ctx.Request.Header.Set("accept", "application/json") ctx.Request.Form = url.Values{ "status": {"this is a brand new status! #helloworld"}, "spoiler_text": {"hello hello"}, "sensitive": {"true"}, - "visibility": {string(model.VisibilityMutualsOnly)}, + "visibility": {string(apimodel.VisibilityMutualsOnly)}, "likeable": {"false"}, "replyable": {"false"}, "federated": {"false"}, @@ -82,16 +82,16 @@ func (suite *StatusCreateTestSuite) TestPostNewStatus() { b, err := ioutil.ReadAll(result.Body) suite.NoError(err) - statusReply := &model.Status{} + statusReply := &apimodel.Status{} err = json.Unmarshal(b, statusReply) suite.NoError(err) suite.Equal("hello hello", statusReply.SpoilerText) suite.Equal("

this is a brand new status! #helloworld

", statusReply.Content) suite.True(statusReply.Sensitive) - suite.Equal(model.VisibilityPrivate, statusReply.Visibility) // even though we set this status to mutuals only, it should serialize to private, because the mastodon api has no idea about mutuals_only + suite.Equal(apimodel.VisibilityPrivate, statusReply.Visibility) // even though we set this status to mutuals only, it should serialize to private, because the mastodon api has no idea about mutuals_only suite.Len(statusReply.Tags, 1) - suite.Equal(model.Tag{ + suite.Equal(apimodel.Tag{ Name: "helloworld", URL: "http://localhost:8080/tags/helloworld", }, statusReply.Tags[0]) @@ -124,11 +124,11 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusMarkdown() { ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, a) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", status.BasePath), nil) + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", statuses.BasePath), nil) ctx.Request.Header.Set("accept", "application/json") ctx.Request.Form = url.Values{ "status": {statusMarkdown}, - "visibility": {string(model.VisibilityPublic)}, + "visibility": {string(apimodel.VisibilityPublic)}, } suite.statusModule.StatusCreatePOSTHandler(ctx) @@ -139,7 +139,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusMarkdown() { b, err := ioutil.ReadAll(result.Body) suite.NoError(err) - statusReply := &model.Status{} + statusReply := &apimodel.Status{} err = json.Unmarshal(b, statusReply) suite.NoError(err) @@ -163,11 +163,11 @@ func (suite *StatusCreateTestSuite) TestMentionUnknownAccount() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", status.BasePath), nil) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", statuses.BasePath), nil) // the endpoint we're hitting ctx.Request.Header.Set("accept", "application/json") ctx.Request.Form = url.Values{ "status": {"hello @brand_new_person@unknown-instance.com"}, - "visibility": {string(model.VisibilityPublic)}, + "visibility": {string(apimodel.VisibilityPublic)}, } suite.statusModule.StatusCreatePOSTHandler(ctx) @@ -178,13 +178,13 @@ func (suite *StatusCreateTestSuite) TestMentionUnknownAccount() { b, err := ioutil.ReadAll(result.Body) suite.NoError(err) - statusReply := &model.Status{} + statusReply := &apimodel.Status{} err = json.Unmarshal(b, statusReply) suite.NoError(err) // if the status is properly formatted, that means the account has been put in the db suite.Equal(`

hello @brand_new_person

`, statusReply.Content) - suite.Equal(model.VisibilityPublic, statusReply.Visibility) + suite.Equal(apimodel.VisibilityPublic, statusReply.Visibility) } func (suite *StatusCreateTestSuite) TestPostAnotherNewStatus() { @@ -198,7 +198,7 @@ func (suite *StatusCreateTestSuite) TestPostAnotherNewStatus() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", status.BasePath), nil) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", statuses.BasePath), nil) // the endpoint we're hitting ctx.Request.Header.Set("accept", "application/json") ctx.Request.Form = url.Values{ "status": {statusWithLinksAndTags}, @@ -215,7 +215,7 @@ func (suite *StatusCreateTestSuite) TestPostAnotherNewStatus() { b, err := ioutil.ReadAll(result.Body) suite.NoError(err) - statusReply := &model.Status{} + statusReply := &apimodel.Status{} err = json.Unmarshal(b, statusReply) suite.NoError(err) @@ -233,7 +233,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusWithEmoji() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", status.BasePath), nil) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", statuses.BasePath), nil) // the endpoint we're hitting ctx.Request.Header.Set("accept", "application/json") ctx.Request.Form = url.Values{ "status": {"here is a rainbow emoji a few times! :rainbow: :rainbow: :rainbow: \n here's an emoji that isn't in the db: :test_emoji: "}, @@ -247,7 +247,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusWithEmoji() { b, err := ioutil.ReadAll(result.Body) suite.NoError(err) - statusReply := &model.Status{} + statusReply := &apimodel.Status{} err = json.Unmarshal(b, statusReply) suite.NoError(err) @@ -275,7 +275,7 @@ func (suite *StatusCreateTestSuite) TestReplyToNonexistentStatus() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", status.BasePath), nil) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", statuses.BasePath), nil) // the endpoint we're hitting ctx.Request.Header.Set("accept", "application/json") ctx.Request.Form = url.Values{ "status": {"this is a reply to a status that doesn't exist"}, @@ -307,7 +307,7 @@ func (suite *StatusCreateTestSuite) TestReplyToLocalStatus() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", status.BasePath), nil) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", statuses.BasePath), nil) // the endpoint we're hitting ctx.Request.Header.Set("accept", "application/json") ctx.Request.Form = url.Values{ "status": {fmt.Sprintf("hello @%s this reply should work!", testrig.NewTestAccounts()["local_account_2"].Username)}, @@ -323,14 +323,14 @@ func (suite *StatusCreateTestSuite) TestReplyToLocalStatus() { b, err := ioutil.ReadAll(result.Body) suite.NoError(err) - statusReply := &model.Status{} + statusReply := &apimodel.Status{} err = json.Unmarshal(b, statusReply) suite.NoError(err) suite.Equal("", statusReply.SpoilerText) suite.Equal(fmt.Sprintf("

hello @%s this reply should work!

", testrig.NewTestAccounts()["local_account_2"].Username, testrig.NewTestAccounts()["local_account_2"].Username), statusReply.Content) suite.False(statusReply.Sensitive) - suite.Equal(model.VisibilityPublic, statusReply.Visibility) + suite.Equal(apimodel.VisibilityPublic, statusReply.Visibility) suite.Equal(testrig.NewTestStatuses()["local_account_2_status_1"].ID, *statusReply.InReplyToID) suite.Equal(testrig.NewTestAccounts()["local_account_2"].ID, *statusReply.InReplyToAccountID) suite.Len(statusReply.Mentions, 1) @@ -350,7 +350,7 @@ func (suite *StatusCreateTestSuite) TestAttachNewMediaSuccess() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", status.BasePath), nil) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", statuses.BasePath), nil) // the endpoint we're hitting ctx.Request.Header.Set("accept", "application/json") ctx.Request.Form = url.Values{ "status": {"here's an image attachment"}, @@ -366,14 +366,14 @@ func (suite *StatusCreateTestSuite) TestAttachNewMediaSuccess() { b, err := ioutil.ReadAll(result.Body) suite.NoError(err) - statusResponse := &model.Status{} + statusResponse := &apimodel.Status{} err = json.Unmarshal(b, statusResponse) suite.NoError(err) suite.Equal("", statusResponse.SpoilerText) suite.Equal("

here's an image attachment

", statusResponse.Content) suite.False(statusResponse.Sensitive) - suite.Equal(model.VisibilityPublic, statusResponse.Visibility) + suite.Equal(apimodel.VisibilityPublic, statusResponse.Visibility) // there should be one media attachment suite.Len(statusResponse.MediaAttachments, 1) diff --git a/internal/api/client/status/statusdelete.go b/internal/api/client/statuses/statusdelete.go similarity index 81% rename from internal/api/client/status/statusdelete.go rename to internal/api/client/statuses/statusdelete.go index b37dd5f14..3db7397db 100644 --- a/internal/api/client/status/statusdelete.go +++ b/internal/api/client/statuses/statusdelete.go @@ -16,14 +16,14 @@ along with this program. If not, see . */ -package status +package statuses import ( "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -74,25 +74,25 @@ import ( func (m *Module) StatusDELETEHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetStatusID := c.Param(IDKey) if targetStatusID == "" { err := errors.New("no status id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } apiStatus, errWithCode := m.processor.StatusDelete(c.Request.Context(), authed, targetStatusID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/status/statusdelete_test.go b/internal/api/client/statuses/statusdelete_test.go similarity index 90% rename from internal/api/client/status/statusdelete_test.go rename to internal/api/client/statuses/statusdelete_test.go index f97a13eec..9a9ceef8f 100644 --- a/internal/api/client/status/statusdelete_test.go +++ b/internal/api/client/statuses/statusdelete_test.go @@ -13,7 +13,7 @@ along with this program. If not, see . */ -package status_test +package statuses_test import ( "encoding/json" @@ -27,8 +27,8 @@ import ( "github.com/gin-gonic/gin" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/client/status" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/api/client/statuses" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/testrig" @@ -50,14 +50,14 @@ func (suite *StatusDeleteTestSuite) TestPostDelete() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) - ctx.Request = httptest.NewRequest(http.MethodDelete, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.BasePathWithID, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodDelete, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.BasePathWithID, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting ctx.Request.Header.Set("accept", "application/json") // normally the router would populate these params from the path values, // but because we're calling the function directly, we need to set them manually. ctx.Params = gin.Params{ gin.Param{ - Key: status.IDKey, + Key: statuses.IDKey, Value: targetStatus.ID, }, } @@ -72,7 +72,7 @@ func (suite *StatusDeleteTestSuite) TestPostDelete() { b, err := ioutil.ReadAll(result.Body) suite.NoError(err) - statusReply := &model.Status{} + statusReply := &apimodel.Status{} err = json.Unmarshal(b, statusReply) suite.NoError(err) suite.NotNil(statusReply) diff --git a/internal/api/client/status/statusfave.go b/internal/api/client/statuses/statusfave.go similarity index 80% rename from internal/api/client/status/statusfave.go rename to internal/api/client/statuses/statusfave.go index 3117e7ef2..bd9ded147 100644 --- a/internal/api/client/status/statusfave.go +++ b/internal/api/client/statuses/statusfave.go @@ -16,14 +16,14 @@ along with this program. If not, see . */ -package status +package statuses import ( "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -71,25 +71,25 @@ import ( func (m *Module) StatusFavePOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetStatusID := c.Param(IDKey) if targetStatusID == "" { err := errors.New("no status id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } apiStatus, errWithCode := m.processor.StatusFave(c.Request.Context(), authed, targetStatusID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/status/statusfave_test.go b/internal/api/client/statuses/statusfave_test.go similarity index 88% rename from internal/api/client/status/statusfave_test.go rename to internal/api/client/statuses/statusfave_test.go index da5d2a48a..20805d87c 100644 --- a/internal/api/client/status/statusfave_test.go +++ b/internal/api/client/statuses/statusfave_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package status_test +package statuses_test import ( "encoding/json" @@ -30,8 +30,9 @@ import ( "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/client/status" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/api/client/statuses" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/testrig" ) @@ -54,14 +55,14 @@ func (suite *StatusFaveTestSuite) TestPostFave() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.FavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.FavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting ctx.Request.Header.Set("accept", "application/json") // normally the router would populate these params from the path values, // but because we're calling the function directly, we need to set them manually. ctx.Params = gin.Params{ gin.Param{ - Key: status.IDKey, + Key: statuses.IDKey, Value: targetStatus.ID, }, } @@ -76,14 +77,14 @@ func (suite *StatusFaveTestSuite) TestPostFave() { b, err := ioutil.ReadAll(result.Body) assert.NoError(suite.T(), err) - statusReply := &model.Status{} + statusReply := &apimodel.Status{} err = json.Unmarshal(b, statusReply) assert.NoError(suite.T(), err) assert.Equal(suite.T(), targetStatus.ContentWarning, statusReply.SpoilerText) assert.Equal(suite.T(), targetStatus.Content, statusReply.Content) assert.True(suite.T(), statusReply.Sensitive) - assert.Equal(suite.T(), model.VisibilityPublic, statusReply.Visibility) + assert.Equal(suite.T(), apimodel.VisibilityPublic, statusReply.Visibility) assert.True(suite.T(), statusReply.Favourited) assert.Equal(suite.T(), 1, statusReply.FavouritesCount) } @@ -102,14 +103,14 @@ func (suite *StatusFaveTestSuite) TestPostUnfaveable() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.FavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.FavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting ctx.Request.Header.Set("accept", "application/json") // normally the router would populate these params from the path values, // but because we're calling the function directly, we need to set them manually. ctx.Params = gin.Params{ gin.Param{ - Key: status.IDKey, + Key: statuses.IDKey, Value: targetStatus.ID, }, } diff --git a/internal/api/client/status/statusfavedby.go b/internal/api/client/statuses/statusfavedby.go similarity index 80% rename from internal/api/client/status/statusfavedby.go rename to internal/api/client/statuses/statusfavedby.go index 20ef86ded..aa0f1f8d6 100644 --- a/internal/api/client/status/statusfavedby.go +++ b/internal/api/client/statuses/statusfavedby.go @@ -16,14 +16,14 @@ along with this program. If not, see . */ -package status +package statuses import ( "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -72,25 +72,25 @@ import ( func (m *Module) StatusFavedByGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetStatusID := c.Param(IDKey) if targetStatusID == "" { err := errors.New("no status id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } apiAccounts, errWithCode := m.processor.StatusFavedBy(c.Request.Context(), authed, targetStatusID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/status/statusfavedby_test.go b/internal/api/client/statuses/statusfavedby_test.go similarity index 90% rename from internal/api/client/status/statusfavedby_test.go rename to internal/api/client/statuses/statusfavedby_test.go index e704fa724..fc04c490e 100644 --- a/internal/api/client/status/statusfavedby_test.go +++ b/internal/api/client/statuses/statusfavedby_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package status_test +package statuses_test import ( "encoding/json" @@ -30,8 +30,8 @@ import ( "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/client/status" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/api/client/statuses" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/testrig" ) @@ -53,14 +53,14 @@ func (suite *StatusFavedByTestSuite) TestGetFavedBy() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_2"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_2"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.FavouritedPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.FavouritedPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting ctx.Request.Header.Set("accept", "application/json") // normally the router would populate these params from the path values, // but because we're calling the function directly, we need to set them manually. ctx.Params = gin.Params{ gin.Param{ - Key: status.IDKey, + Key: statuses.IDKey, Value: targetStatus.ID, }, } @@ -75,7 +75,7 @@ func (suite *StatusFavedByTestSuite) TestGetFavedBy() { b, err := ioutil.ReadAll(result.Body) assert.NoError(suite.T(), err) - accts := []model.Account{} + accts := []apimodel.Account{} err = json.Unmarshal(b, &accts) assert.NoError(suite.T(), err) diff --git a/internal/api/client/status/statusget.go b/internal/api/client/statuses/statusget.go similarity index 79% rename from internal/api/client/status/statusget.go rename to internal/api/client/statuses/statusget.go index a0d0e913c..5e7a59027 100644 --- a/internal/api/client/status/statusget.go +++ b/internal/api/client/statuses/statusget.go @@ -16,14 +16,14 @@ along with this program. If not, see . */ -package status +package statuses import ( "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -71,25 +71,25 @@ import ( func (m *Module) StatusGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetStatusID := c.Param(IDKey) if targetStatusID == "" { err := errors.New("no status id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } apiStatus, errWithCode := m.processor.StatusGet(c.Request.Context(), authed, targetStatusID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/status/statusget_test.go b/internal/api/client/statuses/statusget_test.go similarity index 97% rename from internal/api/client/status/statusget_test.go rename to internal/api/client/statuses/statusget_test.go index d11c9b587..e8e1fd8f4 100644 --- a/internal/api/client/status/statusget_test.go +++ b/internal/api/client/statuses/statusget_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package status_test +package statuses_test import ( "testing" diff --git a/internal/api/client/status/statusunbookmark.go b/internal/api/client/statuses/statusunbookmark.go similarity index 80% rename from internal/api/client/status/statusunbookmark.go rename to internal/api/client/statuses/statusunbookmark.go index aa090f8c9..117ef833b 100644 --- a/internal/api/client/status/statusunbookmark.go +++ b/internal/api/client/statuses/statusunbookmark.go @@ -16,14 +16,14 @@ along with this program. If not, see . */ -package status +package statuses import ( "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -72,25 +72,25 @@ import ( func (m *Module) StatusUnbookmarkPOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetStatusID := c.Param(IDKey) if targetStatusID == "" { err := errors.New("no status id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } apiStatus, errWithCode := m.processor.StatusUnbookmark(c.Request.Context(), authed, targetStatusID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/status/statusunbookmark_test.go b/internal/api/client/statuses/statusunbookmark_test.go similarity index 92% rename from internal/api/client/status/statusunbookmark_test.go rename to internal/api/client/statuses/statusunbookmark_test.go index 09a18ab9b..9c4667ad8 100644 --- a/internal/api/client/status/statusunbookmark_test.go +++ b/internal/api/client/statuses/statusunbookmark_test.go @@ -13,7 +13,7 @@ along with this program. If not, see . */ -package status_test +package statuses_test import ( "encoding/json" @@ -26,7 +26,7 @@ import ( "github.com/gin-gonic/gin" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/client/status" + "github.com/superseriousbusiness/gotosocial/internal/api/client/statuses" "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/testrig" @@ -49,12 +49,12 @@ func (suite *StatusUnbookmarkTestSuite) TestPostUnbookmark() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.UnbookmarkPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.UnbookmarkPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting ctx.Request.Header.Set("accept", "application/json") ctx.Params = gin.Params{ gin.Param{ - Key: status.IDKey, + Key: statuses.IDKey, Value: targetStatus.ID, }, } diff --git a/internal/api/client/status/statusunboost.go b/internal/api/client/statuses/statusunboost.go similarity index 80% rename from internal/api/client/status/statusunboost.go rename to internal/api/client/statuses/statusunboost.go index 45a8e0ece..e91081195 100644 --- a/internal/api/client/status/statusunboost.go +++ b/internal/api/client/statuses/statusunboost.go @@ -16,14 +16,14 @@ along with this program. If not, see . */ -package status +package statuses import ( "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -72,25 +72,25 @@ import ( func (m *Module) StatusUnboostPOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetStatusID := c.Param(IDKey) if targetStatusID == "" { err := errors.New("no status id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } apiStatus, errWithCode := m.processor.StatusUnboost(c.Request.Context(), authed, targetStatusID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/status/statusunfave.go b/internal/api/client/statuses/statusunfave.go similarity index 80% rename from internal/api/client/status/statusunfave.go rename to internal/api/client/statuses/statusunfave.go index 19d3da3bd..57ae88e1e 100644 --- a/internal/api/client/status/statusunfave.go +++ b/internal/api/client/statuses/statusunfave.go @@ -16,14 +16,14 @@ along with this program. If not, see . */ -package status +package statuses import ( "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -71,25 +71,25 @@ import ( func (m *Module) StatusUnfavePOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetStatusID := c.Param(IDKey) if targetStatusID == "" { err := errors.New("no status id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } apiStatus, errWithCode := m.processor.StatusUnfave(c.Request.Context(), authed, targetStatusID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/status/statusunfave_test.go b/internal/api/client/statuses/statusunfave_test.go similarity index 87% rename from internal/api/client/status/statusunfave_test.go rename to internal/api/client/statuses/statusunfave_test.go index b8448d657..2ca3450a4 100644 --- a/internal/api/client/status/statusunfave_test.go +++ b/internal/api/client/statuses/statusunfave_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package status_test +package statuses_test import ( "encoding/json" @@ -30,8 +30,8 @@ import ( "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/client/status" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/api/client/statuses" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/testrig" ) @@ -55,14 +55,14 @@ func (suite *StatusUnfaveTestSuite) TestPostUnfave() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.UnfavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.UnfavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting ctx.Request.Header.Set("accept", "application/json") // normally the router would populate these params from the path values, // but because we're calling the function directly, we need to set them manually. ctx.Params = gin.Params{ gin.Param{ - Key: status.IDKey, + Key: statuses.IDKey, Value: targetStatus.ID, }, } @@ -77,14 +77,14 @@ func (suite *StatusUnfaveTestSuite) TestPostUnfave() { b, err := ioutil.ReadAll(result.Body) assert.NoError(suite.T(), err) - statusReply := &model.Status{} + statusReply := &apimodel.Status{} err = json.Unmarshal(b, statusReply) assert.NoError(suite.T(), err) assert.Equal(suite.T(), targetStatus.ContentWarning, statusReply.SpoilerText) assert.Equal(suite.T(), targetStatus.Content, statusReply.Content) assert.False(suite.T(), statusReply.Sensitive) - assert.Equal(suite.T(), model.VisibilityPublic, statusReply.Visibility) + assert.Equal(suite.T(), apimodel.VisibilityPublic, statusReply.Visibility) assert.False(suite.T(), statusReply.Favourited) assert.Equal(suite.T(), 0, statusReply.FavouritesCount) } @@ -104,14 +104,14 @@ func (suite *StatusUnfaveTestSuite) TestPostAlreadyNotFaved() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.UnfavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.UnfavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting ctx.Request.Header.Set("accept", "application/json") // normally the router would populate these params from the path values, // but because we're calling the function directly, we need to set them manually. ctx.Params = gin.Params{ gin.Param{ - Key: status.IDKey, + Key: statuses.IDKey, Value: targetStatus.ID, }, } @@ -126,14 +126,14 @@ func (suite *StatusUnfaveTestSuite) TestPostAlreadyNotFaved() { b, err := ioutil.ReadAll(result.Body) assert.NoError(suite.T(), err) - statusReply := &model.Status{} + statusReply := &apimodel.Status{} err = json.Unmarshal(b, statusReply) assert.NoError(suite.T(), err) assert.Equal(suite.T(), targetStatus.ContentWarning, statusReply.SpoilerText) assert.Equal(suite.T(), targetStatus.Content, statusReply.Content) assert.True(suite.T(), statusReply.Sensitive) - assert.Equal(suite.T(), model.VisibilityPublic, statusReply.Visibility) + assert.Equal(suite.T(), apimodel.VisibilityPublic, statusReply.Visibility) assert.False(suite.T(), statusReply.Favourited) assert.Equal(suite.T(), 0, statusReply.FavouritesCount) } diff --git a/internal/api/client/streaming/stream.go b/internal/api/client/streaming/stream.go index a9cb62732..de98719c2 100644 --- a/internal/api/client/streaming/stream.go +++ b/internal/api/client/streaming/stream.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + 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 streaming import ( @@ -6,7 +24,7 @@ import ( "time" "codeberg.org/gruf/go-kv" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/log" @@ -14,12 +32,15 @@ import ( "github.com/gorilla/websocket" ) -var wsUpgrader = websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, - // we expect cors requests (via eg., pinafore.social) so be lenient - CheckOrigin: func(r *http.Request) bool { return true }, -} +var ( + wsUpgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + // we expect cors requests (via eg., pinafore.social) so be lenient + CheckOrigin: func(r *http.Request) bool { return true }, + } + errNoToken = fmt.Errorf("no access token provided under query key %s or under header %s", AccessTokenQueryKey, AccessTokenHeader) +) // StreamGETHandler swagger:operation GET /api/v1/streaming streamGet // @@ -125,29 +146,33 @@ func (m *Module) StreamGETHandler(c *gin.Context) { streamType := c.Query(StreamQueryKey) if streamType == "" { err := fmt.Errorf("no stream type provided under query key %s", StreamQueryKey) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - accessToken := c.Query(AccessTokenQueryKey) - if accessToken == "" { - accessToken = c.GetHeader(AccessTokenHeader) - } - if accessToken == "" { - err := fmt.Errorf("no access token provided under query key %s or under header %s", AccessTokenQueryKey, AccessTokenHeader) - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + var accessToken string + if t := c.Query(AccessTokenQueryKey); t != "" { + // try query param first + accessToken = t + } else if t := c.GetHeader(AccessTokenHeader); t != "" { + // fall back to Sec-Websocket-Protocol + accessToken = t + } else { + // no token + err := errNoToken + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } account, errWithCode := m.processor.AuthorizeStreamingRequest(c.Request.Context(), accessToken) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } stream, errWithCode := m.processor.OpenStreamForAccount(c.Request.Context(), account, streamType) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } @@ -175,6 +200,7 @@ func (m *Module) StreamGETHandler(c *gin.Context) { }() streamTicker := time.NewTicker(m.tickDuration) + defer streamTicker.Stop() // We want to stay in the loop as long as possible while the client is connected. // The only thing that should break the loop is if the client leaves or the connection becomes unhealthy. diff --git a/internal/api/client/streaming/streaming.go b/internal/api/client/streaming/streaming.go index b15dfbdbd..f9d9fdf36 100644 --- a/internal/api/client/streaming/streaming.go +++ b/internal/api/client/streaming/streaming.go @@ -22,14 +22,13 @@ import ( "net/http" "time" - "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" ) const ( - // BasePath is the path for the streaming api - BasePath = "/api/v1/streaming" + // BasePath is the path for the streaming api, minus the 'api' prefix + BasePath = "/v1/streaming" // StreamQueryKey is the query key for the type of stream being requested StreamQueryKey = "stream" @@ -41,29 +40,25 @@ const ( AccessTokenHeader = "Sec-Websocket-Protocol" ) -// Module implements the api.ClientModule interface for everything related to streaming type Module struct { processor processing.Processor tickDuration time.Duration } -// New returns a new streaming module -func New(processor processing.Processor) api.ClientModule { +func New(processor processing.Processor) *Module { return &Module{ processor: processor, tickDuration: 30 * time.Second, } } -func NewWithTickDuration(processor processing.Processor, tickDuration time.Duration) api.ClientModule { +func NewWithTickDuration(processor processing.Processor, tickDuration time.Duration) *Module { return &Module{ processor: processor, tickDuration: tickDuration, } } -// Route attaches all routes from this module to the given router -func (m *Module) Route(r router.Router) error { - r.AttachHandler(http.MethodGet, BasePath, m.StreamGETHandler) - return nil +func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { + attachHandler(http.MethodGet, BasePath, m.StreamGETHandler) } diff --git a/internal/api/client/streaming/streaming_test.go b/internal/api/client/streaming/streaming_test.go index 49c983fff..2f2d850c1 100644 --- a/internal/api/client/streaming/streaming_test.go +++ b/internal/api/client/streaming/streaming_test.go @@ -99,7 +99,7 @@ func (suite *StreamingTestSuite) SetupTest() { suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker), suite.storage, suite.mediaManager, fedWorker) suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil) suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker) - suite.streamingModule = streaming.NewWithTickDuration(suite.processor, 1).(*streaming.Module) + suite.streamingModule = streaming.NewWithTickDuration(suite.processor, 1) suite.NoError(suite.processor.Start()) } diff --git a/internal/api/client/timeline/home.go b/internal/api/client/timelines/home.go similarity index 86% rename from internal/api/client/timeline/home.go rename to internal/api/client/timelines/home.go index e6135dd63..33af8fe5e 100644 --- a/internal/api/client/timeline/home.go +++ b/internal/api/client/timelines/home.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package timeline +package timelines import ( "fmt" @@ -24,7 +24,7 @@ import ( "strconv" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -112,12 +112,12 @@ import ( func (m *Module) HomeTimelineGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } @@ -145,7 +145,7 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) { i, err := strconv.ParseInt(limitString, 10, 32) if err != nil { err := fmt.Errorf("error parsing %s: %s", LimitKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } limit = int(i) @@ -157,7 +157,7 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) { i, err := strconv.ParseBool(localString) if err != nil { err := fmt.Errorf("error parsing %s: %s", LocalKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } local = i @@ -165,7 +165,7 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) { resp, errWithCode := m.processor.HomeTimelineGet(c.Request.Context(), authed, maxID, sinceID, minID, limit, local) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/timeline/public.go b/internal/api/client/timelines/public.go similarity index 87% rename from internal/api/client/timeline/public.go rename to internal/api/client/timelines/public.go index fda23438b..efe351a37 100644 --- a/internal/api/client/timeline/public.go +++ b/internal/api/client/timelines/public.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package timeline +package timelines import ( "fmt" @@ -24,7 +24,7 @@ import ( "strconv" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -123,12 +123,12 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) { } if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } @@ -156,7 +156,7 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) { i, err := strconv.ParseInt(limitString, 10, 32) if err != nil { err := fmt.Errorf("error parsing %s: %s", LimitKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } limit = int(i) @@ -168,7 +168,7 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) { i, err := strconv.ParseBool(localString) if err != nil { err := fmt.Errorf("error parsing %s: %s", LocalKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } local = i @@ -176,7 +176,7 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) { resp, errWithCode := m.processor.PublicTimelineGet(c.Request.Context(), authed, maxID, sinceID, minID, limit, local) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/timeline/timeline.go b/internal/api/client/timelines/timeline.go similarity index 70% rename from internal/api/client/timeline/timeline.go rename to internal/api/client/timelines/timeline.go index 3604a1fc2..609e1855e 100644 --- a/internal/api/client/timeline/timeline.go +++ b/internal/api/client/timelines/timeline.go @@ -16,19 +16,18 @@ along with this program. If not, see . */ -package timeline +package timelines import ( "net/http" - "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" ) const ( - // BasePath is the base URI path for serving timelines - BasePath = "/api/v1/timelines" + // BasePath is the base URI path for serving timelines, minus the 'api' prefix. + BasePath = "/v1/timelines" // HomeTimeline is the path for the home timeline HomeTimeline = BasePath + "/home" // PublicTimeline is the path for the public (and public local) timeline @@ -45,21 +44,17 @@ const ( LocalKey = "local" ) -// Module implements the ClientAPIModule interface for everything relating to viewing timelines type Module struct { processor processing.Processor } -// New returns a new timeline module -func New(processor processing.Processor) api.ClientModule { +func New(processor processing.Processor) *Module { return &Module{ processor: processor, } } -// Route attaches all routes from this module to the given router -func (m *Module) Route(r router.Router) error { - r.AttachHandler(http.MethodGet, HomeTimeline, m.HomeTimelineGETHandler) - r.AttachHandler(http.MethodGet, PublicTimeline, m.PublicTimelineGETHandler) - return nil +func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { + attachHandler(http.MethodGet, HomeTimeline, m.HomeTimelineGETHandler) + attachHandler(http.MethodGet, PublicTimeline, m.PublicTimelineGETHandler) } diff --git a/internal/api/client/user/passwordchange.go b/internal/api/client/user/passwordchange.go index a900af897..c766d915c 100644 --- a/internal/api/client/user/passwordchange.go +++ b/internal/api/client/user/passwordchange.go @@ -23,8 +23,8 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + 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" ) @@ -68,35 +68,35 @@ import ( func (m *Module) PasswordChangePOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - form := &model.PasswordChangeRequest{} + form := &apimodel.PasswordChangeRequest{} if err := c.ShouldBind(form); err != nil { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } if form.OldPassword == "" { err := errors.New("password change request missing field old_password") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } if form.NewPassword == "" { err := errors.New("password change request missing field new_password") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } if errWithCode := m.processor.UserChangePassword(c.Request.Context(), authed, form); errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/user/user.go b/internal/api/client/user/user.go index 86a0096e0..5e6002b40 100644 --- a/internal/api/client/user/user.go +++ b/internal/api/client/user/user.go @@ -21,32 +21,27 @@ package user import ( "net/http" - "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" ) const ( - // BasePath is the base URI path for this module - BasePath = "/api/v1/user" + // BasePath is the base URI path for this module, minus the 'api' prefix + BasePath = "/v1/user" // PasswordChangePath is the path for POSTing a password change request. PasswordChangePath = BasePath + "/password_change" ) -// Module implements the ClientAPIModule interface type Module struct { processor processing.Processor } -// New returns a new user module -func New(processor processing.Processor) api.ClientModule { +func New(processor processing.Processor) *Module { return &Module{ processor: processor, } } -// Route attaches all routes from this module to the given router -func (m *Module) Route(r router.Router) error { - r.AttachHandler(http.MethodPost, PasswordChangePath, m.PasswordChangePOSTHandler) - return nil +func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { + attachHandler(http.MethodPost, PasswordChangePath, m.PasswordChangePOSTHandler) } diff --git a/internal/api/client/user/user_test.go b/internal/api/client/user/user_test.go index cc4fafca9..055b1f7a4 100644 --- a/internal/api/client/user/user_test.go +++ b/internal/api/client/user/user_test.go @@ -73,7 +73,7 @@ func (suite *UserStandardTestSuite) SetupTest() { suite.sentEmails = make(map[string]string) suite.emailSender = testrig.NewEmailSender("../../../../web/template/", suite.sentEmails) suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker) - suite.userModule = user.New(suite.processor).(*user.Module) + suite.userModule = user.New(suite.processor) testrig.StandardDBSetup(suite.db, suite.testAccounts) testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") diff --git a/internal/api/fileserver.go b/internal/api/fileserver.go new file mode 100644 index 000000000..8784a8663 --- /dev/null +++ b/internal/api/fileserver.go @@ -0,0 +1,55 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + 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 api + +import ( + "github.com/superseriousbusiness/gotosocial/internal/api/fileserver" + "github.com/superseriousbusiness/gotosocial/internal/middleware" + "github.com/superseriousbusiness/gotosocial/internal/processing" + "github.com/superseriousbusiness/gotosocial/internal/router" +) + +type Fileserver struct { + fileserver *fileserver.Module +} + +func (f *Fileserver) Route(r router.Router) { + fileserverGroup := r.AttachGroup("fileserver") + + // attach middlewares appropriate for this group + fileserverGroup.Use( + middleware.RateLimit(), + // Since we'll never host different files at the same + // URL (bc the ULIDs are generated per piece of media), + // it's sensible and safe to use a long cache here, so + // that clients don't keep fetching files over + over again. + // + // Nevertheless, we should use 'private' to indicate + // that there might be media in there which are gated by ACLs. + middleware.CacheControl("private", "max-age=604800"), + ) + + f.fileserver.Route(fileserverGroup.Handle) +} + +func NewFileserver(p processing.Processor) *Fileserver { + return &Fileserver{ + fileserver: fileserver.New(p), + } +} diff --git a/internal/api/client/fileserver/fileserver.go b/internal/api/fileserver/fileserver.go similarity index 57% rename from internal/api/client/fileserver/fileserver.go rename to internal/api/fileserver/fileserver.go index dcb54f986..bbfd7d200 100644 --- a/internal/api/client/fileserver/fileserver.go +++ b/internal/api/fileserver/fileserver.go @@ -19,18 +19,13 @@ package fileserver import ( - "fmt" "net/http" - "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" - "github.com/superseriousbusiness/gotosocial/internal/uris" ) const ( - // FileServeBasePath forms the first part of the fileserver path. - FileServeBasePath = "/" + uris.FileserverPath // AccountIDKey is the url key for account id (an account ulid) AccountIDKey = "account_id" // MediaTypeKey is the url key for media type (usually something like attachment or header etc) @@ -39,26 +34,21 @@ const ( MediaSizeKey = "media_size" // FileNameKey is the actual filename being sought. Will usually be a UUID then something like .jpeg FileNameKey = "file_name" + // FileServePath is the fileserve path minus the 'fileserver' prefix. + FileServePath = "/:" + AccountIDKey + "/:" + MediaTypeKey + "/:" + MediaSizeKey + "/:" + FileNameKey ) -// FileServer implements the RESTAPIModule interface. -// The goal here is to serve requested media files if the gotosocial server is configured to use local storage. -type FileServer struct { +type Module struct { processor processing.Processor } -// New returns a new fileServer module -func New(processor processing.Processor) api.ClientModule { - return &FileServer{ +func New(processor processing.Processor) *Module { + return &Module{ processor: processor, } } -// Route satisfies the RESTAPIModule interface -func (m *FileServer) Route(s router.Router) error { - // something like "/fileserver/:account_id/:media_type/:media_size/:file_name" - fileServePath := fmt.Sprintf("%s/:%s/:%s/:%s/:%s", FileServeBasePath, AccountIDKey, MediaTypeKey, MediaSizeKey, FileNameKey) - s.AttachHandler(http.MethodGet, fileServePath, m.ServeFile) - s.AttachHandler(http.MethodHead, fileServePath, m.ServeFile) - return nil +func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { + attachHandler(http.MethodGet, FileServePath, m.ServeFile) + attachHandler(http.MethodHead, FileServePath, m.ServeFile) } diff --git a/internal/api/client/fileserver/fileserver_test.go b/internal/api/fileserver/fileserver_test.go similarity index 89% rename from internal/api/client/fileserver/fileserver_test.go rename to internal/api/fileserver/fileserver_test.go index f1fab5672..f6de72a27 100644 --- a/internal/api/client/fileserver/fileserver_test.go +++ b/internal/api/fileserver/fileserver_test.go @@ -22,7 +22,7 @@ import ( "context" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver" + "github.com/superseriousbusiness/gotosocial/internal/api/fileserver" "github.com/superseriousbusiness/gotosocial/internal/concurrency" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/email" @@ -59,7 +59,7 @@ type FileserverTestSuite struct { testAttachments map[string]*gtsmodel.MediaAttachment // item being tested - fileServer *fileserver.FileServer + fileServer *fileserver.Module } /* @@ -75,20 +75,20 @@ func (suite *FileserverTestSuite) SetupSuite() { suite.db = testrig.NewTestDB() suite.storage = testrig.NewInMemoryStorage() - suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker), suite.storage, suite.mediaManager, fedWorker) - suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil) + suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../testrig/media"), suite.db, fedWorker), suite.storage, suite.mediaManager, fedWorker) + suite.emailSender = testrig.NewEmailSender("../../../web/template/", nil) suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, testrig.NewTestMediaManager(suite.db, suite.storage), clientWorker, fedWorker) suite.tc = testrig.NewTestTypeConverter(suite.db) suite.mediaManager = testrig.NewTestMediaManager(suite.db, suite.storage) suite.oauthServer = testrig.NewTestOauthServer(suite.db) - suite.fileServer = fileserver.New(suite.processor).(*fileserver.FileServer) + suite.fileServer = fileserver.New(suite.processor) } func (suite *FileserverTestSuite) SetupTest() { testrig.StandardDBSetup(suite.db, nil) - testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") + testrig.StandardStorageSetup(suite.storage, "../../../testrig/media") suite.testTokens = testrig.NewTestTokens() suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() diff --git a/internal/api/client/fileserver/servefile.go b/internal/api/fileserver/servefile.go similarity index 75% rename from internal/api/client/fileserver/servefile.go rename to internal/api/fileserver/servefile.go index d2328a5fc..08ab3110b 100644 --- a/internal/api/client/fileserver/servefile.go +++ b/internal/api/fileserver/servefile.go @@ -26,8 +26,8 @@ import ( "strconv" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + 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/log" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -37,10 +37,10 @@ import ( // // Note: to mitigate scraping attempts, no information should be given out on a bad request except "404 page not found". // Don't give away account ids or media ids or anything like that; callers shouldn't be able to infer anything. -func (m *FileServer) ServeFile(c *gin.Context) { +func (m *Module) ServeFile(c *gin.Context) { authed, err := oauth.Authed(c, false, false, false, false) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet) return } @@ -50,39 +50,39 @@ func (m *FileServer) ServeFile(c *gin.Context) { accountID := c.Param(AccountIDKey) if accountID == "" { err := fmt.Errorf("missing %s from request", AccountIDKey) - api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet) return } mediaType := c.Param(MediaTypeKey) if mediaType == "" { err := fmt.Errorf("missing %s from request", MediaTypeKey) - api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet) return } mediaSize := c.Param(MediaSizeKey) if mediaSize == "" { err := fmt.Errorf("missing %s from request", MediaSizeKey) - api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet) return } fileName := c.Param(FileNameKey) if fileName == "" { err := fmt.Errorf("missing %s from request", FileNameKey) - api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet) return } - content, errWithCode := m.processor.FileGet(c.Request.Context(), authed, &model.GetContentRequestForm{ + content, errWithCode := m.processor.FileGet(c.Request.Context(), authed, &apimodel.GetContentRequestForm{ AccountID: accountID, MediaType: mediaType, MediaSize: mediaSize, FileName: fileName, }) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } @@ -103,18 +103,13 @@ func (m *FileServer) ServeFile(c *gin.Context) { // TODO: if the requester only accepts text/html we should try to serve them *something*. // This is mostly needed because when sharing a link to a gts-hosted file on something like mastodon, the masto servers will // attempt to look up the content to provide a preview of the link, and they ask for text/html. - format, err := api.NegotiateAccept(c, api.MIME(content.ContentType)) + format, err := apiutil.NegotiateAccept(c, apiutil.MIME(content.ContentType)) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - // since we'll never host different files at the same - // URL (bc the ULIDs are generated per piece of media), - // it's sensible and safe to use a long cache here, so - // that clients don't keep fetching files over + over again - c.Header("Cache-Control", "max-age=604800") - + // if this is a head request, just return info + throw the reader away if c.Request.Method == http.MethodHead { c.Header("Content-Type", format) c.Header("Content-Length", strconv.FormatInt(content.ContentLength, 10)) @@ -126,7 +121,7 @@ func (m *FileServer) ServeFile(c *gin.Context) { b := bytes.NewBuffer(make([]byte, 0, 64)) if _, err := io.CopyN(b, content.Content, 64); err != nil { err = fmt.Errorf("ServeFile: error reading from content: %w", err) - api.ErrorHandler(c, gtserror.NewErrorNotFound(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorNotFound(err, err.Error()), m.processor.InstanceGet) return } diff --git a/internal/api/client/fileserver/servefile_test.go b/internal/api/fileserver/servefile_test.go similarity index 99% rename from internal/api/client/fileserver/servefile_test.go rename to internal/api/fileserver/servefile_test.go index 1ca0c60d6..efaf3896d 100644 --- a/internal/api/client/fileserver/servefile_test.go +++ b/internal/api/fileserver/servefile_test.go @@ -26,7 +26,7 @@ import ( "testing" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver" + "github.com/superseriousbusiness/gotosocial/internal/api/fileserver" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/testrig" diff --git a/internal/api/nodeinfo.go b/internal/api/nodeinfo.go new file mode 100644 index 000000000..906703434 --- /dev/null +++ b/internal/api/nodeinfo.go @@ -0,0 +1,50 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + 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 api + +import ( + "github.com/superseriousbusiness/gotosocial/internal/api/nodeinfo" + "github.com/superseriousbusiness/gotosocial/internal/middleware" + "github.com/superseriousbusiness/gotosocial/internal/processing" + "github.com/superseriousbusiness/gotosocial/internal/router" +) + +type NodeInfo struct { + nodeInfo *nodeinfo.Module +} + +func (w *NodeInfo) Route(r router.Router) { + // group nodeinfo endpoints together + nodeInfoGroup := r.AttachGroup("nodeinfo") + + // attach middlewares appropriate for this group + nodeInfoGroup.Use( + middleware.Gzip(), + middleware.RateLimit(), + middleware.CacheControl("public", "max-age=120"), // allow cache for 2 minutes + ) + + w.nodeInfo.Route(nodeInfoGroup.Handle) +} + +func NewNodeInfo(p processing.Processor) *NodeInfo { + return &NodeInfo{ + nodeInfo: nodeinfo.New(p), + } +} diff --git a/internal/api/nodeinfo/nodeinfo.go b/internal/api/nodeinfo/nodeinfo.go new file mode 100644 index 000000000..aa6aec882 --- /dev/null +++ b/internal/api/nodeinfo/nodeinfo.go @@ -0,0 +1,46 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + 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 nodeinfo + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/processing" +) + +const ( + NodeInfo2Version = "2.0" + NodeInfo2Path = "/" + NodeInfo2Version + NodeInfo2ContentType = "application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/" + NodeInfo2Version + "#\"" +) + +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, NodeInfo2Path, m.NodeInfo2GETHandler) +} diff --git a/internal/api/s2s/nodeinfo/nodeinfoget.go b/internal/api/nodeinfo/nodeinfoget.go similarity index 65% rename from internal/api/s2s/nodeinfo/nodeinfoget.go rename to internal/api/nodeinfo/nodeinfoget.go index 6cb5e1ebf..f2a3604eb 100644 --- a/internal/api/s2s/nodeinfo/nodeinfoget.go +++ b/internal/api/nodeinfo/nodeinfoget.go @@ -23,11 +23,11 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) -// NodeInfoGETHandler swagger:operation GET /nodeinfo/2.0 nodeInfoGet +// NodeInfo2GETHandler swagger:operation GET /nodeinfo/2.0 nodeInfoGet // // Returns a compliant nodeinfo response to node info queries. // @@ -44,23 +44,23 @@ import ( // '200': // schema: // "$ref": "#/definitions/nodeinfo" -func (m *Module) NodeInfoGETHandler(c *gin.Context) { - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) +func (m *Module) NodeInfo2GETHandler(c *gin.Context) { + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - ni, errWithCode := m.processor.GetNodeInfo(c.Request.Context(), c.Request) + nodeInfo, errWithCode := m.processor.GetNodeInfo(c.Request.Context()) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } - b, err := json.Marshal(ni) + b, err := json.Marshal(nodeInfo) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } - c.Data(http.StatusOK, `application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"`, b) + c.Data(http.StatusOK, NodeInfo2ContentType, b) } diff --git a/internal/api/s2s/emoji/emoji.go b/internal/api/s2s/emoji/emoji.go deleted file mode 100644 index d448d2105..000000000 --- a/internal/api/s2s/emoji/emoji.go +++ /dev/null @@ -1,53 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org - - 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 emoji - -import ( - "net/http" - - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" - "github.com/superseriousbusiness/gotosocial/internal/uris" -) - -const ( - // EmojiIDKey is for emoji IDs - EmojiIDKey = "id" - // EmojiBasePath is the base path for serving information about Emojis eg https://example.org/emoji - EmojiWithIDPath = "/" + uris.EmojiPath + "/:" + EmojiIDKey -) - -// Module implements the FederationModule interface -type Module struct { - processor processing.Processor -} - -// New returns a emoji module -func New(processor processing.Processor) api.FederationModule { - return &Module{ - processor: processor, - } -} - -// Route satisfies the RESTAPIModule interface -func (m *Module) Route(s router.Router) error { - s.AttachHandler(http.MethodGet, EmojiWithIDPath, m.EmojiGetHandler) - return nil -} diff --git a/internal/api/s2s/nodeinfo/nodeinfo.go b/internal/api/s2s/nodeinfo/nodeinfo.go deleted file mode 100644 index 539dcc2d1..000000000 --- a/internal/api/s2s/nodeinfo/nodeinfo.go +++ /dev/null @@ -1,53 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org - - 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 nodeinfo - -import ( - "net/http" - - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" -) - -const ( - // NodeInfoWellKnownPath is the base path for serving responses to nodeinfo lookup requests. - NodeInfoWellKnownPath = ".well-known/nodeinfo" - // NodeInfoBasePath is the path for serving nodeinfo responses. - NodeInfoBasePath = "/nodeinfo/2.0" -) - -// Module implements the FederationModule interface -type Module struct { - processor processing.Processor -} - -// New returns a new nodeinfo module -func New(processor processing.Processor) api.FederationModule { - return &Module{ - processor: processor, - } -} - -// Route satisfies the FederationModule interface -func (m *Module) Route(s router.Router) error { - s.AttachHandler(http.MethodGet, NodeInfoWellKnownPath, m.NodeInfoWellKnownGETHandler) - s.AttachHandler(http.MethodGet, NodeInfoBasePath, m.NodeInfoGETHandler) - return nil -} diff --git a/internal/api/s2s/user/user.go b/internal/api/s2s/user/user.go deleted file mode 100644 index 1daa53ad8..000000000 --- a/internal/api/s2s/user/user.go +++ /dev/null @@ -1,89 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org - - 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 user - -import ( - "net/http" - - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" - "github.com/superseriousbusiness/gotosocial/internal/uris" -) - -const ( - // UsernameKey is for account usernames. - UsernameKey = "username" - // StatusIDKey is for status IDs - StatusIDKey = "status" - // OnlyOtherAccountsKey is for filtering status responses. - OnlyOtherAccountsKey = "only_other_accounts" - // MinIDKey is for filtering status responses. - MinIDKey = "min_id" - // MaxIDKey is for filtering status responses. - MaxIDKey = "max_id" - // PageKey is for filtering status responses. - PageKey = "page" - - // UsersBasePath is the base path for serving information about Users eg https://example.org/users - UsersBasePath = "/" + uris.UsersPath - // UsersBasePathWithUsername is just the users base path with the Username key in it. - // Use this anywhere you need to know the username of the user being queried. - // Eg https://example.org/users/:username - UsersBasePathWithUsername = UsersBasePath + "/:" + UsernameKey - // UsersPublicKeyPath is a path to a user's public key, for serving bare minimum AP representations. - UsersPublicKeyPath = UsersBasePathWithUsername + "/" + uris.PublicKeyPath - // UsersInboxPath is for serving POST requests to a user's inbox with the given username key. - UsersInboxPath = UsersBasePathWithUsername + "/" + uris.InboxPath - // UsersOutboxPath is for serving GET requests to a user's outbox with the given username key. - UsersOutboxPath = UsersBasePathWithUsername + "/" + uris.OutboxPath - // UsersFollowersPath is for serving GET request's to a user's followers list, with the given username key. - UsersFollowersPath = UsersBasePathWithUsername + "/" + uris.FollowersPath - // UsersFollowingPath is for serving GET request's to a user's following list, with the given username key. - UsersFollowingPath = UsersBasePathWithUsername + "/" + uris.FollowingPath - // UsersStatusPath is for serving GET requests to a particular status by a user, with the given username key and status ID - UsersStatusPath = UsersBasePathWithUsername + "/" + uris.StatusesPath + "/:" + StatusIDKey - // UsersStatusRepliesPath is for serving the replies collection of a status. - UsersStatusRepliesPath = UsersStatusPath + "/replies" -) - -// Module implements the FederationAPIModule interface -type Module struct { - processor processing.Processor -} - -// New returns a new auth module -func New(processor processing.Processor) api.FederationModule { - return &Module{ - processor: processor, - } -} - -// Route satisfies the RESTAPIModule interface -func (m *Module) Route(s router.Router) error { - s.AttachHandler(http.MethodGet, UsersBasePathWithUsername, m.UsersGETHandler) - s.AttachHandler(http.MethodPost, UsersInboxPath, m.InboxPOSTHandler) - s.AttachHandler(http.MethodGet, UsersFollowersPath, m.FollowersGETHandler) - s.AttachHandler(http.MethodGet, UsersFollowingPath, m.FollowingGETHandler) - s.AttachHandler(http.MethodGet, UsersStatusPath, m.StatusGETHandler) - s.AttachHandler(http.MethodGet, UsersPublicKeyPath, m.PublicKeyGETHandler) - s.AttachHandler(http.MethodGet, UsersStatusRepliesPath, m.StatusRepliesGETHandler) - s.AttachHandler(http.MethodGet, UsersOutboxPath, m.OutboxGETHandler) - return nil -} diff --git a/internal/api/security/flocblock.go b/internal/api/security/flocblock.go deleted file mode 100644 index 0b61f4ef5..000000000 --- a/internal/api/security/flocblock.go +++ /dev/null @@ -1,31 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org - - 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 security - -import "github.com/gin-gonic/gin" - -// FlocBlock is a middleware that prevents google chrome cohort tracking by -// writing the Permissions-Policy header after all other parts of the request -// have been completed. Floc was replaced by Topics in 2022 and the spec says -// that interest-cohort will also block Topics (as of 2022-Nov). -// See: https://smartframe.io/blog/google-topics-api-everything-you-need-to-know -// See: https://github.com/patcg-individual-drafts/topics -func (m *Module) FlocBlock(c *gin.Context) { - c.Header("Permissions-Policy", "browsing-topics=()") -} diff --git a/internal/api/security/ratelimit.go b/internal/api/security/ratelimit.go deleted file mode 100644 index 3c0d078c7..000000000 --- a/internal/api/security/ratelimit.go +++ /dev/null @@ -1,70 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org - - 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 security - -import ( - "net" - "net/http" - "time" - - "github.com/gin-gonic/gin" - limiter "github.com/ulule/limiter/v3" - mgin "github.com/ulule/limiter/v3/drivers/middleware/gin" - memory "github.com/ulule/limiter/v3/drivers/store/memory" -) - -type RateLimitOptions struct { - Period time.Duration - Limit int64 -} - -func (m *Module) LimitReachedHandler(c *gin.Context) { - code := http.StatusTooManyRequests - c.AbortWithStatusJSON(code, gin.H{"error": "rate limit reached"}) -} - -// returns a gin middleware that will automatically rate limit caller (by IP address) -// and enrich the response header with the following headers: -// - `x-ratelimit-limit` maximum number of requests allowed per time period (fixed) -// - `x-ratelimit-remaining` number of remaining requests that can still be performed -// - `x-ratelimit-reset` unix timestamp when the rate limit will reset -// if `x-ratelimit-limit` is exceeded an HTTP 429 error is returned -func (m *Module) RateLimit(rateOptions RateLimitOptions) func(c *gin.Context) { - rate := limiter.Rate{ - Period: rateOptions.Period, - Limit: rateOptions.Limit, - } - - store := memory.NewStore() - - limiterInstance := limiter.New( - store, - rate, - // apply /64 mask to IPv6 addresses - limiter.WithIPv6Mask(net.CIDRMask(64, 128)), - ) - - middleware := mgin.NewMiddleware( - limiterInstance, - // use custom rate limit reached error - mgin.WithLimitReachedHandler(m.LimitReachedHandler), - ) - - return middleware -} diff --git a/internal/api/security/security.go b/internal/api/security/security.go deleted file mode 100644 index 1dce111d3..000000000 --- a/internal/api/security/security.go +++ /dev/null @@ -1,65 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org - - 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 security - -import ( - "net/http" - "time" - - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/oauth" - "github.com/superseriousbusiness/gotosocial/internal/router" -) - -const robotsPath = "/robots.txt" - -// Module implements the ClientAPIModule interface for security middleware -type Module struct { - db db.DB - server oauth.Server -} - -// New returns a new security module -func New(db db.DB, server oauth.Server) api.ClientModule { - return &Module{ - db: db, - server: server, - } -} - -// Route attaches security middleware to the given router -func (m *Module) Route(s router.Router) error { - // only enable rate limit middleware if configured - // advanced-rate-limit-requests is greater than 0 - if rateLimitRequests := config.GetAdvancedRateLimitRequests(); rateLimitRequests > 0 { - s.AttachMiddleware(m.RateLimit(RateLimitOptions{ - Period: 5 * time.Minute, - Limit: int64(rateLimitRequests), - })) - } - s.AttachMiddleware(m.SignatureCheck) - s.AttachMiddleware(m.FlocBlock) - s.AttachMiddleware(m.ExtraHeaders) - s.AttachMiddleware(m.UserAgentBlock) - s.AttachMiddleware(m.TokenCheck) - s.AttachHandler(http.MethodGet, robotsPath, m.RobotsGETHandler) - return nil -} diff --git a/internal/api/security/signaturecheck.go b/internal/api/security/signaturecheck.go deleted file mode 100644 index 1c117cd1b..000000000 --- a/internal/api/security/signaturecheck.go +++ /dev/null @@ -1,51 +0,0 @@ -package security - -import ( - "net/http" - "net/url" - - "github.com/superseriousbusiness/gotosocial/internal/ap" - "github.com/superseriousbusiness/gotosocial/internal/log" - - "github.com/gin-gonic/gin" - "github.com/go-fed/httpsig" -) - -// SignatureCheck checks whether an incoming http request has been signed. If so, it will check if the domain -// that signed the request is permitted to access the server. If it is permitted, the handler will set the key -// verifier and the signature in the gin context for use down the line. -func (m *Module) SignatureCheck(c *gin.Context) { - // create the verifier from the request - // if the request is signed, it will have a signature header - verifier, err := httpsig.NewVerifier(c.Request) - if err == nil { - // the request was signed! - - // The key ID should be given in the signature so that we know where to fetch it from the remote server. - // This will be something like https://example.org/users/whatever_requesting_user#main-key - requestingPublicKeyID, err := url.Parse(verifier.KeyId()) - if err == nil && requestingPublicKeyID != nil { - // we managed to parse the url! - - // if the domain is blocked we want to bail as early as possible - blocked, err := m.db.IsURIBlocked(c.Request.Context(), requestingPublicKeyID) - if err != nil { - log.Errorf("could not tell if domain %s was blocked or not: %s", requestingPublicKeyID.Host, err) - c.AbortWithStatus(http.StatusInternalServerError) - return - } - if blocked { - log.Infof("domain %s is blocked", requestingPublicKeyID.Host) - c.AbortWithStatus(http.StatusForbidden) - return - } - - // set the verifier and signature on the context here to save some work further down the line - c.Set(string(ap.ContextRequestingPublicKeyVerifier), verifier) - signature := c.GetHeader("Signature") - if signature != "" { - c.Set(string(ap.ContextRequestingPublicKeySignature), signature) - } - } - } -} diff --git a/internal/api/security/tokencheck.go b/internal/api/security/tokencheck.go deleted file mode 100644 index 9f2b7f36e..000000000 --- a/internal/api/security/tokencheck.go +++ /dev/null @@ -1,120 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org - - 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 security - -import ( - "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/log" - "github.com/superseriousbusiness/gotosocial/internal/oauth" -) - -// TokenCheck checks if the client has presented a valid oauth Bearer token. -// If so, it will check the User that the token belongs to, and set that in the context of -// the request. Then, it will look up the account for that user, and set that in the request too. -// If user or account can't be found, then the handler won't *fail*, in case the server wants to allow -// public requests that don't have a Bearer token set (eg., for public instance information and so on). -func (m *Module) TokenCheck(c *gin.Context) { - ctx := c.Request.Context() - defer c.Next() - - if c.Request.Header.Get("Authorization") == "" { - // no token set in the header, we can just bail - return - } - - ti, err := m.server.ValidationBearerToken(c.Copy().Request) - if err != nil { - log.Infof("token was passed in Authorization header but we could not validate it: %s", err) - return - } - c.Set(oauth.SessionAuthorizedToken, ti) - - // check for user-level token - if userID := ti.GetUserID(); userID != "" { - log.Tracef("authenticated user %s with bearer token, scope is %s", userID, ti.GetScope()) - - // fetch user for this token - user, err := m.db.GetUserByID(ctx, userID) - if err != nil { - if err != db.ErrNoEntries { - log.Errorf("database error looking for user with id %s: %s", userID, err) - return - } - log.Warnf("no user found for userID %s", userID) - return - } - - if user.ConfirmedAt.IsZero() { - log.Warnf("authenticated user %s has never confirmed thier email address", userID) - return - } - - if !*user.Approved { - log.Warnf("authenticated user %s's account was never approved by an admin", userID) - return - } - - if *user.Disabled { - log.Warnf("authenticated user %s's account was disabled'", userID) - return - } - - c.Set(oauth.SessionAuthorizedUser, user) - - // fetch account for this token - if user.Account == nil { - acct, err := m.db.GetAccountByID(ctx, user.AccountID) - if err != nil { - if err != db.ErrNoEntries { - log.Errorf("database error looking for account with id %s: %s", user.AccountID, err) - return - } - log.Warnf("no account found for userID %s", userID) - return - } - user.Account = acct - } - - if !user.Account.SuspendedAt.IsZero() { - log.Warnf("authenticated user %s's account (accountId=%s) has been suspended", userID, user.AccountID) - return - } - - c.Set(oauth.SessionAuthorizedAccount, user.Account) - } - - // check for application token - if clientID := ti.GetClientID(); clientID != "" { - log.Tracef("authenticated client %s with bearer token, scope is %s", clientID, ti.GetScope()) - - // fetch app for this token - app := >smodel.Application{} - if err := m.db.GetWhere(ctx, []db.Where{{Key: "client_id", Value: clientID}}, app); err != nil { - if err != db.ErrNoEntries { - log.Errorf("database error looking for application with clientID %s: %s", clientID, err) - return - } - log.Warnf("no app found for client %s", clientID) - return - } - c.Set(oauth.SessionAuthorizedApplication, app) - } -} diff --git a/internal/api/errorhandling.go b/internal/api/util/errorhandling.go similarity index 99% rename from internal/api/errorhandling.go rename to internal/api/util/errorhandling.go index f6fec4168..97894dc36 100644 --- a/internal/api/errorhandling.go +++ b/internal/api/util/errorhandling.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package api +package util import ( "context" diff --git a/internal/api/mime.go b/internal/api/util/mime.go similarity index 99% rename from internal/api/mime.go rename to internal/api/util/mime.go index d02b64ab0..396a6f9b3 100644 --- a/internal/api/mime.go +++ b/internal/api/util/mime.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package api +package util // MIME represents a mime-type. type MIME string diff --git a/internal/api/negotiate.go b/internal/api/util/negotiate.go similarity index 99% rename from internal/api/negotiate.go rename to internal/api/util/negotiate.go index cf44f4328..2854ea09c 100644 --- a/internal/api/negotiate.go +++ b/internal/api/util/negotiate.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package api +package util import ( "errors" diff --git a/internal/api/util/signaturectx.go b/internal/api/util/signaturectx.go new file mode 100644 index 000000000..ec7d2c816 --- /dev/null +++ b/internal/api/util/signaturectx.go @@ -0,0 +1,41 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + 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 util + +import ( + "context" + + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/ap" +) + +// TransferSignatureContext transfers a signature verifier and signature from a gin context to a go context. +func TransferSignatureContext(c *gin.Context) context.Context { + ctx := c.Request.Context() + + if verifier, signed := c.Get(string(ap.ContextRequestingPublicKeyVerifier)); signed { + ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeyVerifier, verifier) + } + + if signature, signed := c.Get(string(ap.ContextRequestingPublicKeySignature)); signed { + ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeySignature, signature) + } + + return ctx +} diff --git a/internal/api/wellknown.go b/internal/api/wellknown.go new file mode 100644 index 000000000..e4fe15f94 --- /dev/null +++ b/internal/api/wellknown.go @@ -0,0 +1,55 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + 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 api + +import ( + "github.com/superseriousbusiness/gotosocial/internal/api/wellknown/nodeinfo" + "github.com/superseriousbusiness/gotosocial/internal/api/wellknown/webfinger" + "github.com/superseriousbusiness/gotosocial/internal/middleware" + "github.com/superseriousbusiness/gotosocial/internal/processing" + "github.com/superseriousbusiness/gotosocial/internal/router" +) + +type WellKnown struct { + nodeInfo *nodeinfo.Module + webfinger *webfinger.Module +} + +func (w *WellKnown) Route(r router.Router) { + // group .well-known endpoints together + wellKnownGroup := r.AttachGroup(".well-known") + + // attach middlewares appropriate for this group + wellKnownGroup.Use( + middleware.Gzip(), + middleware.RateLimit(), + // allow .well-known responses to be cached for 2 minutes + middleware.CacheControl("public", "max-age=120"), + ) + + w.nodeInfo.Route(wellKnownGroup.Handle) + w.webfinger.Route(wellKnownGroup.Handle) +} + +func NewWellKnown(p processing.Processor) *WellKnown { + return &WellKnown{ + nodeInfo: nodeinfo.New(p), + webfinger: webfinger.New(p), + } +} diff --git a/internal/api/wellknown/nodeinfo/nodeinfo.go b/internal/api/wellknown/nodeinfo/nodeinfo.go new file mode 100644 index 000000000..fd57ee08b --- /dev/null +++ b/internal/api/wellknown/nodeinfo/nodeinfo.go @@ -0,0 +1,47 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + 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 nodeinfo + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/processing" +) + +const ( + // NodeInfoWellKnownPath is the base path for serving responses + // to nodeinfo lookup requests, minus the '.well-known' prefix. + NodeInfoWellKnownPath = "/nodeinfo" +) + +type Module struct { + processor processing.Processor +} + +// New returns a new nodeinfo module +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, NodeInfoWellKnownPath, m.NodeInfoWellKnownGETHandler) +} diff --git a/internal/api/s2s/nodeinfo/wellknownget.go b/internal/api/wellknown/nodeinfo/nodeinfoget.go similarity index 73% rename from internal/api/s2s/nodeinfo/wellknownget.go rename to internal/api/wellknown/nodeinfo/nodeinfoget.go index dc14e43a3..21b29ebd9 100644 --- a/internal/api/s2s/nodeinfo/wellknownget.go +++ b/internal/api/wellknown/nodeinfo/nodeinfoget.go @@ -22,20 +22,20 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) // NodeInfoWellKnownGETHandler swagger:operation GET /.well-known/nodeinfo nodeInfoWellKnownGet // -// Directs callers to /nodeinfo/2.0. +// Returns a well-known response which redirects callers to `/nodeinfo/2.0`. // // eg. `{"links":[{"rel":"http://nodeinfo.diaspora.software/ns/schema/2.0","href":"http://example.org/nodeinfo/2.0"}]}` // See: https://nodeinfo.diaspora.software/protocol.html // // --- // tags: -// - nodeinfo +// - .well-known // // produces: // - application/json @@ -45,16 +45,16 @@ import ( // schema: // "$ref": "#/definitions/wellKnownResponse" func (m *Module) NodeInfoWellKnownGETHandler(c *gin.Context) { - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - niRel, errWithCode := m.processor.GetNodeInfoRel(c.Request.Context(), c.Request) + resp, errWithCode := m.processor.GetNodeInfoRel(c.Request.Context()) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } - c.JSON(http.StatusOK, niRel) + c.JSON(http.StatusOK, resp) } diff --git a/internal/api/s2s/webfinger/webfinger.go b/internal/api/wellknown/webfinger/webfinger.go similarity index 62% rename from internal/api/s2s/webfinger/webfinger.go rename to internal/api/wellknown/webfinger/webfinger.go index c46ca7260..8d509b409 100644 --- a/internal/api/s2s/webfinger/webfinger.go +++ b/internal/api/wellknown/webfinger/webfinger.go @@ -21,30 +21,26 @@ package webfinger import ( "net/http" - "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" ) const ( - // WebfingerBasePath is the base path for serving webfinger lookup requests - WebfingerBasePath = ".well-known/webfinger" + // WebfingerBasePath is the base path for serving webfinger + // lookup requests, minus the .well-known prefix + WebfingerBasePath = "/webfinger" ) -// Module implements the FederationModule interface type Module struct { processor processing.Processor } -// New returns a new webfinger module -func New(processor processing.Processor) api.FederationModule { +func New(processor processing.Processor) *Module { return &Module{ processor: processor, } } -// Route satisfies the FederationModule interface -func (m *Module) Route(s router.Router) error { - s.AttachHandler(http.MethodGet, WebfingerBasePath, m.WebfingerGETRequest) - return nil +func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { + attachHandler(http.MethodGet, WebfingerBasePath, m.WebfingerGETRequest) } diff --git a/internal/api/s2s/webfinger/webfinger_test.go b/internal/api/wellknown/webfinger/webfinger_test.go similarity index 89% rename from internal/api/s2s/webfinger/webfinger_test.go rename to internal/api/wellknown/webfinger/webfinger_test.go index e5d026d06..bb9f82ccb 100644 --- a/internal/api/s2s/webfinger/webfinger_test.go +++ b/internal/api/wellknown/webfinger/webfinger_test.go @@ -25,8 +25,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/ap" - "github.com/superseriousbusiness/gotosocial/internal/api/s2s/webfinger" - "github.com/superseriousbusiness/gotosocial/internal/api/security" + "github.com/superseriousbusiness/gotosocial/internal/api/wellknown/webfinger" "github.com/superseriousbusiness/gotosocial/internal/concurrency" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/email" @@ -44,15 +43,14 @@ import ( type WebfingerStandardTestSuite struct { // standard suite interfaces suite.Suite - db db.DB - tc typeutils.TypeConverter - mediaManager media.Manager - federator federation.Federator - emailSender email.Sender - processor processing.Processor - storage *storage.Driver - oauthServer oauth.Server - securityModule *security.Module + db db.DB + tc typeutils.TypeConverter + mediaManager media.Manager + federator federation.Federator + emailSender email.Sender + processor processing.Processor + storage *storage.Driver + oauthServer oauth.Server // standard suite models testTokens map[string]*gtsmodel.Token @@ -91,9 +89,8 @@ func (suite *WebfingerStandardTestSuite) SetupTest() { suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker), suite.storage, suite.mediaManager, fedWorker) suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil) suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker) - suite.webfingerModule = webfinger.New(suite.processor).(*webfinger.Module) + suite.webfingerModule = webfinger.New(suite.processor) suite.oauthServer = testrig.NewTestOauthServer(suite.db) - suite.securityModule = security.New(suite.db, suite.oauthServer).(*security.Module) testrig.StandardDBSetup(suite.db, suite.testAccounts) testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") diff --git a/internal/api/s2s/webfinger/webfingerget.go b/internal/api/wellknown/webfinger/webfingerget.go similarity index 59% rename from internal/api/s2s/webfinger/webfingerget.go rename to internal/api/wellknown/webfinger/webfingerget.go index 9949140c1..72d011734 100644 --- a/internal/api/s2s/webfinger/webfingerget.go +++ b/internal/api/wellknown/webfinger/webfingerget.go @@ -19,16 +19,14 @@ package webfinger import ( - "context" + "errors" "fmt" "net/http" - "codeberg.org/gruf/go-kv" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/ap" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/log" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -48,7 +46,7 @@ import ( // // --- // tags: -// - webfinger +// - .well-known // // produces: // - application/json @@ -58,43 +56,34 @@ import ( // schema: // "$ref": "#/definitions/wellKnownResponse" func (m *Module) WebfingerGETRequest(c *gin.Context) { - l := log.WithFields(kv.Fields{ - {K: "user-agent", V: c.Request.UserAgent()}, - }...) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + return + } resourceQuery, set := c.GetQuery("resource") if !set || resourceQuery == "" { - l.Debug("aborting request because no resource was set in query") - c.JSON(http.StatusBadRequest, gin.H{"error": "no 'resource' in request query"}) + err := errors.New("no 'resource' in request query") + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } requestedUsername, requestedHost, err := util.ExtractWebfingerParts(resourceQuery) if err != nil { - l.Debugf("bad webfinger request with resource query %s: %s", resourceQuery, err) - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("bad webfinger request with resource query %s", resourceQuery)}) + err := fmt.Errorf("bad webfinger request with resource query %s: %w", resourceQuery, err) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - accountDomain := config.GetAccountDomain() - host := config.GetHost() - - if requestedHost != host && requestedHost != accountDomain { - l.Debugf("aborting request because requestedHost %s does not belong to this instance", requestedHost) - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("requested host %s does not belong to this instance", requestedHost)}) + if requestedHost != config.GetHost() && requestedHost != config.GetAccountDomain() { + err := fmt.Errorf("requested host %s does not belong to this instance", requestedHost) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - // transfer the signature verifier from the gin context to the request context - ctx := c.Request.Context() - verifier, signed := c.Get(string(ap.ContextRequestingPublicKeyVerifier)) - if signed { - ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeyVerifier, verifier) - } - - resp, errWithCode := m.processor.GetWebfingerAccount(ctx, requestedUsername) + resp, errWithCode := m.processor.GetWebfingerAccount(c.Request.Context(), requestedUsername) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/s2s/webfinger/webfingerget_test.go b/internal/api/wellknown/webfinger/webfingerget_test.go similarity index 97% rename from internal/api/s2s/webfinger/webfingerget_test.go rename to internal/api/wellknown/webfinger/webfingerget_test.go index 3e91b8f6a..70099b930 100644 --- a/internal/api/s2s/webfinger/webfingerget_test.go +++ b/internal/api/wellknown/webfinger/webfingerget_test.go @@ -28,7 +28,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/s2s/webfinger" + "github.com/superseriousbusiness/gotosocial/internal/api/wellknown/webfinger" "github.com/superseriousbusiness/gotosocial/internal/concurrency" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/messages" @@ -73,7 +73,7 @@ func (suite *WebfingerGetTestSuite) TestFingerUserWithDifferentAccountDomainByHo clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1) fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) suite.processor = processing.NewProcessor(suite.tc, suite.federator, testrig.NewTestOauthServer(suite.db), testrig.NewTestMediaManager(suite.db, suite.storage), suite.storage, suite.db, suite.emailSender, clientWorker, fedWorker) - suite.webfingerModule = webfinger.New(suite.processor).(*webfinger.Module) + suite.webfingerModule = webfinger.New(suite.processor) targetAccount := accountDomainAccount() if err := suite.db.Put(context.Background(), targetAccount); err != nil { @@ -110,7 +110,7 @@ func (suite *WebfingerGetTestSuite) TestFingerUserWithDifferentAccountDomainByAc clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1) fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) suite.processor = processing.NewProcessor(suite.tc, suite.federator, testrig.NewTestOauthServer(suite.db), testrig.NewTestMediaManager(suite.db, suite.storage), suite.storage, suite.db, suite.emailSender, clientWorker, fedWorker) - suite.webfingerModule = webfinger.New(suite.processor).(*webfinger.Module) + suite.webfingerModule = webfinger.New(suite.processor) targetAccount := accountDomainAccount() if err := suite.db.Put(context.Background(), targetAccount); err != nil { diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 4fd783611..6d589439a 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -105,7 +105,7 @@ var Defaults = Configuration{ SyslogAddress: "localhost:514", AdvancedCookiesSamesite: "lax", - AdvancedRateLimitRequests: 1000, // per 5 minutes + AdvancedRateLimitRequests: 300, // 1 per second per 5 minutes Cache: CacheConfiguration{ GTS: GTSCacheConfiguration{ diff --git a/internal/db/db.go b/internal/db/db.go index 8ec70d8b2..f07683cab 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -53,7 +53,7 @@ type DB interface { // TagStringsToTags takes a slice of deduplicated, lowercase tags in the form "somehashtag", which have been // used in a status. It takes the id of the account that wrote the status, and the id of the status itself, and then - // returns a slice of *model.Tag corresponding to the given tags. If the tag already exists in database, that tag + // returns a slice of *apimodel.Tag corresponding to the given tags. If the tag already exists in database, that tag // will be returned. Otherwise a pointer to a new tag struct will be created and returned. // // Note: this func doesn't/shouldn't do any manipulation of the tags in the DB, it's just for checking diff --git a/internal/api/security/useragentblock.go b/internal/middleware/cachecontrol.go similarity index 65% rename from internal/api/security/useragentblock.go rename to internal/middleware/cachecontrol.go index b117e8608..6e88daf1a 100644 --- a/internal/api/security/useragentblock.go +++ b/internal/middleware/cachecontrol.go @@ -16,20 +16,20 @@ along with this program. If not, see . */ -package security +package middleware import ( - "errors" - "net/http" + "strings" "github.com/gin-gonic/gin" ) -// UserAgentBlock aborts requests with empty user agent strings. -func (m *Module) UserAgentBlock(c *gin.Context) { - if ua := c.Request.UserAgent(); ua == "" { - code := http.StatusTeapot - err := errors.New(http.StatusText(code) + ": no user-agent sent with request") - c.AbortWithStatusJSON(code, gin.H{"error": err.Error()}) +// CacheControl returns a new gin middleware which allows callers to control cache settings on response headers. +// +// For directives, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control +func CacheControl(directives ...string) gin.HandlerFunc { + ccHeader := strings.Join(directives, ", ") + return func(c *gin.Context) { + c.Header("Cache-Control", ccHeader) } } diff --git a/internal/middleware/cors.go b/internal/middleware/cors.go new file mode 100644 index 000000000..79b72185f --- /dev/null +++ b/internal/middleware/cors.go @@ -0,0 +1,86 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + 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 middleware + +import ( + "time" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +// CORS returns a new gin middleware which allows CORS requests to be processed. +// This is necessary in order for web/browser-based clients like Pinafore to work. +func CORS() gin.HandlerFunc { + cfg := cors.Config{ + // todo: use config to customize this + AllowAllOrigins: true, + + // adds the following: + // "chrome-extension://" + // "safari-extension://" + // "moz-extension://" + // "ms-browser-extension://" + AllowBrowserExtensions: true, + AllowMethods: []string{ + "POST", + "PUT", + "DELETE", + "GET", + "PATCH", + "OPTIONS", + }, + AllowHeaders: []string{ + // basic cors stuff + "Origin", + "Content-Length", + "Content-Type", + + // needed to pass oauth bearer tokens + "Authorization", + + // needed for websocket upgrade requests + "Upgrade", + "Sec-WebSocket-Extensions", + "Sec-WebSocket-Key", + "Sec-WebSocket-Protocol", + "Sec-WebSocket-Version", + "Connection", + }, + AllowWebSockets: true, + ExposeHeaders: []string{ + // needed for accessing next/prev links when making GET timeline requests + "Link", + + // needed so clients can handle rate limits + "X-RateLimit-Reset", + "X-RateLimit-Limit", + "X-RateLimit-Remaining", + "X-Request-Id", + + // websocket stuff + "Connection", + "Sec-WebSocket-Accept", + "Upgrade", + }, + MaxAge: 2 * time.Minute, + } + + return cors.New(cfg) +} diff --git a/internal/api/security/extraheaders.go b/internal/middleware/extraheaders.go similarity index 52% rename from internal/api/security/extraheaders.go rename to internal/middleware/extraheaders.go index f66ac43b4..c7d4dd3e2 100644 --- a/internal/api/security/extraheaders.go +++ b/internal/middleware/extraheaders.go @@ -16,11 +16,22 @@ along with this program. If not, see . */ -package security +package middleware import "github.com/gin-gonic/gin" -// ExtraHeaders adds any additional required headers to the response -func (m *Module) ExtraHeaders(c *gin.Context) { - c.Header("Server", "gotosocial") +// ExtraHeaders returns a new gin middleware which adds various extra headers to the response. +func ExtraHeaders() gin.HandlerFunc { + return func(c *gin.Context) { + // Inform all callers which server implementation this is. + c.Header("Server", "gotosocial") + // Prevent google chrome cohort tracking. Originally this was referred + // to as FlocBlock. Floc was replaced by Topics in 2022 and the spec says + // that interest-cohort will also block Topics (as of 2022-Nov). + // + // See: https://smartframe.io/blog/google-topics-api-everything-you-need-to-know + // + // See: https://github.com/patcg-individual-drafts/topics + c.Header("Permissions-Policy", "browsing-topics=()") + } } diff --git a/internal/router/gzip.go b/internal/middleware/gzip.go similarity index 80% rename from internal/router/gzip.go rename to internal/middleware/gzip.go index 4c5c99eee..ddea62b63 100644 --- a/internal/router/gzip.go +++ b/internal/middleware/gzip.go @@ -16,15 +16,15 @@ along with this program. If not, see . */ -package router +package middleware import ( ginGzip "github.com/gin-contrib/gzip" "github.com/gin-gonic/gin" ) -func useGzip(engine *gin.Engine) error { - gzipMiddleware := ginGzip.Gzip(ginGzip.DefaultCompression) - engine.Use(gzipMiddleware) - return nil +// Gzip returns a gzip gin middleware using default compression. +func Gzip() gin.HandlerFunc { + // todo: make this configurable + return ginGzip.Gzip(ginGzip.DefaultCompression) } diff --git a/internal/middleware/logger.go b/internal/middleware/logger.go new file mode 100644 index 000000000..7474b0fe0 --- /dev/null +++ b/internal/middleware/logger.go @@ -0,0 +1,99 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + 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 middleware + +import ( + "fmt" + "net/http" + "time" + + "codeberg.org/gruf/go-bytesize" + "codeberg.org/gruf/go-errors/v2" + "codeberg.org/gruf/go-kv" + "codeberg.org/gruf/go-logger/v2/level" + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/log" +) + +// Logger returns a gin middleware which provides request logging and panic recovery. +func Logger() gin.HandlerFunc { + return func(c *gin.Context) { + // Initialize the logging fields + fields := make(kv.Fields, 6, 7) + + // Determine pre-handler time + before := time.Now() + + // defer so that we log *after the request has completed* + defer func() { + code := c.Writer.Status() + path := c.Request.URL.Path + + if r := recover(); r != nil { + if c.Writer.Status() == 0 { + // No response was written, send a generic Internal Error + c.Writer.WriteHeader(http.StatusInternalServerError) + } + + // Append panic information to the request ctx + err := fmt.Errorf("recovered panic: %v", r) + _ = c.Error(err) + + // Dump a stacktrace to error log + callers := errors.GetCallers(3, 10) + log.WithField("stacktrace", callers).Error(err) + } + + // NOTE: + // It is very important here that we are ONLY logging + // the request path, and none of the query parameters. + // Query parameters can contain sensitive information + // and could lead to storing plaintext API keys in logs + + // Set request logging fields + fields[0] = kv.Field{"latency", time.Since(before)} + fields[1] = kv.Field{"clientIP", c.ClientIP()} + fields[2] = kv.Field{"userAgent", c.Request.UserAgent()} + fields[3] = kv.Field{"method", c.Request.Method} + fields[4] = kv.Field{"statusCode", code} + fields[5] = kv.Field{"path", path} + + // Create log entry with fields + l := log.WithFields(fields...) + + // Default is info + lvl := level.INFO + + if code >= 500 { + // This is a server error + lvl = level.ERROR + l = l.WithField("error", c.Errors) + } + + // Generate a nicer looking bytecount + size := bytesize.Size(c.Writer.Size()) + + // Finally, write log entry with status text body size + l.Logf(lvl, "%s: wrote %s", http.StatusText(code), size) + }() + + // Process request + c.Next() + } +} diff --git a/internal/middleware/ratelimit.go b/internal/middleware/ratelimit.go new file mode 100644 index 000000000..ab947a124 --- /dev/null +++ b/internal/middleware/ratelimit.go @@ -0,0 +1,77 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + 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 middleware + +import ( + "net" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/ulule/limiter/v3" + limitergin "github.com/ulule/limiter/v3/drivers/middleware/gin" + "github.com/ulule/limiter/v3/drivers/store/memory" +) + +const rateLimitPeriod = 5 * time.Minute + +// RateLimit returns a gin middleware that will automatically rate limit caller (by IP address), +// and enrich the response header with the following headers: +// +// - `x-ratelimit-limit` - maximum number of requests allowed per time period (fixed). +// - `x-ratelimit-remaining` - number of remaining requests that can still be performed. +// - `x-ratelimit-reset` - unix timestamp when the rate limit will reset. +// +// If `x-ratelimit-limit` is exceeded, the request is aborted and an HTTP 429 TooManyRequests +// status is returned. +// +// If the config AdvancedRateLimitRequests value is <= 0, then a noop handler will be returned, +// which performs no rate limiting. +func RateLimit() gin.HandlerFunc { + // only enable rate limit middleware if configured + // advanced-rate-limit-requests is greater than 0 + rateLimitRequests := config.GetAdvancedRateLimitRequests() + if rateLimitRequests <= 0 { + // use noop middleware if ratelimiting is disabled + return func(c *gin.Context) {} + } + + rate := limiter.Rate{ + Period: rateLimitPeriod, + Limit: int64(rateLimitRequests), + } + + limiterInstance := limiter.New( + memory.NewStore(), + rate, + limiter.WithIPv6Mask(net.CIDRMask(64, 128)), // apply /64 mask to IPv6 addresses + ) + + limitReachedHandler := func(c *gin.Context) { + c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "rate limit reached"}) + } + + middleware := limitergin.NewMiddleware( + limiterInstance, + limitergin.WithLimitReachedHandler(limitReachedHandler), // use custom rate limit reached error + ) + + return middleware +} diff --git a/internal/router/session.go b/internal/middleware/session.go similarity index 82% rename from internal/router/session.go rename to internal/middleware/session.go index eb4b83874..bffbdcd92 100644 --- a/internal/router/session.go +++ b/internal/middleware/session.go @@ -16,11 +16,9 @@ along with this program. If not, see . */ -package router +package middleware import ( - "context" - "errors" "fmt" "net/http" "net/url" @@ -30,7 +28,6 @@ import ( "github.com/gin-contrib/sessions/memstore" "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/log" "golang.org/x/net/idna" ) @@ -88,24 +85,11 @@ func SessionName() (string, error) { return fmt.Sprintf("gotosocial-%s", punyHostname), nil } -func useSession(ctx context.Context, sessionDB db.Session, engine *gin.Engine) error { - // check if we have a saved router session already - rs, err := sessionDB.GetSession(ctx) - if err != nil { - return fmt.Errorf("error using session: %s", err) - } - if rs == nil || rs.Auth == nil || rs.Crypt == nil { - return errors.New("router session was nil") - } - - store := memstore.NewStore(rs.Auth, rs.Crypt) +// Session returns a new gin middleware that implements session cookies using the given +// sessionName, authentication key, and encryption key. Session name can be derived from the +// SessionName utility function in this package. +func Session(sessionName string, auth []byte, crypt []byte) gin.HandlerFunc { + store := memstore.NewStore(auth, crypt) store.Options(SessionOptions()) - - sessionName, err := SessionName() - if err != nil { - return err - } - - engine.Use(sessions.Sessions(sessionName, store)) - return nil + return sessions.Sessions(sessionName, store) } diff --git a/internal/router/session_test.go b/internal/middleware/session_test.go similarity index 86% rename from internal/router/session_test.go rename to internal/middleware/session_test.go index 5f4777a48..d11d8c202 100644 --- a/internal/router/session_test.go +++ b/internal/middleware/session_test.go @@ -16,14 +16,14 @@ along with this program. If not, see . */ -package router_test +package middleware_test import ( "testing" "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/router" + "github.com/superseriousbusiness/gotosocial/internal/middleware" "github.com/superseriousbusiness/gotosocial/testrig" ) @@ -39,7 +39,7 @@ func (suite *SessionTestSuite) TestDeriveSessionNameLocalhostWithPort() { config.SetProtocol("http") config.SetHost("localhost:8080") - sessionName, err := router.SessionName() + sessionName, err := middleware.SessionName() suite.NoError(err) suite.Equal("gotosocial-localhost", sessionName) } @@ -48,7 +48,7 @@ func (suite *SessionTestSuite) TestDeriveSessionNameLocalhost() { config.SetProtocol("http") config.SetHost("localhost") - sessionName, err := router.SessionName() + sessionName, err := middleware.SessionName() suite.NoError(err) suite.Equal("gotosocial-localhost", sessionName) } @@ -57,7 +57,7 @@ func (suite *SessionTestSuite) TestDeriveSessionNoProtocol() { config.SetProtocol("") config.SetHost("localhost") - sessionName, err := router.SessionName() + sessionName, err := middleware.SessionName() suite.EqualError(err, "parse \"://localhost\": missing protocol scheme") suite.Equal("", sessionName) } @@ -67,7 +67,7 @@ func (suite *SessionTestSuite) TestDeriveSessionNoHost() { config.SetHost("") config.SetPort(0) - sessionName, err := router.SessionName() + sessionName, err := middleware.SessionName() suite.EqualError(err, "could not derive hostname without port from https://") suite.Equal("", sessionName) } @@ -76,7 +76,7 @@ func (suite *SessionTestSuite) TestDeriveSessionOK() { config.SetProtocol("https") config.SetHost("example.org") - sessionName, err := router.SessionName() + sessionName, err := middleware.SessionName() suite.NoError(err) suite.Equal("gotosocial-example.org", sessionName) } @@ -85,7 +85,7 @@ func (suite *SessionTestSuite) TestDeriveSessionIDNOK() { config.SetProtocol("https") config.SetHost("fóid.org") - sessionName, err := router.SessionName() + sessionName, err := middleware.SessionName() suite.NoError(err) suite.Equal("gotosocial-xn--fid-gna.org", sessionName) } diff --git a/internal/middleware/signaturecheck.go b/internal/middleware/signaturecheck.go new file mode 100644 index 000000000..c1f190eb5 --- /dev/null +++ b/internal/middleware/signaturecheck.go @@ -0,0 +1,93 @@ +package middleware + +import ( + "context" + "fmt" + "net/http" + "net/url" + + "github.com/superseriousbusiness/gotosocial/internal/ap" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/log" + + "github.com/gin-gonic/gin" + "github.com/go-fed/httpsig" +) + +var ( + // this mimics an untyped error returned by httpsig when no signature is present; + // define it here so that we can use it to decide what to log without hitting + // performance too hard + noSignatureError = fmt.Sprintf("neither %q nor %q have signature parameters", httpsig.Signature, httpsig.Authorization) + signatureHeader = string(httpsig.Signature) + authorizationHeader = string(httpsig.Authorization) +) + +// SignatureCheck returns a gin middleware for checking http signatures. +// +// The middleware first checks whether an incoming http request has been http-signed with a well-formed signature. +// +// If so, it will check if the domain that signed the request is permitted to access the server, using the provided isURIBlocked function. +// +// If it is permitted, the handler will set the key verifier and the signature in the gin context for use down the line. +// +// If the domain is blocked, the middleware will abort the request chain instead with http code 403 forbidden. +// +// In case of an error, the request will be aborted with http code 500 internal server error. +func SignatureCheck(isURIBlocked func(context.Context, *url.URL) (bool, db.Error)) func(*gin.Context) { + return func(c *gin.Context) { + // create the verifier from the request, this will error if the request wasn't signed + verifier, err := httpsig.NewVerifier(c.Request) + if err != nil { + // Something went wrong, so we need to return regardless, but only actually + // *abort* the request with 401 if a signature was present but malformed + if err.Error() != noSignatureError { + log.Debugf("http signature was present but invalid: %s", err) + c.AbortWithStatus(http.StatusUnauthorized) + } + return + } + + // The request was signed! + // The key ID should be given in the signature so that we know where to fetch it from the remote server. + // This will be something like https://example.org/users/whatever_requesting_user#main-key + requestingPublicKeyIDString := verifier.KeyId() + requestingPublicKeyID, err := url.Parse(requestingPublicKeyIDString) + if err != nil { + log.Debugf("http signature requesting public key id %s could not be parsed as a url: %s", requestingPublicKeyIDString, err) + c.AbortWithStatus(http.StatusUnauthorized) + return + } else if requestingPublicKeyID == nil { + // Key can sometimes be nil, according to url parse function: + // 'Trying to parse a hostname and path without a scheme is invalid but may not necessarily return an error, due to parsing ambiguities' + log.Debugf("http signature requesting public key id %s was nil after parsing as a url", requestingPublicKeyIDString) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + // we managed to parse the url! + // if the domain is blocked we want to bail as early as possible + if blocked, err := isURIBlocked(c.Request.Context(), requestingPublicKeyID); err != nil { + log.Errorf("could not tell if domain %s was blocked or not: %s", requestingPublicKeyID.Host, err) + c.AbortWithStatus(http.StatusInternalServerError) + return + } else if blocked { + log.Infof("domain %s is blocked", requestingPublicKeyID.Host) + c.AbortWithStatus(http.StatusForbidden) + return + } + + // assume signature was set on Signature header (most common behavior), + // but fall back to Authorization header if necessary + var signature string + if s := c.GetHeader(signatureHeader); s != "" { + signature = s + } else { + signature = c.GetHeader(authorizationHeader) + } + + // set the verifier and signature on the context here to save some work further down the line + c.Set(string(ap.ContextRequestingPublicKeyVerifier), verifier) + c.Set(string(ap.ContextRequestingPublicKeySignature), signature) + } +} diff --git a/internal/middleware/tokencheck.go b/internal/middleware/tokencheck.go new file mode 100644 index 000000000..ca93f379a --- /dev/null +++ b/internal/middleware/tokencheck.go @@ -0,0 +1,140 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + 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 middleware + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" + "github.com/superseriousbusiness/gotosocial/internal/oauth" + "github.com/superseriousbusiness/oauth2/v4" +) + +// TokenCheck returns a new gin middleware for validating oauth tokens in requests. +// +// The middleware checks the request Authorization header for a valid oauth Bearer token. +// +// If no token was set in the Authorization header, or the token was invalid, the handler will return. +// +// If a valid oauth Bearer token was provided, it will be set on the gin context for further use. +// +// Then, it will check which *gtsmodel.User the token belongs to. If the user is not confirmed, not approved, +// or has been disabled, then the middleware will return early. Otherwise, the User will be set on the +// gin context for further processing by other functions. +// +// Next, it will look up the *gtsmodel.Account for the User. If the Account has been suspended, then the +// middleware will return early. Otherwise, it will set the Account on the gin context too. +// +// Finally, it will check the client ID of the token to see if a *gtsmodel.Application can be retrieved +// for that client ID. This will also be set on the gin context. +// +// If an invalid token is presented, or a user/account/application can't be found, then this middleware +// won't abort the request, since the server might want to still allow public requests that don't have a +// Bearer token set (eg., for public instance information and so on). +func TokenCheck(dbConn db.DB, validateBearerToken func(r *http.Request) (oauth2.TokenInfo, error)) func(*gin.Context) { + return func(c *gin.Context) { + ctx := c.Request.Context() + + if c.Request.Header.Get("Authorization") == "" { + // no token set in the header, we can just bail + return + } + + ti, err := validateBearerToken(c.Copy().Request) + if err != nil { + log.Debugf("token was passed in Authorization header but we could not validate it: %s", err) + return + } + c.Set(oauth.SessionAuthorizedToken, ti) + + // check for user-level token + if userID := ti.GetUserID(); userID != "" { + log.Tracef("authenticated user %s with bearer token, scope is %s", userID, ti.GetScope()) + + // fetch user for this token + user, err := dbConn.GetUserByID(ctx, userID) + if err != nil { + if err != db.ErrNoEntries { + log.Errorf("database error looking for user with id %s: %s", userID, err) + return + } + log.Warnf("no user found for userID %s", userID) + return + } + + if user.ConfirmedAt.IsZero() { + log.Warnf("authenticated user %s has never confirmed thier email address", userID) + return + } + + if !*user.Approved { + log.Warnf("authenticated user %s's account was never approved by an admin", userID) + return + } + + if *user.Disabled { + log.Warnf("authenticated user %s's account was disabled'", userID) + return + } + + c.Set(oauth.SessionAuthorizedUser, user) + + // fetch account for this token + if user.Account == nil { + acct, err := dbConn.GetAccountByID(ctx, user.AccountID) + if err != nil { + if err != db.ErrNoEntries { + log.Errorf("database error looking for account with id %s: %s", user.AccountID, err) + return + } + log.Warnf("no account found for userID %s", userID) + return + } + user.Account = acct + } + + if !user.Account.SuspendedAt.IsZero() { + log.Warnf("authenticated user %s's account (accountId=%s) has been suspended", userID, user.AccountID) + return + } + + c.Set(oauth.SessionAuthorizedAccount, user.Account) + } + + // check for application token + if clientID := ti.GetClientID(); clientID != "" { + log.Tracef("authenticated client %s with bearer token, scope is %s", clientID, ti.GetScope()) + + // fetch app for this token + app := >smodel.Application{} + if err := dbConn.GetWhere(ctx, []db.Where{{Key: "client_id", Value: clientID}}, app); err != nil { + if err != db.ErrNoEntries { + log.Errorf("database error looking for application with clientID %s: %s", clientID, err) + return + } + log.Warnf("no app found for client %s", clientID) + return + } + c.Set(oauth.SessionAuthorizedApplication, app) + } + } +} diff --git a/internal/api/client/auth/util.go b/internal/middleware/useragent.go similarity index 59% rename from internal/api/client/auth/util.go rename to internal/middleware/useragent.go index d59983c55..e55a19434 100644 --- a/internal/api/client/auth/util.go +++ b/internal/middleware/useragent.go @@ -16,16 +16,24 @@ along with this program. If not, see . */ -package auth +package middleware import ( - "github.com/gin-contrib/sessions" + "errors" + "net/http" + + "github.com/gin-gonic/gin" ) -func (m *Module) clearSession(s sessions.Session) { - s.Clear() - - if err := s.Save(); err != nil { - panic(err) +// UserAgent returns a gin middleware which aborts requests with +// empty user agent strings, returning code 418 - I'm a teapot. +func UserAgent() gin.HandlerFunc { + // todo: make this configurable + return func(c *gin.Context) { + if ua := c.Request.UserAgent(); ua == "" { + code := http.StatusTeapot + err := errors.New(http.StatusText(code) + ": no user-agent sent with request") + c.AbortWithStatusJSON(code, gin.H{"error": err.Error()}) + } } } diff --git a/internal/oidc/idp.go b/internal/oidc/idp.go index 90aee81f4..24af6835d 100644 --- a/internal/oidc/idp.go +++ b/internal/oidc/idp.go @@ -52,15 +52,7 @@ type idp struct { } // NewIDP returns a new IDP configured with the given config. -// If the passed config contains a nil value for the OIDCConfig, or OIDCConfig.Enabled -// is set to false, then nil, nil will be returned. If OIDCConfig.Enabled is true, -// then the other OIDC config fields must also be set. func NewIDP(ctx context.Context) (IDP, error) { - if !config.GetOIDCEnabled() { - // oidc isn't enabled so we don't need to do anything - return nil, nil - } - // validate config fields idpName := config.GetOIDCIdpName() if idpName == "" { diff --git a/internal/processing/federation.go b/internal/processing/federation.go index b7b05d0fa..951bda5c2 100644 --- a/internal/processing/federation.go +++ b/internal/processing/federation.go @@ -59,12 +59,12 @@ func (p *processor) GetWebfingerAccount(ctx context.Context, requestedUsername s return p.federationProcessor.GetWebfingerAccount(ctx, requestedUsername) } -func (p *processor) GetNodeInfoRel(ctx context.Context, request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode) { - return p.federationProcessor.GetNodeInfoRel(ctx, request) +func (p *processor) GetNodeInfoRel(ctx context.Context) (*apimodel.WellKnownResponse, gtserror.WithCode) { + return p.federationProcessor.GetNodeInfoRel(ctx) } -func (p *processor) GetNodeInfo(ctx context.Context, request *http.Request) (*apimodel.Nodeinfo, gtserror.WithCode) { - return p.federationProcessor.GetNodeInfo(ctx, request) +func (p *processor) GetNodeInfo(ctx context.Context) (*apimodel.Nodeinfo, gtserror.WithCode) { + return p.federationProcessor.GetNodeInfo(ctx) } func (p *processor) InboxPost(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) { diff --git a/internal/processing/federation/federation.go b/internal/processing/federation/federation.go index c79baec3c..311ca4909 100644 --- a/internal/processing/federation/federation.go +++ b/internal/processing/federation/federation.go @@ -60,10 +60,10 @@ type Processor interface { GetEmoji(ctx context.Context, requestedEmojiID string, requestURL *url.URL) (interface{}, gtserror.WithCode) // GetNodeInfoRel returns a well known response giving the path to node info. - GetNodeInfoRel(ctx context.Context, request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode) + GetNodeInfoRel(ctx context.Context) (*apimodel.WellKnownResponse, gtserror.WithCode) // GetNodeInfo returns a node info struct in response to a node info request. - GetNodeInfo(ctx context.Context, request *http.Request) (*apimodel.Nodeinfo, gtserror.WithCode) + GetNodeInfo(ctx context.Context) (*apimodel.Nodeinfo, gtserror.WithCode) // GetOutbox returns the activitypub representation of a local user's outbox. // This contains links to PUBLIC posts made by this user. diff --git a/internal/processing/federation/getnodeinfo.go b/internal/processing/federation/getnodeinfo.go index c4aebe57d..770c411ab 100644 --- a/internal/processing/federation/getnodeinfo.go +++ b/internal/processing/federation/getnodeinfo.go @@ -21,7 +21,6 @@ package federation import ( "context" "fmt" - "net/http" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" @@ -38,7 +37,7 @@ var ( nodeInfoProtocols = []string{"activitypub"} ) -func (p *processor) GetNodeInfoRel(ctx context.Context, request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode) { +func (p *processor) GetNodeInfoRel(ctx context.Context) (*apimodel.WellKnownResponse, gtserror.WithCode) { protocol := config.GetProtocol() host := config.GetHost() @@ -52,7 +51,7 @@ func (p *processor) GetNodeInfoRel(ctx context.Context, request *http.Request) ( }, nil } -func (p *processor) GetNodeInfo(ctx context.Context, request *http.Request) (*apimodel.Nodeinfo, gtserror.WithCode) { +func (p *processor) GetNodeInfo(ctx context.Context) (*apimodel.Nodeinfo, gtserror.WithCode) { openRegistration := config.GetAccountsRegistrationOpen() softwareVersion := config.GetSoftwareVersion() diff --git a/internal/processing/fromclientapi_test.go b/internal/processing/fromclientapi_test.go index c4e06ea62..5f0670779 100644 --- a/internal/processing/fromclientapi_test.go +++ b/internal/processing/fromclientapi_test.go @@ -25,7 +25,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/ap" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/messages" @@ -102,7 +102,7 @@ func (suite *FromClientAPITestSuite) TestProcessStreamNewStatus() { suite.Equal(stream.EventTypeUpdate, msg.Event) suite.NotEmpty(msg.Payload) suite.EqualValues([]string{stream.TimelineHome}, msg.Stream) - statusStreamed := &model.Status{} + statusStreamed := &apimodel.Status{} err = json.Unmarshal([]byte(msg.Payload), statusStreamed) suite.NoError(err) suite.Equal("01FN4B2F88TF9676DYNXWE1WSS", statusStreamed.ID) diff --git a/internal/processing/fromfederator_test.go b/internal/processing/fromfederator_test.go index 75cef43f5..a95c150fa 100644 --- a/internal/processing/fromfederator_test.go +++ b/internal/processing/fromfederator_test.go @@ -27,7 +27,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/ap" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" @@ -171,7 +171,7 @@ func (suite *FromFederatorTestSuite) TestProcessReplyMention() { suite.Equal(stream.EventTypeNotification, msg.Event) suite.NotEmpty(msg.Payload) suite.EqualValues([]string{stream.TimelineHome}, msg.Stream) - notifStreamed := &model.Notification{} + notifStreamed := &apimodel.Notification{} err = json.Unmarshal([]byte(msg.Payload), notifStreamed) suite.NoError(err) suite.Equal("mention", notifStreamed.Type) @@ -439,7 +439,7 @@ func (suite *FromFederatorTestSuite) TestProcessFollowRequestLocked() { suite.Equal(stream.EventTypeNotification, msg.Event) suite.NotEmpty(msg.Payload) suite.EqualValues([]string{stream.TimelineHome}, msg.Stream) - notif := &model.Notification{} + notif := &apimodel.Notification{} err = json.Unmarshal([]byte(msg.Payload), notif) suite.NoError(err) suite.Equal("follow_request", notif.Type) @@ -537,7 +537,7 @@ func (suite *FromFederatorTestSuite) TestProcessFollowRequestUnlocked() { suite.Equal(stream.EventTypeNotification, msg.Event) suite.NotEmpty(msg.Payload) suite.EqualValues([]string{stream.TimelineHome}, msg.Stream) - notif := &model.Notification{} + notif := &apimodel.Notification{} err = json.Unmarshal([]byte(msg.Payload), notif) suite.NoError(err) suite.Equal("follow", notif.Type) diff --git a/internal/processing/oauth.go b/internal/processing/oauth.go index 0a36bc336..285cb4d6a 100644 --- a/internal/processing/oauth.go +++ b/internal/processing/oauth.go @@ -22,6 +22,7 @@ import ( "net/http" "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/oauth2/v4" ) func (p *processor) OAuthHandleAuthorizeRequest(w http.ResponseWriter, r *http.Request) gtserror.WithCode { @@ -33,3 +34,8 @@ func (p *processor) OAuthHandleTokenRequest(r *http.Request) (map[string]interfa // todo: some kind of metrics stuff here return p.oauthServer.HandleTokenRequest(r) } + +func (p *processor) OAuthValidateBearerToken(r *http.Request) (oauth2.TokenInfo, error) { + // todo: some kind of metrics stuff here + return p.oauthServer.ValidationBearerToken(r) +} diff --git a/internal/processing/processor.go b/internal/processing/processor.go index 3067c56b7..6d664f986 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -46,6 +46,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/timeline" "github.com/superseriousbusiness/gotosocial/internal/typeutils" "github.com/superseriousbusiness/gotosocial/internal/visibility" + "github.com/superseriousbusiness/oauth2/v4" ) // Processor should be passed to api modules (see internal/apimodule/...). It is used for @@ -183,6 +184,7 @@ type Processor interface { OAuthHandleTokenRequest(r *http.Request) (map[string]interface{}, gtserror.WithCode) OAuthHandleAuthorizeRequest(w http.ResponseWriter, r *http.Request) gtserror.WithCode + OAuthValidateBearerToken(r *http.Request) (oauth2.TokenInfo, error) // SearchGet performs a search with the given params, resolving/dereferencing remotely as desired SearchGet(ctx context.Context, authed *oauth.Auth, searchQuery *apimodel.SearchQuery) (*apimodel.SearchResult, gtserror.WithCode) @@ -260,9 +262,9 @@ type Processor interface { // GetWebfingerAccount handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups. GetWebfingerAccount(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode) // GetNodeInfoRel returns a well known response giving the path to node info. - GetNodeInfoRel(ctx context.Context, request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode) + GetNodeInfoRel(ctx context.Context) (*apimodel.WellKnownResponse, gtserror.WithCode) // GetNodeInfo returns a node info struct in response to a node info request. - GetNodeInfo(ctx context.Context, request *http.Request) (*apimodel.Nodeinfo, gtserror.WithCode) + GetNodeInfo(ctx context.Context) (*apimodel.Nodeinfo, gtserror.WithCode) // InboxPost handles POST requests to a user's inbox for new activitypub messages. // // InboxPost returns true if the request was handled as an ActivityPub POST to an actor's inbox. diff --git a/internal/processing/status/create_test.go b/internal/processing/status/create_test.go index 1573dd9ae..b1f7d6a3b 100644 --- a/internal/processing/status/create_test.go +++ b/internal/processing/status/create_test.go @@ -23,7 +23,7 @@ import ( "testing" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" @@ -39,20 +39,20 @@ func (suite *StatusCreateTestSuite) TestProcessContentWarningWithQuotationMarks( creatingAccount := suite.testAccounts["local_account_1"] creatingApplication := suite.testApplications["application_1"] - statusCreateForm := &model.AdvancedStatusCreateForm{ - StatusCreateRequest: model.StatusCreateRequest{ + statusCreateForm := &apimodel.AdvancedStatusCreateForm{ + StatusCreateRequest: apimodel.StatusCreateRequest{ Status: "poopoo peepee", MediaIDs: []string{}, Poll: nil, InReplyToID: "", Sensitive: false, SpoilerText: "\"test\"", // these should not be html-escaped when the final text is rendered - Visibility: model.VisibilityPublic, + Visibility: apimodel.VisibilityPublic, ScheduledAt: "", Language: "en", - Format: model.StatusFormatPlain, + Format: apimodel.StatusFormatPlain, }, - AdvancedVisibilityFlagsForm: model.AdvancedVisibilityFlagsForm{ + AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ Federated: nil, Boostable: nil, Replyable: nil, @@ -73,20 +73,20 @@ func (suite *StatusCreateTestSuite) TestProcessContentWarningWithHTMLEscapedQuot creatingAccount := suite.testAccounts["local_account_1"] creatingApplication := suite.testApplications["application_1"] - statusCreateForm := &model.AdvancedStatusCreateForm{ - StatusCreateRequest: model.StatusCreateRequest{ + statusCreateForm := &apimodel.AdvancedStatusCreateForm{ + StatusCreateRequest: apimodel.StatusCreateRequest{ Status: "poopoo peepee", MediaIDs: []string{}, Poll: nil, InReplyToID: "", Sensitive: false, SpoilerText: ""test"", // the html-escaped quotation marks should appear as normal quotation marks in the finished text - Visibility: model.VisibilityPublic, + Visibility: apimodel.VisibilityPublic, ScheduledAt: "", Language: "en", - Format: model.StatusFormatPlain, + Format: apimodel.StatusFormatPlain, }, - AdvancedVisibilityFlagsForm: model.AdvancedVisibilityFlagsForm{ + AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ Federated: nil, Boostable: nil, Replyable: nil, @@ -112,19 +112,19 @@ func (suite *StatusCreateTestSuite) TestProcessStatusMarkdownWithUnderscoreEmoji creatingAccount := suite.testAccounts["local_account_1"] creatingApplication := suite.testApplications["application_1"] - statusCreateForm := &model.AdvancedStatusCreateForm{ - StatusCreateRequest: model.StatusCreateRequest{ + statusCreateForm := &apimodel.AdvancedStatusCreateForm{ + StatusCreateRequest: apimodel.StatusCreateRequest{ Status: "poopoo peepee :_rainbow_:", MediaIDs: []string{}, Poll: nil, InReplyToID: "", Sensitive: false, - Visibility: model.VisibilityPublic, + Visibility: apimodel.VisibilityPublic, ScheduledAt: "", Language: "en", - Format: model.StatusFormatMarkdown, + Format: apimodel.StatusFormatMarkdown, }, - AdvancedVisibilityFlagsForm: model.AdvancedVisibilityFlagsForm{ + AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ Federated: nil, Boostable: nil, Replyable: nil, @@ -145,20 +145,20 @@ func (suite *StatusCreateTestSuite) TestProcessStatusMarkdownWithSpoilerTextEmoj creatingAccount := suite.testAccounts["local_account_1"] creatingApplication := suite.testApplications["application_1"] - statusCreateForm := &model.AdvancedStatusCreateForm{ - StatusCreateRequest: model.StatusCreateRequest{ + statusCreateForm := &apimodel.AdvancedStatusCreateForm{ + StatusCreateRequest: apimodel.StatusCreateRequest{ Status: "poopoo peepee", SpoilerText: "testing something :rainbow:", MediaIDs: []string{}, Poll: nil, InReplyToID: "", Sensitive: false, - Visibility: model.VisibilityPublic, + Visibility: apimodel.VisibilityPublic, ScheduledAt: "", Language: "en", - Format: model.StatusFormatMarkdown, + Format: apimodel.StatusFormatMarkdown, }, - AdvancedVisibilityFlagsForm: model.AdvancedVisibilityFlagsForm{ + AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ Federated: nil, Boostable: nil, Replyable: nil, @@ -183,20 +183,20 @@ func (suite *StatusCreateTestSuite) TestProcessMediaDescriptionTooShort() { creatingAccount := suite.testAccounts["local_account_1"] creatingApplication := suite.testApplications["application_1"] - statusCreateForm := &model.AdvancedStatusCreateForm{ - StatusCreateRequest: model.StatusCreateRequest{ + statusCreateForm := &apimodel.AdvancedStatusCreateForm{ + StatusCreateRequest: apimodel.StatusCreateRequest{ Status: "poopoo peepee", MediaIDs: []string{suite.testAttachments["local_account_1_unattached_1"].ID}, Poll: nil, InReplyToID: "", Sensitive: false, SpoilerText: "", - Visibility: model.VisibilityPublic, + Visibility: apimodel.VisibilityPublic, ScheduledAt: "", Language: "en", - Format: model.StatusFormatPlain, + Format: apimodel.StatusFormatPlain, }, - AdvancedVisibilityFlagsForm: model.AdvancedVisibilityFlagsForm{ + AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ Federated: nil, Boostable: nil, Replyable: nil, diff --git a/internal/processing/status/util.go b/internal/processing/status/util.go index ca6e5063b..9a56f37b2 100644 --- a/internal/processing/status/util.go +++ b/internal/processing/status/util.go @@ -23,7 +23,6 @@ import ( "errors" "fmt" - "github.com/superseriousbusiness/gotosocial/internal/api/model" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" @@ -303,11 +302,11 @@ func (p *processor) ProcessContent(ctx context.Context, form *apimodel.AdvancedS switch acct.StatusFormat { case "plain": - form.Format = model.StatusFormatPlain + form.Format = apimodel.StatusFormatPlain case "markdown": - form.Format = model.StatusFormatMarkdown + form.Format = apimodel.StatusFormatMarkdown default: - form.Format = model.StatusFormatDefault + form.Format = apimodel.StatusFormatDefault } } diff --git a/internal/processing/status/util_test.go b/internal/processing/status/util_test.go index 6f551d63d..c260aaea6 100644 --- a/internal/processing/status/util_test.go +++ b/internal/processing/status/util_test.go @@ -24,7 +24,7 @@ import ( "testing" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) @@ -45,20 +45,20 @@ func (suite *UtilTestSuite) TestProcessMentions1() { creatingAccount := suite.testAccounts["local_account_1"] mentionedAccount := suite.testAccounts["remote_account_1"] - form := &model.AdvancedStatusCreateForm{ - StatusCreateRequest: model.StatusCreateRequest{ + form := &apimodel.AdvancedStatusCreateForm{ + StatusCreateRequest: apimodel.StatusCreateRequest{ Status: statusText1, MediaIDs: []string{}, Poll: nil, InReplyToID: "", Sensitive: false, SpoilerText: "", - Visibility: model.VisibilityPublic, + Visibility: apimodel.VisibilityPublic, ScheduledAt: "", Language: "en", - Format: model.StatusFormatPlain, + Format: apimodel.StatusFormatPlain, }, - AdvancedVisibilityFlagsForm: model.AdvancedVisibilityFlagsForm{ + AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ Federated: nil, Boostable: nil, Replyable: nil, @@ -94,20 +94,20 @@ func (suite *UtilTestSuite) TestProcessContentFull1() { */ // we need to partially process the status first since processContent expects a status with some stuff already set on it creatingAccount := suite.testAccounts["local_account_1"] - form := &model.AdvancedStatusCreateForm{ - StatusCreateRequest: model.StatusCreateRequest{ + form := &apimodel.AdvancedStatusCreateForm{ + StatusCreateRequest: apimodel.StatusCreateRequest{ Status: statusText1, MediaIDs: []string{}, Poll: nil, InReplyToID: "", Sensitive: false, SpoilerText: "", - Visibility: model.VisibilityPublic, + Visibility: apimodel.VisibilityPublic, ScheduledAt: "", Language: "en", - Format: model.StatusFormatPlain, + Format: apimodel.StatusFormatPlain, }, - AdvancedVisibilityFlagsForm: model.AdvancedVisibilityFlagsForm{ + AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ Federated: nil, Boostable: nil, Replyable: nil, @@ -142,20 +142,20 @@ func (suite *UtilTestSuite) TestProcessContentPartial1() { */ // we need to partially process the status first since processContent expects a status with some stuff already set on it creatingAccount := suite.testAccounts["local_account_1"] - form := &model.AdvancedStatusCreateForm{ - StatusCreateRequest: model.StatusCreateRequest{ + form := &apimodel.AdvancedStatusCreateForm{ + StatusCreateRequest: apimodel.StatusCreateRequest{ Status: statusText1, MediaIDs: []string{}, Poll: nil, InReplyToID: "", Sensitive: false, SpoilerText: "", - Visibility: model.VisibilityPublic, + Visibility: apimodel.VisibilityPublic, ScheduledAt: "", Language: "en", - Format: model.StatusFormatPlain, + Format: apimodel.StatusFormatPlain, }, - AdvancedVisibilityFlagsForm: model.AdvancedVisibilityFlagsForm{ + AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ Federated: nil, Boostable: nil, Replyable: nil, @@ -184,20 +184,20 @@ func (suite *UtilTestSuite) TestProcessMentions2() { creatingAccount := suite.testAccounts["local_account_1"] mentionedAccount := suite.testAccounts["remote_account_1"] - form := &model.AdvancedStatusCreateForm{ - StatusCreateRequest: model.StatusCreateRequest{ + form := &apimodel.AdvancedStatusCreateForm{ + StatusCreateRequest: apimodel.StatusCreateRequest{ Status: statusText2, MediaIDs: []string{}, Poll: nil, InReplyToID: "", Sensitive: false, SpoilerText: "", - Visibility: model.VisibilityPublic, + Visibility: apimodel.VisibilityPublic, ScheduledAt: "", Language: "en", - Format: model.StatusFormatPlain, + Format: apimodel.StatusFormatPlain, }, - AdvancedVisibilityFlagsForm: model.AdvancedVisibilityFlagsForm{ + AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ Federated: nil, Boostable: nil, Replyable: nil, @@ -233,20 +233,20 @@ func (suite *UtilTestSuite) TestProcessContentFull2() { */ // we need to partially process the status first since processContent expects a status with some stuff already set on it creatingAccount := suite.testAccounts["local_account_1"] - form := &model.AdvancedStatusCreateForm{ - StatusCreateRequest: model.StatusCreateRequest{ + form := &apimodel.AdvancedStatusCreateForm{ + StatusCreateRequest: apimodel.StatusCreateRequest{ Status: statusText2, MediaIDs: []string{}, Poll: nil, InReplyToID: "", Sensitive: false, SpoilerText: "", - Visibility: model.VisibilityPublic, + Visibility: apimodel.VisibilityPublic, ScheduledAt: "", Language: "en", - Format: model.StatusFormatPlain, + Format: apimodel.StatusFormatPlain, }, - AdvancedVisibilityFlagsForm: model.AdvancedVisibilityFlagsForm{ + AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ Federated: nil, Boostable: nil, Replyable: nil, @@ -282,20 +282,20 @@ func (suite *UtilTestSuite) TestProcessContentPartial2() { */ // we need to partially process the status first since processContent expects a status with some stuff already set on it creatingAccount := suite.testAccounts["local_account_1"] - form := &model.AdvancedStatusCreateForm{ - StatusCreateRequest: model.StatusCreateRequest{ + form := &apimodel.AdvancedStatusCreateForm{ + StatusCreateRequest: apimodel.StatusCreateRequest{ Status: statusText2, MediaIDs: []string{}, Poll: nil, InReplyToID: "", Sensitive: false, SpoilerText: "", - Visibility: model.VisibilityPublic, + Visibility: apimodel.VisibilityPublic, ScheduledAt: "", Language: "en", - Format: model.StatusFormatPlain, + Format: apimodel.StatusFormatPlain, }, - AdvancedVisibilityFlagsForm: model.AdvancedVisibilityFlagsForm{ + AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ Federated: nil, Boostable: nil, Replyable: nil, diff --git a/internal/router/attach.go b/internal/router/attach.go index 7c20b33d8..c65d88717 100644 --- a/internal/router/attach.go +++ b/internal/router/attach.go @@ -20,29 +20,18 @@ package router import "github.com/gin-gonic/gin" -// AttachHandler attaches the given gin.HandlerFunc to the router with the specified method and path. -// If the path is set to ANY, then the handlerfunc will be used for ALL methods at its given path. -func (r *router) AttachHandler(method string, path string, handler gin.HandlerFunc) { - if method == "ANY" { - r.engine.Any(path, handler) - } else { - r.engine.Handle(method, path, handler) - } +func (r *router) AttachGlobalMiddleware(handlers ...gin.HandlerFunc) gin.IRoutes { + return r.engine.Use(handlers...) } -// AttachMiddleware attaches a gin middleware to the router that will be used globally -func (r *router) AttachMiddleware(middleware gin.HandlerFunc) { - r.engine.Use(middleware) -} - -// AttachNoRouteHandler attaches a gin.HandlerFunc to NoRoute to handle 404's func (r *router) AttachNoRouteHandler(handler gin.HandlerFunc) { r.engine.NoRoute(handler) } -// AttachGroup attaches the given handlers into a group with the given relativePath as -// base path for that group. It then returns the *gin.RouterGroup so that the caller -// can add any extra middlewares etc specific to that group, as desired. func (r *router) AttachGroup(relativePath string, handlers ...gin.HandlerFunc) *gin.RouterGroup { return r.engine.Group(relativePath, handlers...) } + +func (r *router) AttachHandler(method string, path string, handler gin.HandlerFunc) { + r.engine.Handle(method, path, handler) +} diff --git a/internal/router/cors.go b/internal/router/cors.go deleted file mode 100644 index c8ef040d8..000000000 --- a/internal/router/cors.go +++ /dev/null @@ -1,87 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org - - 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 router - -import ( - "time" - - "github.com/gin-contrib/cors" - "github.com/gin-gonic/gin" -) - -var corsConfig = cors.Config{ - // TODO: make this customizable so instance admins can specify an origin for CORS requests - AllowAllOrigins: true, - - // adds the following: - // "chrome-extension://" - // "safari-extension://" - // "moz-extension://" - // "ms-browser-extension://" - AllowBrowserExtensions: true, - AllowMethods: []string{ - "POST", - "PUT", - "DELETE", - "GET", - "PATCH", - "OPTIONS", - }, - AllowHeaders: []string{ - // basic cors stuff - "Origin", - "Content-Length", - "Content-Type", - - // needed to pass oauth bearer tokens - "Authorization", - - // needed for websocket upgrade requests - "Upgrade", - "Sec-WebSocket-Extensions", - "Sec-WebSocket-Key", - "Sec-WebSocket-Protocol", - "Sec-WebSocket-Version", - "Connection", - }, - AllowWebSockets: true, - ExposeHeaders: []string{ - // needed for accessing next/prev links when making GET timeline requests - "Link", - - // needed so clients can handle rate limits - "X-RateLimit-Reset", - "X-RateLimit-Limit", - "X-RateLimit-Remaining", - "X-Request-Id", - - // websocket stuff - "Connection", - "Sec-WebSocket-Accept", - "Upgrade", - }, - MaxAge: 2 * time.Minute, -} - -// useCors attaches the corsConfig above to the given gin engine -func useCors(engine *gin.Engine) error { - c := cors.New(corsConfig) - engine.Use(c) - return nil -} diff --git a/internal/router/logger.go b/internal/router/logger.go deleted file mode 100644 index 6eb271a84..000000000 --- a/internal/router/logger.go +++ /dev/null @@ -1,96 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org - - 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 router - -import ( - "fmt" - "net/http" - "time" - - "codeberg.org/gruf/go-bytesize" - "codeberg.org/gruf/go-errors/v2" - "codeberg.org/gruf/go-kv" - "codeberg.org/gruf/go-logger/v2/level" - "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/log" -) - -// loggingMiddleware provides a request logging and panic recovery gin handler. -func loggingMiddleware(c *gin.Context) { - // Initialize the logging fields - fields := make(kv.Fields, 6, 7) - - // Determine pre-handler time - before := time.Now() - - defer func() { - code := c.Writer.Status() - path := c.Request.URL.Path - - if r := recover(); r != nil { - if c.Writer.Status() == 0 { - // No response was written, send a generic Internal Error - c.Writer.WriteHeader(http.StatusInternalServerError) - } - - // Append panic information to the request ctx - err := fmt.Errorf("recovered panic: %v", r) - _ = c.Error(err) - - // Dump a stacktrace to error log - callers := errors.GetCallers(3, 10) - log.WithField("stacktrace", callers).Error(err) - } - - // NOTE: - // It is very important here that we are ONLY logging - // the request path, and none of the query parameters. - // Query parameters can contain sensitive information - // and could lead to storing plaintext API keys in logs - - // Set request logging fields - fields[0] = kv.Field{"latency", time.Since(before)} - fields[1] = kv.Field{"clientIP", c.ClientIP()} - fields[2] = kv.Field{"userAgent", c.Request.UserAgent()} - fields[3] = kv.Field{"method", c.Request.Method} - fields[4] = kv.Field{"statusCode", code} - fields[5] = kv.Field{"path", path} - - // Create log entry with fields - l := log.WithFields(fields...) - - // Default is info - lvl := level.INFO - - if code >= 500 { - // This is a server error - lvl = level.ERROR - l = l.WithField("error", c.Errors) - } - - // Generate a nicer looking bytecount - size := bytesize.Size(c.Writer.Size()) - - // Finally, write log entry with status text body size - l.Logf(lvl, "%s: wrote %s", http.StatusText(code), size) - }() - - // Process request - c.Next() -} diff --git a/internal/router/router.go b/internal/router/router.go index d66e5f9cd..dd3ff61d7 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -25,34 +25,39 @@ import ( "net/http" "time" + "codeberg.org/gruf/go-bytesize" "codeberg.org/gruf/go-debug" "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/log" "golang.org/x/crypto/acme/autocert" ) const ( - readTimeout = 60 * time.Second - writeTimeout = 30 * time.Second - idleTimeout = 30 * time.Second - readHeaderTimeout = 30 * time.Second - shutdownTimeout = 30 * time.Second + readTimeout = 60 * time.Second + writeTimeout = 30 * time.Second + idleTimeout = 30 * time.Second + readHeaderTimeout = 30 * time.Second + shutdownTimeout = 30 * time.Second + maxMultipartMemory = int64(8 * bytesize.MiB) ) // Router provides the REST interface for gotosocial, using gin. type Router interface { - // Attach a gin handler to the router with the given method and path - AttachHandler(method string, path string, f gin.HandlerFunc) - // Attach a gin middleware to the router that will be used globally - AttachMiddleware(handler gin.HandlerFunc) + // Attach global gin middlewares to this router. + AttachGlobalMiddleware(handlers ...gin.HandlerFunc) gin.IRoutes + // AttachGroup attaches the given handlers into a group with the given relativePath as + // base path for that group. It then returns the *gin.RouterGroup so that the caller + // can add any extra middlewares etc specific to that group, as desired. + AttachGroup(relativePath string, handlers ...gin.HandlerFunc) *gin.RouterGroup + // Attach a single gin handler to the router with the given method and path. + // To make middleware management easier, AttachGroup should be preferred where possible. + // However, this function can be used for attaching single handlers that only require + // global middlewares. + AttachHandler(method string, path string, handler gin.HandlerFunc) + // Attach 404 NoRoute handler AttachNoRouteHandler(handler gin.HandlerFunc) - // Attach a router group, and receive that group back. - // More middlewares and handlers can then be attached on - // the group by the caller. - AttachGroup(path string, handlers ...gin.HandlerFunc) *gin.RouterGroup // Start the router Start() // Stop the router @@ -142,19 +147,20 @@ func (r *router) Stop(ctx context.Context) error { return nil } -// New returns a new Router with the specified configuration. +// New returns a new Router. // -// The given DB is only used in the New function for parsing config values, and is not otherwise -// pinned to the router. -func New(ctx context.Context, db db.DB) (Router, error) { - gin.SetMode(gin.ReleaseMode) +// The router's Attach functions should be used *before* the router is Started. +// +// When the router's work is finished, Stop should be called on it to close connections gracefully. +// +// The provided context will be used as the base context for all requests passing +// through the underlying http.Server, so this should be a long-running context. +func New(ctx context.Context) (Router, error) { + gin.SetMode(gin.TestMode) // create the actual engine here -- this is the core request routing handler for gts engine := gin.New() - engine.Use(loggingMiddleware) - - // 8 MiB - engine.MaxMultipartMemory = 8 << 20 + engine.MaxMultipartMemory = maxMultipartMemory // set up IP forwarding via x-forward-* headers. trustedProxies := config.GetTrustedProxies() @@ -162,21 +168,6 @@ func New(ctx context.Context, db db.DB) (Router, error) { return nil, err } - // enable cors on the engine - if err := useCors(engine); err != nil { - return nil, err - } - - // enable gzip compression on the engine - if err := useGzip(engine); err != nil { - return nil, err - } - - // enable session store middleware on the engine - if err := useSession(ctx, db, engine); err != nil { - return nil, err - } - // set template functions LoadTemplateFunctions(engine) diff --git a/internal/router/template.go b/internal/router/template.go index e87f6b69e..e2deb9ca8 100644 --- a/internal/router/template.go +++ b/internal/router/template.go @@ -26,7 +26,7 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/text" @@ -130,19 +130,19 @@ type iconWithLabel struct { label string } -func visibilityIcon(visibility model.Visibility) template.HTML { +func visibilityIcon(visibility apimodel.Visibility) template.HTML { var icon iconWithLabel switch visibility { - case model.VisibilityPublic: + case apimodel.VisibilityPublic: icon = iconWithLabel{"globe", "public"} - case model.VisibilityUnlisted: + case apimodel.VisibilityUnlisted: icon = iconWithLabel{"unlock", "unlisted"} - case model.VisibilityPrivate: + case apimodel.VisibilityPrivate: icon = iconWithLabel{"lock", "private"} - case model.VisibilityMutualsOnly: + case apimodel.VisibilityMutualsOnly: icon = iconWithLabel{"handshake-o", "mutuals only"} - case model.VisibilityDirect: + case apimodel.VisibilityDirect: icon = iconWithLabel{"envelope", "direct"} } @@ -151,7 +151,7 @@ func visibilityIcon(visibility model.Visibility) template.HTML { } // text is a template.HTML to affirm that the input of this function is already escaped -func emojify(emojis []model.Emoji, inputText template.HTML) template.HTML { +func emojify(emojis []apimodel.Emoji, inputText template.HTML) template.HTML { out := text.Emojify(emojis, string(inputText)) /* #nosec G203 */ diff --git a/internal/text/emojify.go b/internal/text/emojify.go index c9e25e5f9..673631be3 100644 --- a/internal/text/emojify.go +++ b/internal/text/emojify.go @@ -22,7 +22,7 @@ import ( "bytes" "html" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/regexes" ) @@ -30,8 +30,8 @@ import ( // // Callers should ensure that inputText and resulting text are escaped // appropriately depending on what they're used for. -func Emojify(emojis []model.Emoji, inputText string) string { - emojisMap := make(map[string]model.Emoji, len(emojis)) +func Emojify(emojis []apimodel.Emoji, inputText string) string { + emojisMap := make(map[string]apimodel.Emoji, len(emojis)) for _, emoji := range emojis { shortcode := ":" + emoji.Shortcode + ":" diff --git a/internal/transport/deliver.go b/internal/transport/deliver.go index 258fcb6c4..501e7cc92 100644 --- a/internal/transport/deliver.go +++ b/internal/transport/deliver.go @@ -27,7 +27,7 @@ import ( "strings" "sync" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" ) @@ -80,7 +80,7 @@ func (t *transport) Deliver(ctx context.Context, b []byte, to *url.URL) error { return err } - req.Header.Add("Content-Type", string(api.AppActivityLDJSON)) + req.Header.Add("Content-Type", string(apiutil.AppActivityLDJSON)) req.Header.Add("Accept-Charset", "utf-8") req.Header.Set("Host", to.Host) diff --git a/internal/transport/dereference.go b/internal/transport/dereference.go index 589bb2f0b..988605bb9 100644 --- a/internal/transport/dereference.go +++ b/internal/transport/dereference.go @@ -26,7 +26,7 @@ import ( "net/http" "net/url" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/uris" ) @@ -61,7 +61,7 @@ func (t *transport) Dereference(ctx context.Context, iri *url.URL) ([]byte, erro return nil, err } - req.Header.Add("Accept", string(api.AppActivityLDJSON)+","+string(api.AppActivityJSON)) + req.Header.Add("Accept", string(apiutil.AppActivityLDJSON)+","+string(apiutil.AppActivityJSON)) req.Header.Add("Accept-Charset", "utf-8") req.Header.Set("Host", iri.Host) diff --git a/internal/transport/derefinstance.go b/internal/transport/derefinstance.go index 5dd108f54..829bf6725 100644 --- a/internal/transport/derefinstance.go +++ b/internal/transport/derefinstance.go @@ -28,8 +28,8 @@ import ( "net/url" "strings" - "github.com/superseriousbusiness/gotosocial/internal/api" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/log" @@ -92,7 +92,7 @@ func dereferenceByAPIV1Instance(ctx context.Context, t *transport, iri *url.URL) return nil, err } - req.Header.Add("Accept", string(api.AppJSON)) + req.Header.Add("Accept", string(apiutil.AppJSON)) req.Header.Set("Host", cleanIRI.Host) resp, err := t.GET(req) @@ -242,7 +242,7 @@ func callNodeInfoWellKnown(ctx context.Context, t *transport, iri *url.URL) (*ur if err != nil { return nil, err } - req.Header.Add("Accept", string(api.AppJSON)) + req.Header.Add("Accept", string(apiutil.AppJSON)) req.Header.Set("Host", cleanIRI.Host) resp, err := t.GET(req) @@ -293,7 +293,7 @@ func callNodeInfo(ctx context.Context, t *transport, iri *url.URL) (*apimodel.No if err != nil { return nil, err } - req.Header.Add("Accept", string(api.AppJSON)) + req.Header.Add("Accept", string(apiutil.AppJSON)) req.Header.Set("Host", iri.Host) resp, err := t.GET(req) diff --git a/internal/transport/finger.go b/internal/transport/finger.go index 5523e479d..a69d31e42 100644 --- a/internal/transport/finger.go +++ b/internal/transport/finger.go @@ -24,7 +24,7 @@ import ( "io" "net/http" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" ) func (t *transport) Finger(ctx context.Context, targetUsername string, targetDomain string) ([]byte, error) { @@ -39,7 +39,7 @@ func (t *transport) Finger(ctx context.Context, targetUsername string, targetDom if err != nil { return nil, err } - req.Header.Add("Accept", string(api.AppJSON)) + req.Header.Add("Accept", string(apiutil.AppJSON)) req.Header.Add("Accept", "application/jrd+json") req.Header.Set("Host", req.URL.Host) diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go index d797c3e0c..7e2753dab 100644 --- a/internal/typeutils/converter.go +++ b/internal/typeutils/converter.go @@ -26,7 +26,7 @@ import ( "github.com/gorilla/feeds" "github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/ap" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) @@ -44,49 +44,49 @@ type TypeConverter interface { // AccountToAPIAccountSensitive takes a db model account as a param, and returns a populated apitype account, or an error // if something goes wrong. The returned account should be ready to serialize on an API level, and may have sensitive fields, // so serve it only to an authorized user who should have permission to see it. - AccountToAPIAccountSensitive(ctx context.Context, account *gtsmodel.Account) (*model.Account, error) + AccountToAPIAccountSensitive(ctx context.Context, account *gtsmodel.Account) (*apimodel.Account, error) // AccountToAPIAccountPublic takes a db model account as a param, and returns a populated apitype account, or an error // if something goes wrong. The returned account should be ready to serialize on an API level, and may NOT have sensitive fields. // In other words, this is the public record that the server has of an account. - AccountToAPIAccountPublic(ctx context.Context, account *gtsmodel.Account) (*model.Account, error) + AccountToAPIAccountPublic(ctx context.Context, account *gtsmodel.Account) (*apimodel.Account, error) // AccountToAPIAccountBlocked takes a db model account as a param, and returns a apitype account, or an error if // something goes wrong. The returned account will be a bare minimum representation of the account. This function should be used // when someone wants to view an account they've blocked. - AccountToAPIAccountBlocked(ctx context.Context, account *gtsmodel.Account) (*model.Account, error) + AccountToAPIAccountBlocked(ctx context.Context, account *gtsmodel.Account) (*apimodel.Account, error) // AppToAPIAppSensitive takes a db model application as a param, and returns a populated apitype application, or an error // if something goes wrong. The returned application should be ready to serialize on an API level, and may have sensitive fields // (such as client id and client secret), so serve it only to an authorized user who should have permission to see it. - AppToAPIAppSensitive(ctx context.Context, application *gtsmodel.Application) (*model.Application, error) + AppToAPIAppSensitive(ctx context.Context, application *gtsmodel.Application) (*apimodel.Application, error) // AppToAPIAppPublic takes a db model application as a param, and returns a populated apitype application, or an error // if something goes wrong. The returned application should be ready to serialize on an API level, and has sensitive // fields sanitized so that it can be served to non-authorized accounts without revealing any private information. - AppToAPIAppPublic(ctx context.Context, application *gtsmodel.Application) (*model.Application, error) + AppToAPIAppPublic(ctx context.Context, application *gtsmodel.Application) (*apimodel.Application, error) // AttachmentToAPIAttachment converts a gts model media attacahment into its api representation for serialization on the API. - AttachmentToAPIAttachment(ctx context.Context, attachment *gtsmodel.MediaAttachment) (model.Attachment, error) + AttachmentToAPIAttachment(ctx context.Context, attachment *gtsmodel.MediaAttachment) (apimodel.Attachment, error) // MentionToAPIMention converts a gts model mention into its api (frontend) representation for serialization on the API. - MentionToAPIMention(ctx context.Context, m *gtsmodel.Mention) (model.Mention, error) + MentionToAPIMention(ctx context.Context, m *gtsmodel.Mention) (apimodel.Mention, error) // EmojiToAPIEmoji converts a gts model emoji into its api (frontend) representation for serialization on the API. - EmojiToAPIEmoji(ctx context.Context, e *gtsmodel.Emoji) (model.Emoji, error) + EmojiToAPIEmoji(ctx context.Context, e *gtsmodel.Emoji) (apimodel.Emoji, error) // EmojiToAdminAPIEmoji converts a gts model emoji into an API representation with extra admin information. - EmojiToAdminAPIEmoji(ctx context.Context, e *gtsmodel.Emoji) (*model.AdminEmoji, error) + EmojiToAdminAPIEmoji(ctx context.Context, e *gtsmodel.Emoji) (*apimodel.AdminEmoji, error) // EmojiCategoryToAPIEmojiCategory converts a gts model emoji category into its api (frontend) representation. - EmojiCategoryToAPIEmojiCategory(ctx context.Context, category *gtsmodel.EmojiCategory) (*model.EmojiCategory, error) + EmojiCategoryToAPIEmojiCategory(ctx context.Context, category *gtsmodel.EmojiCategory) (*apimodel.EmojiCategory, error) // TagToAPITag converts a gts model tag into its api (frontend) representation for serialization on the API. - TagToAPITag(ctx context.Context, t *gtsmodel.Tag) (model.Tag, error) + TagToAPITag(ctx context.Context, t *gtsmodel.Tag) (apimodel.Tag, error) // StatusToAPIStatus converts a gts model status into its api (frontend) representation for serialization on the API. // // Requesting account can be nil. - StatusToAPIStatus(ctx context.Context, s *gtsmodel.Status, requestingAccount *gtsmodel.Account) (*model.Status, error) + StatusToAPIStatus(ctx context.Context, s *gtsmodel.Status, requestingAccount *gtsmodel.Account) (*apimodel.Status, error) // VisToAPIVis converts a gts visibility into its api equivalent - VisToAPIVis(ctx context.Context, m gtsmodel.Visibility) model.Visibility + VisToAPIVis(ctx context.Context, m gtsmodel.Visibility) apimodel.Visibility // InstanceToAPIInstance converts a gts instance into its api equivalent for serving at /api/v1/instance - InstanceToAPIInstance(ctx context.Context, i *gtsmodel.Instance) (*model.Instance, error) + InstanceToAPIInstance(ctx context.Context, i *gtsmodel.Instance) (*apimodel.Instance, error) // RelationshipToAPIRelationship converts a gts relationship into its api equivalent for serving in various places - RelationshipToAPIRelationship(ctx context.Context, r *gtsmodel.Relationship) (*model.Relationship, error) + RelationshipToAPIRelationship(ctx context.Context, r *gtsmodel.Relationship) (*apimodel.Relationship, error) // NotificationToAPINotification converts a gts notification into a api notification - NotificationToAPINotification(ctx context.Context, n *gtsmodel.Notification) (*model.Notification, error) + NotificationToAPINotification(ctx context.Context, n *gtsmodel.Notification) (*apimodel.Notification, error) // DomainBlockToAPIDomainBlock converts a gts model domin block into a api domain block, for serving at /api/v1/admin/domain_blocks - DomainBlockToAPIDomainBlock(ctx context.Context, b *gtsmodel.DomainBlock, export bool) (*model.DomainBlock, error) + DomainBlockToAPIDomainBlock(ctx context.Context, b *gtsmodel.DomainBlock, export bool) (*apimodel.DomainBlock, error) /* INTERNAL (gts) MODEL TO FRONTEND (rss) MODEL @@ -99,7 +99,7 @@ type TypeConverter interface { */ // APIVisToVis converts an API model visibility into its internal gts equivalent. - APIVisToVis(m model.Visibility) gtsmodel.Visibility + APIVisToVis(m apimodel.Visibility) gtsmodel.Visibility /* ACTIVITYSTREAMS MODEL TO INTERNAL (gts) MODEL diff --git a/internal/typeutils/frontendtointernal.go b/internal/typeutils/frontendtointernal.go index 716c41317..39d31d2dc 100644 --- a/internal/typeutils/frontendtointernal.go +++ b/internal/typeutils/frontendtointernal.go @@ -19,21 +19,21 @@ package typeutils import ( - "github.com/superseriousbusiness/gotosocial/internal/api/model" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) -func (c *converter) APIVisToVis(m model.Visibility) gtsmodel.Visibility { +func (c *converter) APIVisToVis(m apimodel.Visibility) gtsmodel.Visibility { switch m { - case model.VisibilityPublic: + case apimodel.VisibilityPublic: return gtsmodel.VisibilityPublic - case model.VisibilityUnlisted: + case apimodel.VisibilityUnlisted: return gtsmodel.VisibilityUnlocked - case model.VisibilityPrivate: + case apimodel.VisibilityPrivate: return gtsmodel.VisibilityFollowersOnly - case model.VisibilityMutualsOnly: + case apimodel.VisibilityMutualsOnly: return gtsmodel.VisibilityMutualsOnly - case model.VisibilityDirect: + case apimodel.VisibilityDirect: return gtsmodel.VisibilityDirect } return "" diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index d5b448e62..348d3c19a 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -26,7 +26,7 @@ import ( "strconv" "strings" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" @@ -45,7 +45,7 @@ const ( instancePollsMaxExpiration = 2629746 // seconds ) -func (c *converter) AccountToAPIAccountSensitive(ctx context.Context, a *gtsmodel.Account) (*model.Account, error) { +func (c *converter) AccountToAPIAccountSensitive(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) { // we can build this sensitive account easily by first getting the public account.... apiAccount, err := c.AccountToAPIAccountPublic(ctx, a) if err != nil { @@ -66,12 +66,12 @@ func (c *converter) AccountToAPIAccountSensitive(ctx context.Context, a *gtsmode frc = len(frs) } - statusFormat := string(model.StatusFormatDefault) + statusFormat := string(apimodel.StatusFormatDefault) if a.StatusFormat != "" { statusFormat = a.StatusFormat } - apiAccount.Source = &model.Source{ + apiAccount.Source = &apimodel.Source{ Privacy: c.VisToAPIVis(ctx, a.Privacy), Sensitive: *a.Sensitive, Language: a.Language, @@ -84,7 +84,7 @@ func (c *converter) AccountToAPIAccountSensitive(ctx context.Context, a *gtsmode return apiAccount, nil } -func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.Account) (*model.Account, error) { +func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) { // count followers followersCount, err := c.db.CountAccountFollowedBy(ctx, a.ID, false) if err != nil { @@ -146,11 +146,11 @@ func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A } // preallocate frontend fields slice - fields := make([]model.Field, len(a.Fields)) + fields := make([]apimodel.Field, len(a.Fields)) // Convert account GTS model fields to frontend for i, field := range a.Fields { - mField := model.Field{ + mField := apimodel.Field{ Name: field.Name, Value: field.Value, } @@ -168,7 +168,7 @@ func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A var ( acct string - role = model.AccountRoleUnknown + role = apimodel.AccountRoleUnknown ) if a.Domain != "" { @@ -184,11 +184,11 @@ func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A switch { case *user.Admin: - role = model.AccountRoleAdmin + role = apimodel.AccountRoleAdmin case *user.Moderator: - role = model.AccountRoleModerator + role = apimodel.AccountRoleModerator default: - role = model.AccountRoleUser + role = apimodel.AccountRoleUser } } @@ -197,7 +197,7 @@ func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A suspended = true } - accountFrontend := &model.Account{ + accountFrontend := &apimodel.Account{ ID: a.ID, Username: a.Username, Acct: acct, @@ -229,7 +229,7 @@ func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A return accountFrontend, nil } -func (c *converter) AccountToAPIAccountBlocked(ctx context.Context, a *gtsmodel.Account) (*model.Account, error) { +func (c *converter) AccountToAPIAccountBlocked(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) { var acct string if a.Domain != "" { // this is a remote user @@ -244,7 +244,7 @@ func (c *converter) AccountToAPIAccountBlocked(ctx context.Context, a *gtsmodel. suspended = true } - return &model.Account{ + return &apimodel.Account{ ID: a.ID, Username: a.Username, Acct: acct, @@ -256,8 +256,8 @@ func (c *converter) AccountToAPIAccountBlocked(ctx context.Context, a *gtsmodel. }, nil } -func (c *converter) AppToAPIAppSensitive(ctx context.Context, a *gtsmodel.Application) (*model.Application, error) { - return &model.Application{ +func (c *converter) AppToAPIAppSensitive(ctx context.Context, a *gtsmodel.Application) (*apimodel.Application, error) { + return &apimodel.Application{ ID: a.ID, Name: a.Name, Website: a.Website, @@ -267,33 +267,33 @@ func (c *converter) AppToAPIAppSensitive(ctx context.Context, a *gtsmodel.Applic }, nil } -func (c *converter) AppToAPIAppPublic(ctx context.Context, a *gtsmodel.Application) (*model.Application, error) { - return &model.Application{ +func (c *converter) AppToAPIAppPublic(ctx context.Context, a *gtsmodel.Application) (*apimodel.Application, error) { + return &apimodel.Application{ Name: a.Name, Website: a.Website, }, nil } -func (c *converter) AttachmentToAPIAttachment(ctx context.Context, a *gtsmodel.MediaAttachment) (model.Attachment, error) { - apiAttachment := model.Attachment{ +func (c *converter) AttachmentToAPIAttachment(ctx context.Context, a *gtsmodel.MediaAttachment) (apimodel.Attachment, error) { + apiAttachment := apimodel.Attachment{ ID: a.ID, Type: strings.ToLower(string(a.Type)), TextURL: a.URL, PreviewURL: a.Thumbnail.URL, - Meta: model.MediaMeta{ - Original: model.MediaDimensions{ + Meta: apimodel.MediaMeta{ + Original: apimodel.MediaDimensions{ Width: a.FileMeta.Original.Width, Height: a.FileMeta.Original.Height, Size: fmt.Sprintf("%dx%d", a.FileMeta.Original.Width, a.FileMeta.Original.Height), Aspect: float32(a.FileMeta.Original.Aspect), }, - Small: model.MediaDimensions{ + Small: apimodel.MediaDimensions{ Width: a.FileMeta.Small.Width, Height: a.FileMeta.Small.Height, Size: fmt.Sprintf("%dx%d", a.FileMeta.Small.Width, a.FileMeta.Small.Height), Aspect: float32(a.FileMeta.Small.Aspect), }, - Focus: model.MediaFocus{ + Focus: apimodel.MediaFocus{ X: a.FileMeta.Focus.X, Y: a.FileMeta.Focus.Y, }, @@ -337,11 +337,11 @@ func (c *converter) AttachmentToAPIAttachment(ctx context.Context, a *gtsmodel.M return apiAttachment, nil } -func (c *converter) MentionToAPIMention(ctx context.Context, m *gtsmodel.Mention) (model.Mention, error) { +func (c *converter) MentionToAPIMention(ctx context.Context, m *gtsmodel.Mention) (apimodel.Mention, error) { if m.TargetAccount == nil { targetAccount, err := c.db.GetAccountByID(ctx, m.TargetAccountID) if err != nil { - return model.Mention{}, err + return apimodel.Mention{}, err } m.TargetAccount = targetAccount } @@ -358,7 +358,7 @@ func (c *converter) MentionToAPIMention(ctx context.Context, m *gtsmodel.Mention acct = fmt.Sprintf("%s@%s", m.TargetAccount.Username, m.TargetAccount.Domain) } - return model.Mention{ + return apimodel.Mention{ ID: m.TargetAccount.ID, Username: m.TargetAccount.Username, URL: m.TargetAccount.URL, @@ -366,20 +366,20 @@ func (c *converter) MentionToAPIMention(ctx context.Context, m *gtsmodel.Mention }, nil } -func (c *converter) EmojiToAPIEmoji(ctx context.Context, e *gtsmodel.Emoji) (model.Emoji, error) { +func (c *converter) EmojiToAPIEmoji(ctx context.Context, e *gtsmodel.Emoji) (apimodel.Emoji, error) { var category string if e.CategoryID != "" { if e.Category == nil { var err error e.Category, err = c.db.GetEmojiCategory(ctx, e.CategoryID) if err != nil { - return model.Emoji{}, err + return apimodel.Emoji{}, err } } category = e.Category.Name } - return model.Emoji{ + return apimodel.Emoji{ Shortcode: e.Shortcode, URL: e.ImageURL, StaticURL: e.ImageStaticURL, @@ -388,13 +388,13 @@ func (c *converter) EmojiToAPIEmoji(ctx context.Context, e *gtsmodel.Emoji) (mod }, nil } -func (c *converter) EmojiToAdminAPIEmoji(ctx context.Context, e *gtsmodel.Emoji) (*model.AdminEmoji, error) { +func (c *converter) EmojiToAdminAPIEmoji(ctx context.Context, e *gtsmodel.Emoji) (*apimodel.AdminEmoji, error) { emoji, err := c.EmojiToAPIEmoji(ctx, e) if err != nil { return nil, err } - return &model.AdminEmoji{ + return &apimodel.AdminEmoji{ Emoji: emoji, ID: e.ID, Disabled: *e.Disabled, @@ -406,21 +406,21 @@ func (c *converter) EmojiToAdminAPIEmoji(ctx context.Context, e *gtsmodel.Emoji) }, nil } -func (c *converter) EmojiCategoryToAPIEmojiCategory(ctx context.Context, category *gtsmodel.EmojiCategory) (*model.EmojiCategory, error) { - return &model.EmojiCategory{ +func (c *converter) EmojiCategoryToAPIEmojiCategory(ctx context.Context, category *gtsmodel.EmojiCategory) (*apimodel.EmojiCategory, error) { + return &apimodel.EmojiCategory{ ID: category.ID, Name: category.Name, }, nil } -func (c *converter) TagToAPITag(ctx context.Context, t *gtsmodel.Tag) (model.Tag, error) { - return model.Tag{ +func (c *converter) TagToAPITag(ctx context.Context, t *gtsmodel.Tag) (apimodel.Tag, error) { + return apimodel.Tag{ Name: t.Name, URL: t.URL, }, nil } -func (c *converter) StatusToAPIStatus(ctx context.Context, s *gtsmodel.Status, requestingAccount *gtsmodel.Account) (*model.Status, error) { +func (c *converter) StatusToAPIStatus(ctx context.Context, s *gtsmodel.Status, requestingAccount *gtsmodel.Account) (*apimodel.Status, error) { repliesCount, err := c.db.CountStatusReplies(ctx, s) if err != nil { return nil, fmt.Errorf("error counting replies: %s", err) @@ -436,7 +436,7 @@ func (c *converter) StatusToAPIStatus(ctx context.Context, s *gtsmodel.Status, r return nil, fmt.Errorf("error counting faves: %s", err) } - var apiRebloggedStatus *model.Status + var apiRebloggedStatus *apimodel.Status if s.BoostOfID != "" { // the boosted status might have been set on this struct already so check first before doing db calls if s.BoostOf == nil { @@ -465,7 +465,7 @@ func (c *converter) StatusToAPIStatus(ctx context.Context, s *gtsmodel.Status, r } } - var apiApplication *model.Application + var apiApplication *apimodel.Application if s.CreatedWithApplicationID != "" { gtsApplication := >smodel.Application{} if err := c.db.GetByID(ctx, s.CreatedWithApplicationID, gtsApplication); err != nil { @@ -528,7 +528,7 @@ func (c *converter) StatusToAPIStatus(ctx context.Context, s *gtsmodel.Status, r language = &s.Language } - apiStatus := &model.Status{ + apiStatus := &apimodel.Status{ ID: s.ID, CreatedAt: util.FormatISO8601(s.CreatedAt), InReplyToID: nil, @@ -572,29 +572,29 @@ func (c *converter) StatusToAPIStatus(ctx context.Context, s *gtsmodel.Status, r } if apiRebloggedStatus != nil { - apiStatus.Reblog = &model.StatusReblogged{Status: apiRebloggedStatus} + apiStatus.Reblog = &apimodel.StatusReblogged{Status: apiRebloggedStatus} } return apiStatus, nil } // VisToapi converts a gts visibility into its api equivalent -func (c *converter) VisToAPIVis(ctx context.Context, m gtsmodel.Visibility) model.Visibility { +func (c *converter) VisToAPIVis(ctx context.Context, m gtsmodel.Visibility) apimodel.Visibility { switch m { case gtsmodel.VisibilityPublic: - return model.VisibilityPublic + return apimodel.VisibilityPublic case gtsmodel.VisibilityUnlocked: - return model.VisibilityUnlisted + return apimodel.VisibilityUnlisted case gtsmodel.VisibilityFollowersOnly, gtsmodel.VisibilityMutualsOnly: - return model.VisibilityPrivate + return apimodel.VisibilityPrivate case gtsmodel.VisibilityDirect: - return model.VisibilityDirect + return apimodel.VisibilityDirect } return "" } -func (c *converter) InstanceToAPIInstance(ctx context.Context, i *gtsmodel.Instance) (*model.Instance, error) { - mi := &model.Instance{ +func (c *converter) InstanceToAPIInstance(ctx context.Context, i *gtsmodel.Instance) (*apimodel.Instance, error) { + mi := &apimodel.Instance{ URI: i.URI, Title: i.Title, Description: i.Description, @@ -650,19 +650,19 @@ func (c *converter) InstanceToAPIInstance(ctx context.Context, i *gtsmodel.Insta mi.ApprovalRequired = config.GetAccountsApprovalRequired() mi.InvitesEnabled = false // TODO mi.MaxTootChars = uint(config.GetStatusesMaxChars()) - mi.URLS = &model.InstanceURLs{ + mi.URLS = &apimodel.InstanceURLs{ StreamingAPI: "wss://" + host, } mi.Version = config.GetSoftwareVersion() // todo: remove hardcoded values and put them in config somewhere - mi.Configuration = &model.InstanceConfiguration{ - Statuses: &model.InstanceConfigurationStatuses{ + mi.Configuration = &apimodel.InstanceConfiguration{ + Statuses: &apimodel.InstanceConfigurationStatuses{ MaxCharacters: config.GetStatusesMaxChars(), MaxMediaAttachments: config.GetStatusesMediaMaxFiles(), CharactersReservedPerURL: instanceStatusesCharactersReservedPerURL, }, - MediaAttachments: &model.InstanceConfigurationMediaAttachments{ + MediaAttachments: &apimodel.InstanceConfigurationMediaAttachments{ SupportedMimeTypes: media.AllSupportedMIMETypes(), ImageSizeLimit: int(config.GetMediaImageMaxSize()), // bytes ImageMatrixLimit: instanceMediaAttachmentsImageMatrixLimit, // height*width @@ -670,16 +670,16 @@ func (c *converter) InstanceToAPIInstance(ctx context.Context, i *gtsmodel.Insta VideoFrameRateLimit: instanceMediaAttachmentsVideoFrameRateLimit, VideoMatrixLimit: instanceMediaAttachmentsVideoMatrixLimit, // height*width }, - Polls: &model.InstanceConfigurationPolls{ + Polls: &apimodel.InstanceConfigurationPolls{ MaxOptions: config.GetStatusesPollMaxOptions(), MaxCharactersPerOption: config.GetStatusesPollOptionMaxChars(), MinExpiration: instancePollsMinExpiration, // seconds MaxExpiration: instancePollsMaxExpiration, // seconds }, - Accounts: &model.InstanceConfigurationAccounts{ + Accounts: &apimodel.InstanceConfigurationAccounts{ AllowCustomCSS: config.GetAccountsAllowCustomCSS(), }, - Emojis: &model.InstanceConfigurationEmojis{ + Emojis: &apimodel.InstanceConfigurationEmojis{ EmojiSizeLimit: int(config.GetMediaEmojiLocalMaxSize()), // bytes }, } @@ -702,8 +702,8 @@ func (c *converter) InstanceToAPIInstance(ctx context.Context, i *gtsmodel.Insta return mi, nil } -func (c *converter) RelationshipToAPIRelationship(ctx context.Context, r *gtsmodel.Relationship) (*model.Relationship, error) { - return &model.Relationship{ +func (c *converter) RelationshipToAPIRelationship(ctx context.Context, r *gtsmodel.Relationship) (*apimodel.Relationship, error) { + return &apimodel.Relationship{ ID: r.ID, Following: r.Following, ShowingReblogs: r.ShowingReblogs, @@ -720,7 +720,7 @@ func (c *converter) RelationshipToAPIRelationship(ctx context.Context, r *gtsmod }, nil } -func (c *converter) NotificationToAPINotification(ctx context.Context, n *gtsmodel.Notification) (*model.Notification, error) { +func (c *converter) NotificationToAPINotification(ctx context.Context, n *gtsmodel.Notification) (*apimodel.Notification, error) { if n.TargetAccount == nil { tAccount, err := c.db.GetAccountByID(ctx, n.TargetAccountID) if err != nil { @@ -742,7 +742,7 @@ func (c *converter) NotificationToAPINotification(ctx context.Context, n *gtsmod return nil, fmt.Errorf("NotificationToapi: error converting account to api: %s", err) } - var apiStatus *model.Status + var apiStatus *apimodel.Status if n.StatusID != "" { if n.Status == nil { status, err := c.db.GetStatusByID(ctx, n.StatusID) @@ -772,7 +772,7 @@ func (c *converter) NotificationToAPINotification(ctx context.Context, n *gtsmod apiStatus = apiStatus.Reblog.Status } - return &model.Notification{ + return &apimodel.Notification{ ID: n.ID, Type: string(n.NotificationType), CreatedAt: util.FormatISO8601(n.CreatedAt), @@ -781,9 +781,9 @@ func (c *converter) NotificationToAPINotification(ctx context.Context, n *gtsmod }, nil } -func (c *converter) DomainBlockToAPIDomainBlock(ctx context.Context, b *gtsmodel.DomainBlock, export bool) (*model.DomainBlock, error) { - domainBlock := &model.DomainBlock{ - Domain: model.Domain{ +func (c *converter) DomainBlockToAPIDomainBlock(ctx context.Context, b *gtsmodel.DomainBlock, export bool) (*apimodel.DomainBlock, error) { + domainBlock := &apimodel.DomainBlock{ + Domain: apimodel.Domain{ Domain: b.Domain, PublicComment: b.PublicComment, }, @@ -803,7 +803,7 @@ func (c *converter) DomainBlockToAPIDomainBlock(ctx context.Context, b *gtsmodel } // convertAttachmentsToAPIAttachments will convert a slice of GTS model attachments to frontend API model attachments, falling back to IDs if no GTS models supplied. -func (c *converter) convertAttachmentsToAPIAttachments(ctx context.Context, attachments []*gtsmodel.MediaAttachment, attachmentIDs []string) ([]model.Attachment, error) { +func (c *converter) convertAttachmentsToAPIAttachments(ctx context.Context, attachments []*gtsmodel.MediaAttachment, attachmentIDs []string) ([]apimodel.Attachment, error) { var errs gtserror.MultiError if len(attachments) == 0 { @@ -824,7 +824,7 @@ func (c *converter) convertAttachmentsToAPIAttachments(ctx context.Context, atta } // Preallocate expected frontend slice - apiAttachments := make([]model.Attachment, 0, len(attachments)) + apiAttachments := make([]apimodel.Attachment, 0, len(attachments)) // Convert GTS models to frontend models for _, attachment := range attachments { @@ -840,7 +840,7 @@ func (c *converter) convertAttachmentsToAPIAttachments(ctx context.Context, atta } // convertEmojisToAPIEmojis will convert a slice of GTS model emojis to frontend API model emojis, falling back to IDs if no GTS models supplied. -func (c *converter) convertEmojisToAPIEmojis(ctx context.Context, emojis []*gtsmodel.Emoji, emojiIDs []string) ([]model.Emoji, error) { +func (c *converter) convertEmojisToAPIEmojis(ctx context.Context, emojis []*gtsmodel.Emoji, emojiIDs []string) ([]apimodel.Emoji, error) { var errs gtserror.MultiError if len(emojis) == 0 { @@ -861,7 +861,7 @@ func (c *converter) convertEmojisToAPIEmojis(ctx context.Context, emojis []*gtsm } // Preallocate expected frontend slice - apiEmojis := make([]model.Emoji, 0, len(emojis)) + apiEmojis := make([]apimodel.Emoji, 0, len(emojis)) // Convert GTS models to frontend models for _, emoji := range emojis { @@ -877,7 +877,7 @@ func (c *converter) convertEmojisToAPIEmojis(ctx context.Context, emojis []*gtsm } // convertMentionsToAPIMentions will convert a slice of GTS model mentions to frontend API model mentions, falling back to IDs if no GTS models supplied. -func (c *converter) convertMentionsToAPIMentions(ctx context.Context, mentions []*gtsmodel.Mention, mentionIDs []string) ([]model.Mention, error) { +func (c *converter) convertMentionsToAPIMentions(ctx context.Context, mentions []*gtsmodel.Mention, mentionIDs []string) ([]apimodel.Mention, error) { var errs gtserror.MultiError if len(mentions) == 0 { @@ -893,7 +893,7 @@ func (c *converter) convertMentionsToAPIMentions(ctx context.Context, mentions [ } // Preallocate expected frontend slice - apiMentions := make([]model.Mention, 0, len(mentions)) + apiMentions := make([]apimodel.Mention, 0, len(mentions)) // Convert GTS models to frontend models for _, mention := range mentions { @@ -909,7 +909,7 @@ func (c *converter) convertMentionsToAPIMentions(ctx context.Context, mentions [ } // convertTagsToAPITags will convert a slice of GTS model tags to frontend API model tags, falling back to IDs if no GTS models supplied. -func (c *converter) convertTagsToAPITags(ctx context.Context, tags []*gtsmodel.Tag, tagIDs []string) ([]model.Tag, error) { +func (c *converter) convertTagsToAPITags(ctx context.Context, tags []*gtsmodel.Tag, tagIDs []string) ([]apimodel.Tag, error) { var errs gtserror.MultiError if len(tags) == 0 { @@ -930,7 +930,7 @@ func (c *converter) convertTagsToAPITags(ctx context.Context, tags []*gtsmodel.T } // Preallocate expected frontend slice - apiTags := make([]model.Tag, 0, len(tags)) + apiTags := make([]apimodel.Tag, 0, len(tags)) // Convert GTS models to frontend models for _, tag := range tags { diff --git a/internal/typeutils/internaltorss.go b/internal/typeutils/internaltorss.go index 609725d73..7557ca9ae 100644 --- a/internal/typeutils/internaltorss.go +++ b/internal/typeutils/internaltorss.go @@ -25,7 +25,7 @@ import ( "strings" "github.com/gorilla/feeds" - "github.com/superseriousbusiness/gotosocial/internal/api/model" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" @@ -123,7 +123,7 @@ func (c *converter) StatusToRSSItem(ctx context.Context, s *gtsmodel.Status) (*f } // Content - apiEmojis := []model.Emoji{} + apiEmojis := []apimodel.Emoji{} // the status might already have some gts emojis on it if it's not been pulled directly from the database // if so, we can directly convert the gts emojis into api ones if s.Emojis != nil { diff --git a/internal/web/assets.go b/internal/web/assets.go index aab4346eb..470bab752 100644 --- a/internal/web/assets.go +++ b/internal/web/assets.go @@ -22,11 +22,9 @@ import ( "fmt" "net/http" "path" - "path/filepath" "strings" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/log" ) @@ -53,22 +51,6 @@ func (fs fileSystem) Open(path string) (http.File, error) { return f, nil } -func (m *Module) mountAssetsFilesystem(group *gin.RouterGroup) { - webAssetsAbsFilePath, err := filepath.Abs(config.GetWebAssetBaseDir()) - if err != nil { - log.Panicf("mountAssetsFilesystem: error getting absolute path of assets dir: %s", err) - } - - fs := fileSystem{http.Dir(webAssetsAbsFilePath)} - - // use the cache middleware on all handlers in this group - group.Use(m.assetsCacheControlMiddleware(fs)) - - // serve static file system in the root of this group, - // will end up being something like "/assets/" - group.StaticFS("/", fs) -} - // getAssetFileInfo tries to fetch the ETag for the given filePath from the module's // assetsETagCache. If it can't be found there, it uses the provided http.FileSystem // to generate a new ETag to go in the cache, which it then returns. @@ -115,6 +97,9 @@ func (m *Module) getAssetETag(filePath string, fs http.FileSystem) (string, erro // // See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match // and: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control +// +// todo: move this middleware out of the 'web' package and into the 'middleware' +// package along with the other middlewares func (m *Module) assetsCacheControlMiddleware(fs http.FileSystem) gin.HandlerFunc { return func(c *gin.Context) { // set this Cache-Control header to instruct clients to validate the response with us diff --git a/internal/web/base.go b/internal/web/base.go index 51238afaf..c2fcdbe39 100644 --- a/internal/web/base.go +++ b/internal/web/base.go @@ -23,7 +23,7 @@ import ( "strings" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) @@ -39,7 +39,7 @@ func (m *Module) baseHandler(c *gin.Context) { host := config.GetHost() instance, err := m.processor.InstanceGet(c.Request.Context(), host) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } diff --git a/internal/web/confirmemail.go b/internal/web/confirmemail.go index 58f932bde..360e99e83 100644 --- a/internal/web/confirmemail.go +++ b/internal/web/confirmemail.go @@ -23,7 +23,7 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) @@ -34,20 +34,20 @@ func (m *Module) confirmEmailGETHandler(c *gin.Context) { // if there's no token in the query, just serve the 404 web handler token := c.Query(tokenParam) if token == "" { - api.ErrorHandler(c, gtserror.NewErrorNotFound(errors.New(http.StatusText(http.StatusNotFound))), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorNotFound(errors.New(http.StatusText(http.StatusNotFound))), m.processor.InstanceGet) return } user, errWithCode := m.processor.UserConfirmEmail(ctx, token) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } host := config.GetHost() instance, err := m.processor.InstanceGet(ctx, host) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } diff --git a/internal/web/customcss.go b/internal/web/customcss.go index 48f8c0f71..b12aee442 100644 --- a/internal/web/customcss.go +++ b/internal/web/customcss.go @@ -24,22 +24,22 @@ import ( "strings" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) -const textCSSUTF8 = string(api.TextCSS + "; charset=utf-8") +const textCSSUTF8 = string(apiutil.TextCSS + "; charset=utf-8") func (m *Module) customCSSGETHandler(c *gin.Context) { if !config.GetAccountsAllowCustomCSS() { err := errors.New("accounts-allow-custom-css is not enabled on this instance") - api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet) return } - if _, err := api.NegotiateAccept(c, api.TextCSS); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.TextCSS); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } @@ -47,13 +47,13 @@ func (m *Module) customCSSGETHandler(c *gin.Context) { username := strings.ToLower(c.Param(usernameKey)) if username == "" { err := errors.New("no account username specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } customCSS, errWithCode := m.processor.AccountGetCustomCSSForUsername(c.Request.Context(), username) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/web/profile.go b/internal/web/profile.go index 8a0368a3c..c562a6cff 100644 --- a/internal/web/profile.go +++ b/internal/web/profile.go @@ -28,8 +28,8 @@ import ( "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/ap" - "github.com/superseriousbusiness/gotosocial/internal/api" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -45,21 +45,21 @@ func (m *Module) profileGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, false, false, false, false) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } username := strings.ToLower(c.Param(usernameKey)) if username == "" { err := errors.New("no account username specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } host := config.GetHost() instance, err := m.processor.InstanceGet(ctx, host) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } @@ -69,14 +69,14 @@ func (m *Module) profileGETHandler(c *gin.Context) { account, errWithCode := m.processor.AccountGetLocalByUsername(ctx, authed, username) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, instanceGet) + apiutil.ErrorHandler(c, errWithCode, instanceGet) return } // if we're getting an AP request on this endpoint we // should render the account's AP representation instead - accept := c.NegotiateFormat(string(api.TextHTML), string(api.AppActivityJSON), string(api.AppActivityLDJSON)) - if accept == string(api.AppActivityJSON) || accept == string(api.AppActivityLDJSON) { + accept := c.NegotiateFormat(string(apiutil.TextHTML), string(apiutil.AppActivityJSON), string(apiutil.AppActivityLDJSON)) + if accept == string(apiutil.AppActivityJSON) || accept == string(apiutil.AppActivityLDJSON) { m.returnAPProfile(ctx, c, username, accept) return } @@ -89,7 +89,7 @@ func (m *Module) profileGETHandler(c *gin.Context) { // only allow search engines / robots to view this page if account is discoverable var robotsMeta string if account.Discoverable { - robotsMeta = robotsAllowSome + robotsMeta = robotsMetaAllowSome } // we should only show the 'back to top' button if the @@ -105,7 +105,7 @@ func (m *Module) profileGETHandler(c *gin.Context) { statusResp, errWithCode := m.processor.AccountWebStatusesGet(ctx, account.ID, maxStatusID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, instanceGet) + apiutil.ErrorHandler(c, errWithCode, instanceGet) return } @@ -145,14 +145,14 @@ func (m *Module) returnAPProfile(ctx context.Context, c *gin.Context, username s user, errWithCode := m.processor.GetFediUser(ctx, username, c.Request.URL) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) //nolint:contextcheck + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) //nolint:contextcheck return } b, mErr := json.Marshal(user) if mErr != nil { err := fmt.Errorf("could not marshal json: %s", mErr) - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) //nolint:contextcheck + apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) //nolint:contextcheck return } diff --git a/internal/web/robots.go b/internal/web/robots.go index c3307d068..0babb31b7 100644 --- a/internal/web/robots.go +++ b/internal/web/robots.go @@ -18,7 +18,45 @@ package web -// https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag#robotsmeta -const ( - robotsAllowSome = "nofollow, noarchive, nositelinkssearchbox, max-image-preview:standard" +import ( + "net/http" + + "github.com/gin-gonic/gin" ) + +const ( + robotsPath = "/robots.txt" + robotsMetaAllowSome = "nofollow, noarchive, nositelinkssearchbox, max-image-preview:standard" // https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag#robotsmeta + robotsTxt = `# GoToSocial robots.txt -- to edit, see internal/web/robots.go +# more info @ https://developers.google.com/search/docs/crawling-indexing/robots/intro +User-agent: * +Crawl-delay: 500 +# api stuff +Disallow: /api/ +# auth/login stuff +Disallow: /auth/ +Disallow: /oauth/ +Disallow: /check_your_email +Disallow: /wait_for_approval +Disallow: /account_disabled +# well known stuff +Disallow: /.well-known/ +# files +Disallow: /fileserver/ +# s2s AP stuff +Disallow: /users/ +Disallow: /emoji/ +# panels +Disallow: /admin +Disallow: /user +Disallow: /settings/` +) + +// robotsGETHandler returns a decent robots.txt that prevents crawling +// the api, auth pages, settings pages, etc. +// +// More granular robots meta tags are then applied for web pages +// depending on user preferences (see internal/web). +func (m *Module) robotsGETHandler(c *gin.Context) { + c.String(http.StatusOK, robotsTxt) +} diff --git a/internal/web/rss.go b/internal/web/rss.go index 827a19e87..27f4a34db 100644 --- a/internal/web/rss.go +++ b/internal/web/rss.go @@ -27,12 +27,12 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/log" ) -const appRSSUTF8 = string(api.AppRSSXML + "; charset=utf-8") +const appRSSUTF8 = string(apiutil.AppRSSXML + "; charset=utf-8") func (m *Module) GetRSSETag(urlPath string, lastModified time.Time, getRSSFeed func() (string, gtserror.WithCode)) (string, error) { if cachedETag, ok := m.eTagCache.Get(urlPath); ok && !lastModified.After(cachedETag.lastModified) { @@ -81,8 +81,8 @@ func (m *Module) rssFeedGETHandler(c *gin.Context) { c.Header(cacheControlHeader, cacheControlNoCache) ctx := c.Request.Context() - if _, err := api.NegotiateAccept(c, api.AppRSSXML); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + if _, err := apiutil.NegotiateAccept(c, apiutil.AppRSSXML); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } @@ -90,7 +90,7 @@ func (m *Module) rssFeedGETHandler(c *gin.Context) { username := strings.ToLower(c.Param(usernameKey)) if username == "" { err := errors.New("no account username specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } @@ -99,7 +99,7 @@ func (m *Module) rssFeedGETHandler(c *gin.Context) { getRssFeed, accountLastPostedPublic, errWithCode := m.processor.AccountGetRSSFeedForUsername(ctx, username) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } @@ -111,13 +111,13 @@ func (m *Module) rssFeedGETHandler(c *gin.Context) { // we either have no cache entry for this, or we have an expired cache entry; generate a new one rssFeed, errWithCode = getRssFeed() if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } eTag, err := generateEtag(bytes.NewBufferString(rssFeed)) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } @@ -145,7 +145,7 @@ func (m *Module) rssFeedGETHandler(c *gin.Context) { // we had a cache entry already so we didn't call to get the rss feed yet rssFeed, errWithCode = getRssFeed() if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } } diff --git a/internal/web/settings-panel.go b/internal/web/settings-panel.go index f38d7522c..53e5a4f29 100644 --- a/internal/web/settings-panel.go +++ b/internal/web/settings-panel.go @@ -22,7 +22,7 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) @@ -31,7 +31,7 @@ func (m *Module) SettingsPanelHandler(c *gin.Context) { host := config.GetHost() instance, err := m.processor.InstanceGet(c.Request.Context(), host) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } diff --git a/internal/web/thread.go b/internal/web/thread.go index 0cb6af6a6..d42b5929f 100644 --- a/internal/web/thread.go +++ b/internal/web/thread.go @@ -28,8 +28,8 @@ import ( "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/ap" - "github.com/superseriousbusiness/gotosocial/internal/api" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -40,7 +40,7 @@ func (m *Module) threadGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, false, false, false, false) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } @@ -48,7 +48,7 @@ func (m *Module) threadGETHandler(c *gin.Context) { username := strings.ToLower(c.Param(usernameKey)) if username == "" { err := errors.New("no account username specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } @@ -56,14 +56,14 @@ func (m *Module) threadGETHandler(c *gin.Context) { statusID := strings.ToUpper(c.Param(statusIDKey)) if statusID == "" { err := errors.New("no status id specified") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } host := config.GetHost() instance, err := m.processor.InstanceGet(ctx, host) if err != nil { - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } @@ -74,33 +74,33 @@ func (m *Module) threadGETHandler(c *gin.Context) { // do this check to make sure the status is actually from a local account, // we shouldn't render threads from statuses that don't belong to us! if _, errWithCode := m.processor.AccountGetLocalByUsername(ctx, authed, username); errWithCode != nil { - api.ErrorHandler(c, errWithCode, instanceGet) + apiutil.ErrorHandler(c, errWithCode, instanceGet) return } status, errWithCode := m.processor.StatusGet(ctx, authed, statusID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, instanceGet) + apiutil.ErrorHandler(c, errWithCode, instanceGet) return } if !strings.EqualFold(username, status.Account.Username) { err := gtserror.NewErrorNotFound(errors.New("path username not equal to status author username")) - api.ErrorHandler(c, gtserror.NewErrorNotFound(err), instanceGet) + apiutil.ErrorHandler(c, gtserror.NewErrorNotFound(err), instanceGet) return } // if we're getting an AP request on this endpoint we // should render the status's AP representation instead - accept := c.NegotiateFormat(string(api.TextHTML), string(api.AppActivityJSON), string(api.AppActivityLDJSON)) - if accept == string(api.AppActivityJSON) || accept == string(api.AppActivityLDJSON) { + accept := c.NegotiateFormat(string(apiutil.TextHTML), string(apiutil.AppActivityJSON), string(apiutil.AppActivityLDJSON)) + if accept == string(apiutil.AppActivityJSON) || accept == string(apiutil.AppActivityLDJSON) { m.returnAPStatus(ctx, c, username, statusID, accept) return } context, errWithCode := m.processor.StatusGetContext(ctx, authed, statusID) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, instanceGet) + apiutil.ErrorHandler(c, errWithCode, instanceGet) return } @@ -135,14 +135,14 @@ func (m *Module) returnAPStatus(ctx context.Context, c *gin.Context, username st status, errWithCode := m.processor.GetFediStatus(ctx, username, statusID, c.Request.URL) if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) //nolint:contextcheck + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet) //nolint:contextcheck return } b, mErr := json.Marshal(status) if mErr != nil { err := fmt.Errorf("could not marshal json: %s", mErr) - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) //nolint:contextcheck + apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) //nolint:contextcheck return } diff --git a/internal/web/web.go b/internal/web/web.go index b9e0a63ff..f263c4655 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -19,28 +19,30 @@ package web import ( - "errors" "net/http" + "path/filepath" "codeberg.org/gruf/go-cache/v3" "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/processing" "github.com/superseriousbusiness/gotosocial/internal/router" "github.com/superseriousbusiness/gotosocial/internal/uris" ) const ( - confirmEmailPath = "/" + uris.ConfirmEmailPath - profilePath = "/@:" + usernameKey - customCSSPath = profilePath + "/custom.css" - rssFeedPath = profilePath + "/feed.rss" - statusPath = profilePath + "/statuses/:" + statusIDKey - assetsPathPrefix = "/assets" - distPathPrefix = assetsPathPrefix + "/dist" - userPanelPath = "/settings/user" - adminPanelPath = "/settings/admin" + confirmEmailPath = "/" + uris.ConfirmEmailPath + profilePath = "/@:" + usernameKey + customCSSPath = profilePath + "/custom.css" + rssFeedPath = profilePath + "/feed.rss" + statusPath = profilePath + "/statuses/:" + statusIDKey + assetsPathPrefix = "/assets" + distPathPrefix = assetsPathPrefix + "/dist" + settingsPathPrefix = "/settings" + settingsPanelGlob = settingsPathPrefix + "/*panel" + userPanelPath = settingsPathPrefix + "/user" + adminPanelPath = settingsPathPrefix + "/admin" tokenParam = "token" usernameKey = "username" @@ -54,67 +56,54 @@ const ( lastModifiedHeader = "Last-Modified" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified ) -// Module implements the api.ClientModule interface for web pages. type Module struct { processor processing.Processor eTagCache cache.Cache[string, eTagCacheEntry] } -// New returns a new api.ClientModule for web pages. -func New(processor processing.Processor) api.ClientModule { +func New(processor processing.Processor) *Module { return &Module{ processor: processor, eTagCache: newETagCache(), } } -// Route satisfies the RESTAPIModule interface -func (m *Module) Route(s router.Router) error { +func (m *Module) Route(r router.Router) { // serve static files from assets dir at /assets - assetsGroup := s.AttachGroup(assetsPathPrefix) - m.mountAssetsFilesystem(assetsGroup) + assetsGroup := r.AttachGroup(assetsPathPrefix) + webAssetsAbsFilePath, err := filepath.Abs(config.GetWebAssetBaseDir()) + if err != nil { + log.Panicf("error getting absolute path of assets dir: %s", err) + } - s.AttachHandler(http.MethodGet, "/settings", m.SettingsPanelHandler) - s.AttachHandler(http.MethodGet, "/settings/*panel", m.SettingsPanelHandler) + fs := fileSystem{http.Dir(webAssetsAbsFilePath)} - // User panel redirects - // used by clients - s.AttachHandler(http.MethodGet, "/auth/edit", func(c *gin.Context) { - c.Redirect(http.StatusMovedPermanently, userPanelPath) - }) + // use the cache middleware on all handlers in this group + assetsGroup.Use(m.assetsCacheControlMiddleware(fs)) - // old version of settings panel - s.AttachHandler(http.MethodGet, "/user", func(c *gin.Context) { - c.Redirect(http.StatusMovedPermanently, userPanelPath) - }) + // serve static file system in the root of this group, + // will end up being something like "/assets/" + assetsGroup.StaticFS("/", fs) - // Admin panel redirects - // old version of settings panel - s.AttachHandler(http.MethodGet, "/admin", func(c *gin.Context) { - c.Redirect(http.StatusMovedPermanently, adminPanelPath) - }) + /* + Attach individual web handlers which require no specific middlewares + */ - // serve front-page - s.AttachHandler(http.MethodGet, "/", m.baseHandler) + r.AttachHandler(http.MethodGet, "/", m.baseHandler) // front-page + r.AttachHandler(http.MethodGet, settingsPathPrefix, m.SettingsPanelHandler) + r.AttachHandler(http.MethodGet, settingsPanelGlob, m.SettingsPanelHandler) + r.AttachHandler(http.MethodGet, profilePath, m.profileGETHandler) + r.AttachHandler(http.MethodGet, customCSSPath, m.customCSSGETHandler) + r.AttachHandler(http.MethodGet, rssFeedPath, m.rssFeedGETHandler) + r.AttachHandler(http.MethodGet, statusPath, m.threadGETHandler) + r.AttachHandler(http.MethodGet, confirmEmailPath, m.confirmEmailGETHandler) + r.AttachHandler(http.MethodGet, robotsPath, m.robotsGETHandler) - // serve profile pages at /@username - s.AttachHandler(http.MethodGet, profilePath, m.profileGETHandler) + /* + Attach redirects from old endpoints to current ones for backwards compatibility + */ - // serve custom css at /@username/custom.css - s.AttachHandler(http.MethodGet, customCSSPath, m.customCSSGETHandler) - - s.AttachHandler(http.MethodGet, rssFeedPath, m.rssFeedGETHandler) - - // serve statuses - s.AttachHandler(http.MethodGet, statusPath, m.threadGETHandler) - - // serve email confirmation page at /confirm_email?token=whatever - s.AttachHandler(http.MethodGet, confirmEmailPath, m.confirmEmailGETHandler) - - // 404 handler - s.AttachNoRouteHandler(func(c *gin.Context) { - api.ErrorHandler(c, gtserror.NewErrorNotFound(errors.New(http.StatusText(http.StatusNotFound))), m.processor.InstanceGet) - }) - - return nil + r.AttachHandler(http.MethodGet, "/auth/edit", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, userPanelPath) }) + r.AttachHandler(http.MethodGet, "/user", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, userPanelPath) }) + r.AttachHandler(http.MethodGet, "/admin", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, adminPanelPath) }) } diff --git a/testrig/router.go b/testrig/router.go index b8f6a77f5..ce849423d 100644 --- a/testrig/router.go +++ b/testrig/router.go @@ -51,7 +51,7 @@ func NewTestRouter(db db.DB) router.Router { } } - r, err := router.New(context.Background(), db) + r, err := router.New(context.Background()) if err != nil { panic(err) }