diff options
Diffstat (limited to 'internal')
-rw-r--r-- | internal/api/activitypub.go | 66 | ||||
-rw-r--r-- | internal/api/activitypub/emoji/emoji.go (renamed from internal/api/client/emoji/emoji.go) | 19 | ||||
-rw-r--r-- | internal/api/activitypub/emoji/emojiget.go (renamed from internal/api/s2s/emoji/emojiget.go) | 29 | ||||
-rw-r--r-- | internal/api/activitypub/emoji/emojiget_test.go (renamed from internal/api/s2s/emoji/emojiget_test.go) | 31 | ||||
-rw-r--r-- | internal/api/activitypub/users/common.go (renamed from internal/api/s2s/user/common.go) | 26 | ||||
-rw-r--r-- | internal/api/activitypub/users/followers.go (renamed from internal/api/s2s/user/followers.go) | 18 | ||||
-rw-r--r-- | internal/api/activitypub/users/following.go (renamed from internal/api/s2s/user/following.go) | 18 | ||||
-rw-r--r-- | internal/api/activitypub/users/inboxpost.go (renamed from internal/api/s2s/user/inboxpost.go) | 14 | ||||
-rw-r--r-- | internal/api/activitypub/users/inboxpost_test.go (renamed from internal/api/s2s/user/inboxpost_test.go) | 28 | ||||
-rw-r--r-- | internal/api/activitypub/users/outboxget.go (renamed from internal/api/s2s/user/outboxget.go) | 20 | ||||
-rw-r--r-- | internal/api/activitypub/users/outboxget_test.go (renamed from internal/api/s2s/user/outboxget_test.go) | 22 | ||||
-rw-r--r-- | internal/api/activitypub/users/publickeyget.go (renamed from internal/api/s2s/user/publickeyget.go) | 18 | ||||
-rw-r--r-- | internal/api/activitypub/users/repliesget.go (renamed from internal/api/s2s/user/repliesget.go) | 24 | ||||
-rw-r--r-- | internal/api/activitypub/users/repliesget_test.go (renamed from internal/api/s2s/user/repliesget_test.go) | 26 | ||||
-rw-r--r-- | internal/api/activitypub/users/statusget.go (renamed from internal/api/s2s/user/statusget.go) | 20 | ||||
-rw-r--r-- | internal/api/activitypub/users/statusget_test.go (renamed from internal/api/s2s/user/statusget_test.go) | 16 | ||||
-rw-r--r-- | internal/api/activitypub/users/user.go | 80 | ||||
-rw-r--r-- | internal/api/activitypub/users/user_test.go (renamed from internal/api/s2s/user/user_test.go) | 34 | ||||
-rw-r--r-- | internal/api/activitypub/users/userget.go (renamed from internal/api/s2s/user/userget.go) | 18 | ||||
-rw-r--r-- | internal/api/activitypub/users/userget_test.go (renamed from internal/api/s2s/user/userget_test.go) | 14 | ||||
-rw-r--r-- | internal/api/apimodule.go | 37 | ||||
-rw-r--r-- | internal/api/auth.go | 64 | ||||
-rw-r--r-- | internal/api/auth/auth.go | 117 | ||||
-rw-r--r-- | internal/api/auth/auth_test.go (renamed from internal/api/client/auth/auth_test.go) | 24 | ||||
-rw-r--r-- | internal/api/auth/authorize.go (renamed from internal/api/client/auth/authorize.go) | 46 | ||||
-rw-r--r-- | internal/api/auth/authorize_test.go (renamed from internal/api/client/auth/authorize_test.go) | 10 | ||||
-rw-r--r-- | internal/api/auth/callback.go (renamed from internal/api/client/auth/callback.go) | 48 | ||||
-rw-r--r-- | internal/api/auth/oob.go (renamed from internal/api/client/auth/oob.go) | 18 | ||||
-rw-r--r-- | internal/api/auth/signin.go (renamed from internal/api/client/auth/signin.go) | 20 | ||||
-rw-r--r-- | internal/api/auth/token.go (renamed from internal/api/client/auth/token.go) | 12 | ||||
-rw-r--r-- | internal/api/auth/token_test.go (renamed from internal/api/client/auth/token_test.go) | 0 | ||||
-rw-r--r-- | internal/api/client.go | 129 | ||||
-rw-r--r-- | internal/api/client/accounts/account_test.go (renamed from internal/api/client/account/account_test.go) | 8 | ||||
-rw-r--r-- | internal/api/client/accounts/accountcreate.go (renamed from internal/api/client/account/accountcreate.go) | 24 | ||||
-rw-r--r-- | internal/api/client/accounts/accountcreate_test.go (renamed from internal/api/client/account/accountcreate_test.go) | 2 | ||||
-rw-r--r-- | internal/api/client/accounts/accountdelete.go (renamed from internal/api/client/account/accountdelete.go) | 16 | ||||
-rw-r--r-- | internal/api/client/accounts/accountdelete_test.go (renamed from internal/api/client/account/accountdelete_test.go) | 16 | ||||
-rw-r--r-- | internal/api/client/accounts/accountget.go (renamed from internal/api/client/account/accountget.go) | 14 | ||||
-rw-r--r-- | internal/api/client/accounts/accounts.go (renamed from internal/api/client/account/account.go) | 64 | ||||
-rw-r--r-- | internal/api/client/accounts/accountupdate.go (renamed from internal/api/client/account/accountupdate.go) | 22 | ||||
-rw-r--r-- | internal/api/client/accounts/accountupdate_test.go (renamed from internal/api/client/account/accountupdate_test.go) | 44 | ||||
-rw-r--r-- | internal/api/client/accounts/accountverify.go (renamed from internal/api/client/account/accountverify.go) | 12 | ||||
-rw-r--r-- | internal/api/client/accounts/accountverify_test.go (renamed from internal/api/client/account/accountverify_test.go) | 8 | ||||
-rw-r--r-- | internal/api/client/accounts/block.go (renamed from internal/api/client/account/block.go) | 14 | ||||
-rw-r--r-- | internal/api/client/accounts/block_test.go (renamed from internal/api/client/account/block_test.go) | 10 | ||||
-rw-r--r-- | internal/api/client/accounts/follow.go (renamed from internal/api/client/account/follow.go) | 20 | ||||
-rw-r--r-- | internal/api/client/accounts/follow_test.go (renamed from internal/api/client/account/follow_test.go) | 10 | ||||
-rw-r--r-- | internal/api/client/accounts/followers.go (renamed from internal/api/client/account/followers.go) | 14 | ||||
-rw-r--r-- | internal/api/client/accounts/following.go (renamed from internal/api/client/account/following.go) | 14 | ||||
-rw-r--r-- | internal/api/client/accounts/relationships.go (renamed from internal/api/client/account/relationships.go) | 18 | ||||
-rw-r--r-- | internal/api/client/accounts/statuses.go (renamed from internal/api/client/account/statuses.go) | 26 | ||||
-rw-r--r-- | internal/api/client/accounts/statuses_test.go (renamed from internal/api/client/account/statuses_test.go) | 12 | ||||
-rw-r--r-- | internal/api/client/accounts/unblock.go (renamed from internal/api/client/account/unblock.go) | 14 | ||||
-rw-r--r-- | internal/api/client/accounts/unfollow.go (renamed from internal/api/client/account/unfollow.go) | 14 | ||||
-rw-r--r-- | internal/api/client/admin/accountaction.go | 18 | ||||
-rw-r--r-- | internal/api/client/admin/admin.go | 41 | ||||
-rw-r--r-- | internal/api/client/admin/admin_test.go | 2 | ||||
-rw-r--r-- | internal/api/client/admin/domainblockcreate.go | 26 | ||||
-rw-r--r-- | internal/api/client/admin/domainblockdelete.go | 14 | ||||
-rw-r--r-- | internal/api/client/admin/domainblockget.go | 16 | ||||
-rw-r--r-- | internal/api/client/admin/domainblocksget.go | 14 | ||||
-rw-r--r-- | internal/api/client/admin/emojicategoriesget.go | 12 | ||||
-rw-r--r-- | internal/api/client/admin/emojicreate.go | 22 | ||||
-rw-r--r-- | internal/api/client/admin/emojidelete.go | 14 | ||||
-rw-r--r-- | internal/api/client/admin/emojiget.go | 14 | ||||
-rw-r--r-- | internal/api/client/admin/emojisget.go | 16 | ||||
-rw-r--r-- | internal/api/client/admin/emojiupdate.go | 36 | ||||
-rw-r--r-- | internal/api/client/admin/mediacleanup.go | 14 | ||||
-rw-r--r-- | internal/api/client/admin/mediarefetch.go | 8 | ||||
-rw-r--r-- | internal/api/client/app/app_test.go | 21 | ||||
-rw-r--r-- | internal/api/client/apps/appcreate.go (renamed from internal/api/client/app/appcreate.go) | 26 | ||||
-rw-r--r-- | internal/api/client/apps/apps.go (renamed from internal/api/client/app/app.go) | 19 | ||||
-rw-r--r-- | internal/api/client/auth/auth.go | 105 | ||||
-rw-r--r-- | internal/api/client/blocks/blocks.go | 17 | ||||
-rw-r--r-- | internal/api/client/blocks/blocksget.go | 12 | ||||
-rw-r--r-- | internal/api/client/bookmarks/bookmarks.go | 13 | ||||
-rw-r--r-- | internal/api/client/bookmarks/bookmarks_test.go | 10 | ||||
-rw-r--r-- | internal/api/client/bookmarks/bookmarksget.go | 14 | ||||
-rw-r--r-- | internal/api/client/customemojis/customemojis.go | 45 | ||||
-rw-r--r-- | internal/api/client/customemojis/customemojisget.go | 76 | ||||
-rw-r--r-- | internal/api/client/emoji/emojisget.go | 58 | ||||
-rw-r--r-- | internal/api/client/favourites/favourites.go | 17 | ||||
-rw-r--r-- | internal/api/client/favourites/favourites_test.go | 2 | ||||
-rw-r--r-- | internal/api/client/favourites/favouritesget.go | 12 | ||||
-rw-r--r-- | internal/api/client/filters/filter.go (renamed from internal/api/client/filter/filter.go) | 17 | ||||
-rw-r--r-- | internal/api/client/filters/filtersget.go (renamed from internal/api/client/filter/filtersget.go) | 8 | ||||
-rw-r--r-- | internal/api/client/followrequests/authorize.go (renamed from internal/api/client/followrequest/authorize.go) | 14 | ||||
-rw-r--r-- | internal/api/client/followrequests/authorize_test.go (renamed from internal/api/client/followrequest/authorize_test.go) | 8 | ||||
-rw-r--r-- | internal/api/client/followrequests/followrequest.go (renamed from internal/api/client/followrequest/followrequest.go) | 23 | ||||
-rw-r--r-- | internal/api/client/followrequests/followrequest_test.go (renamed from internal/api/client/followrequest/followrequest_test.go) | 8 | ||||
-rw-r--r-- | internal/api/client/followrequests/get.go (renamed from internal/api/client/followrequest/get.go) | 12 | ||||
-rw-r--r-- | internal/api/client/followrequests/get_test.go (renamed from internal/api/client/followrequest/get_test.go) | 2 | ||||
-rw-r--r-- | internal/api/client/followrequests/reject.go (renamed from internal/api/client/followrequest/reject.go) | 14 | ||||
-rw-r--r-- | internal/api/client/followrequests/reject_test.go (renamed from internal/api/client/followrequest/reject_test.go) | 6 | ||||
-rw-r--r-- | internal/api/client/instance/instance.go | 21 | ||||
-rw-r--r-- | internal/api/client/instance/instance_test.go | 2 | ||||
-rw-r--r-- | internal/api/client/instance/instanceget.go | 8 | ||||
-rw-r--r-- | internal/api/client/instance/instancepatch.go | 22 | ||||
-rw-r--r-- | internal/api/client/instance/instancepeersget.go | 12 | ||||
-rw-r--r-- | internal/api/client/list/listsgets.go | 25 | ||||
-rw-r--r-- | internal/api/client/lists/list.go (renamed from internal/api/client/list/list.go) | 19 | ||||
-rw-r--r-- | internal/api/client/lists/listsgets.go (renamed from internal/api/security/robots.go) | 49 | ||||
-rw-r--r-- | internal/api/client/media/media.go | 27 | ||||
-rw-r--r-- | internal/api/client/media/mediacreate.go | 34 | ||||
-rw-r--r-- | internal/api/client/media/mediacreate_test.go | 53 | ||||
-rw-r--r-- | internal/api/client/media/mediaget.go | 18 | ||||
-rw-r--r-- | internal/api/client/media/mediaupdate.go | 28 | ||||
-rw-r--r-- | internal/api/client/media/mediaupdate_test.go | 31 | ||||
-rw-r--r-- | internal/api/client/notifications/notifications.go (renamed from internal/api/client/notification/notification.go) | 21 | ||||
-rw-r--r-- | internal/api/client/notifications/notificationsclear.go (renamed from internal/api/client/notification/notificationsclear.go) | 12 | ||||
-rw-r--r-- | internal/api/client/notifications/notificationsget.go (renamed from internal/api/client/notification/notificationsget.go) | 14 | ||||
-rw-r--r-- | internal/api/client/search/search.go | 23 | ||||
-rw-r--r-- | internal/api/client/search/search_test.go | 2 | ||||
-rw-r--r-- | internal/api/client/search/searchget.go | 26 | ||||
-rw-r--r-- | internal/api/client/statuses/status.go (renamed from internal/api/client/status/status.go) | 77 | ||||
-rw-r--r-- | internal/api/client/statuses/status_test.go (renamed from internal/api/client/status/status_test.go) | 8 | ||||
-rw-r--r-- | internal/api/client/statuses/statusbookmark.go (renamed from internal/api/client/status/statusbookmark.go) | 14 | ||||
-rw-r--r-- | internal/api/client/statuses/statusbookmark_test.go (renamed from internal/api/client/status/statusbookmark_test.go) | 8 | ||||
-rw-r--r-- | internal/api/client/statuses/statusboost.go (renamed from internal/api/client/status/statusboost.go) | 14 | ||||
-rw-r--r-- | internal/api/client/statuses/statusboost_test.go (renamed from internal/api/client/status/statusboost_test.go) | 28 | ||||
-rw-r--r-- | internal/api/client/statuses/statusboostedby.go (renamed from internal/api/client/status/statusboostedby.go) | 10 | ||||
-rw-r--r-- | internal/api/client/statuses/statusboostedby_test.go (renamed from internal/api/client/status/statusboostedby_test.go) | 8 | ||||
-rw-r--r-- | internal/api/client/statuses/statuscontext.go (renamed from internal/api/client/status/statuscontext.go) | 14 | ||||
-rw-r--r-- | internal/api/client/statuses/statuscreate.go (renamed from internal/api/client/status/statuscreate.go) | 22 | ||||
-rw-r--r-- | internal/api/client/statuses/statuscreate_test.go (renamed from internal/api/client/status/statuscreate_test.go) | 52 | ||||
-rw-r--r-- | internal/api/client/statuses/statusdelete.go (renamed from internal/api/client/status/statusdelete.go) | 14 | ||||
-rw-r--r-- | internal/api/client/statuses/statusdelete_test.go (renamed from internal/api/client/status/statusdelete_test.go) | 12 | ||||
-rw-r--r-- | internal/api/client/statuses/statusfave.go (renamed from internal/api/client/status/statusfave.go) | 14 | ||||
-rw-r--r-- | internal/api/client/statuses/statusfave_test.go (renamed from internal/api/client/status/statusfave_test.go) | 19 | ||||
-rw-r--r-- | internal/api/client/statuses/statusfavedby.go (renamed from internal/api/client/status/statusfavedby.go) | 14 | ||||
-rw-r--r-- | internal/api/client/statuses/statusfavedby_test.go (renamed from internal/api/client/status/statusfavedby_test.go) | 12 | ||||
-rw-r--r-- | internal/api/client/statuses/statusget.go (renamed from internal/api/client/status/statusget.go) | 14 | ||||
-rw-r--r-- | internal/api/client/statuses/statusget_test.go (renamed from internal/api/client/status/statusget_test.go) | 2 | ||||
-rw-r--r-- | internal/api/client/statuses/statusunbookmark.go (renamed from internal/api/client/status/statusunbookmark.go) | 14 | ||||
-rw-r--r-- | internal/api/client/statuses/statusunbookmark_test.go (renamed from internal/api/client/status/statusunbookmark_test.go) | 8 | ||||
-rw-r--r-- | internal/api/client/statuses/statusunboost.go (renamed from internal/api/client/status/statusunboost.go) | 14 | ||||
-rw-r--r-- | internal/api/client/statuses/statusunfave.go (renamed from internal/api/client/status/statusunfave.go) | 14 | ||||
-rw-r--r-- | internal/api/client/statuses/statusunfave_test.go (renamed from internal/api/client/status/statusunfave_test.go) | 22 | ||||
-rw-r--r-- | internal/api/client/streaming/stream.go | 60 | ||||
-rw-r--r-- | internal/api/client/streaming/streaming.go | 19 | ||||
-rw-r--r-- | internal/api/client/streaming/streaming_test.go | 2 | ||||
-rw-r--r-- | internal/api/client/timelines/home.go (renamed from internal/api/client/timeline/home.go) | 16 | ||||
-rw-r--r-- | internal/api/client/timelines/public.go (renamed from internal/api/client/timeline/public.go) | 16 | ||||
-rw-r--r-- | internal/api/client/timelines/timeline.go (renamed from internal/api/client/timeline/timeline.go) | 21 | ||||
-rw-r--r-- | internal/api/client/user/passwordchange.go | 20 | ||||
-rw-r--r-- | internal/api/client/user/user.go | 17 | ||||
-rw-r--r-- | internal/api/client/user/user_test.go | 2 | ||||
-rw-r--r-- | internal/api/fileserver.go | 55 | ||||
-rw-r--r-- | internal/api/fileserver/fileserver.go (renamed from internal/api/client/fileserver/fileserver.go) | 28 | ||||
-rw-r--r-- | internal/api/fileserver/fileserver_test.go (renamed from internal/api/client/fileserver/fileserver_test.go) | 12 | ||||
-rw-r--r-- | internal/api/fileserver/servefile.go (renamed from internal/api/client/fileserver/servefile.go) | 33 | ||||
-rw-r--r-- | internal/api/fileserver/servefile_test.go (renamed from internal/api/client/fileserver/servefile_test.go) | 2 | ||||
-rw-r--r-- | internal/api/nodeinfo.go | 50 | ||||
-rw-r--r-- | internal/api/nodeinfo/nodeinfo.go | 46 | ||||
-rw-r--r-- | internal/api/nodeinfo/nodeinfoget.go (renamed from internal/api/s2s/nodeinfo/nodeinfoget.go) | 20 | ||||
-rw-r--r-- | internal/api/s2s/emoji/emoji.go | 53 | ||||
-rw-r--r-- | internal/api/s2s/nodeinfo/nodeinfo.go | 53 | ||||
-rw-r--r-- | internal/api/s2s/user/user.go | 89 | ||||
-rw-r--r-- | internal/api/security/ratelimit.go | 70 | ||||
-rw-r--r-- | internal/api/security/security.go | 65 | ||||
-rw-r--r-- | internal/api/security/signaturecheck.go | 51 | ||||
-rw-r--r-- | internal/api/security/tokencheck.go | 120 | ||||
-rw-r--r-- | internal/api/util/errorhandling.go (renamed from internal/api/errorhandling.go) | 2 | ||||
-rw-r--r-- | internal/api/util/mime.go (renamed from internal/api/mime.go) | 2 | ||||
-rw-r--r-- | internal/api/util/negotiate.go (renamed from internal/api/negotiate.go) | 2 | ||||
-rw-r--r-- | internal/api/util/signaturectx.go (renamed from internal/api/security/flocblock.go) | 34 | ||||
-rw-r--r-- | internal/api/wellknown.go | 55 | ||||
-rw-r--r-- | internal/api/wellknown/nodeinfo/nodeinfo.go | 47 | ||||
-rw-r--r-- | internal/api/wellknown/nodeinfo/nodeinfoget.go (renamed from internal/api/s2s/nodeinfo/wellknownget.go) | 16 | ||||
-rw-r--r-- | internal/api/wellknown/webfinger/webfinger.go (renamed from internal/api/s2s/webfinger/webfinger.go) | 18 | ||||
-rw-r--r-- | internal/api/wellknown/webfinger/webfinger_test.go (renamed from internal/api/s2s/webfinger/webfinger_test.go) | 23 | ||||
-rw-r--r-- | internal/api/wellknown/webfinger/webfingerget.go (renamed from internal/api/s2s/webfinger/webfingerget.go) | 45 | ||||
-rw-r--r-- | internal/api/wellknown/webfinger/webfingerget_test.go (renamed from internal/api/s2s/webfinger/webfingerget_test.go) | 6 | ||||
-rw-r--r-- | internal/config/defaults.go | 2 | ||||
-rw-r--r-- | internal/db/db.go | 2 | ||||
-rw-r--r-- | internal/middleware/cachecontrol.go (renamed from internal/api/security/useragentblock.go) | 18 | ||||
-rw-r--r-- | internal/middleware/cors.go | 86 | ||||
-rw-r--r-- | internal/middleware/extraheaders.go (renamed from internal/api/security/extraheaders.go) | 19 | ||||
-rw-r--r-- | internal/middleware/gzip.go (renamed from internal/router/gzip.go) | 10 | ||||
-rw-r--r-- | internal/middleware/logger.go | 99 | ||||
-rw-r--r-- | internal/middleware/ratelimit.go | 77 | ||||
-rw-r--r-- | internal/middleware/session.go (renamed from internal/router/session.go) | 30 | ||||
-rw-r--r-- | internal/middleware/session_test.go (renamed from internal/router/session_test.go) | 16 | ||||
-rw-r--r-- | internal/middleware/signaturecheck.go | 93 | ||||
-rw-r--r-- | internal/middleware/tokencheck.go | 140 | ||||
-rw-r--r-- | internal/middleware/useragent.go (renamed from internal/api/client/auth/util.go) | 22 | ||||
-rw-r--r-- | internal/oidc/idp.go | 8 | ||||
-rw-r--r-- | internal/processing/federation.go | 8 | ||||
-rw-r--r-- | internal/processing/federation/federation.go | 4 | ||||
-rw-r--r-- | internal/processing/federation/getnodeinfo.go | 5 | ||||
-rw-r--r-- | internal/processing/fromclientapi_test.go | 4 | ||||
-rw-r--r-- | internal/processing/fromfederator_test.go | 8 | ||||
-rw-r--r-- | internal/processing/oauth.go | 6 | ||||
-rw-r--r-- | internal/processing/processor.go | 6 | ||||
-rw-r--r-- | internal/processing/status/create_test.go | 52 | ||||
-rw-r--r-- | internal/processing/status/util.go | 7 | ||||
-rw-r--r-- | internal/processing/status/util_test.go | 62 | ||||
-rw-r--r-- | internal/router/attach.go | 23 | ||||
-rw-r--r-- | internal/router/cors.go | 87 | ||||
-rw-r--r-- | internal/router/logger.go | 96 | ||||
-rw-r--r-- | internal/router/router.go | 67 | ||||
-rw-r--r-- | internal/router/template.go | 16 | ||||
-rw-r--r-- | internal/text/emojify.go | 6 | ||||
-rw-r--r-- | internal/transport/deliver.go | 4 | ||||
-rw-r--r-- | internal/transport/dereference.go | 4 | ||||
-rw-r--r-- | internal/transport/derefinstance.go | 8 | ||||
-rw-r--r-- | internal/transport/finger.go | 4 | ||||
-rw-r--r-- | internal/typeutils/converter.go | 38 | ||||
-rw-r--r-- | internal/typeutils/frontendtointernal.go | 14 | ||||
-rw-r--r-- | internal/typeutils/internaltofrontend.go | 142 | ||||
-rw-r--r-- | internal/typeutils/internaltorss.go | 4 | ||||
-rw-r--r-- | internal/web/assets.go | 21 | ||||
-rw-r--r-- | internal/web/base.go | 4 | ||||
-rw-r--r-- | internal/web/confirmemail.go | 8 | ||||
-rw-r--r-- | internal/web/customcss.go | 14 | ||||
-rw-r--r-- | internal/web/profile.go | 22 | ||||
-rw-r--r-- | internal/web/robots.go | 42 | ||||
-rw-r--r-- | internal/web/rss.go | 18 | ||||
-rw-r--r-- | internal/web/settings-panel.go | 4 | ||||
-rw-r--r-- | internal/web/thread.go | 26 | ||||
-rw-r--r-- | internal/web/web.go | 101 |
221 files changed, 3044 insertions, 2846 deletions
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 <http://www.gnu.org/licenses/>. +*/ + +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 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 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 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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 <http://www.gnu.org/licenses/>. +*/ + +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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 <http://www.gnu.org/licenses/>. -*/ - -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 <http://www.gnu.org/licenses/>. +*/ + +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 <http://www.gnu.org/licenses/>. +*/ + +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 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 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 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 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 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 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 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 index 50bbd6918..50bbd6918 100644 --- a/internal/api/client/auth/token_test.go +++ b/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 <http://www.gnu.org/licenses/>. +*/ + +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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. // */ -package account_test +package accounts_test diff --git a/internal/api/client/account/accountdelete.go b/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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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) + attachHandler(http.MethodPost, BasePath, m.AccountCreatePOSTHandler) + + // get account + attachHandler(http.MethodGet, BasePathWithID, m.AccountGETHandler) // delete account - r.AttachHandler(http.MethodPost, DeleteAccountPath, m.AccountDeletePOSTHandler) + attachHandler(http.MethodPost, DeleteAccountPath, m.AccountDeletePOSTHandler) - // get account - r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler) + // 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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 <http://www.gnu.org/licenses/>. -*/ - -package app_test - -// TODO: write tests diff --git a/internal/api/client/app/appcreate.go b/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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 <http://www.gnu.org/licenses/>. -*/ - -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 <http://www.gnu.org/licenses/>. +*/ + +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 <http://www.gnu.org/licenses/>. +*/ + +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 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 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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -package followrequest_test +package followrequests_test import ( "context" diff --git a/internal/api/client/followrequest/reject.go b/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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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/ -` - -// 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) +// 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 + } + + 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) { - authed, err := oauth.Authed(c, true, true, true, true) - if err != nil { - api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + 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 } - 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 } - 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) + if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { + apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(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) { + 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 + } + 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 } 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) { + 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 + } + 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 } 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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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) - - r.AttachHandler(http.MethodPost, FavouritePath, m.StatusFavePOSTHandler) - r.AttachHandler(http.MethodPost, UnfavouritePath, m.StatusUnfavePOSTHandler) - r.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) - - 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) - } - } +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) + + // fave stuff + attachHandler(http.MethodPost, FavouritePath, m.StatusFavePOSTHandler) + attachHandler(http.MethodPost, UnfavouritePath, m.StatusUnfavePOSTHandler) + attachHandler(http.MethodGet, FavouritedPath, m.StatusFavedByGETHandler) + + // 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) + + // 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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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("<p>this is a brand new status! <a href=\"http://localhost:8080/tags/helloworld\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>helloworld</span></a></p>", 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(`<p>hello <span class="h-card"><a href="https://unknown-instance.com/@brand_new_person" class="u-url mention" rel="nofollow noreferrer noopener" target="_blank">@<span>brand_new_person</span></a></span></p>`, 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("<p>hello <span class=\"h-card\"><a href=\"http://localhost:8080/@%s\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>%s</span></a></span> this reply should work!</p>", 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("<p>here's an image attachment</p>", 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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -package status_test +package statuses_test import ( "testing" diff --git a/internal/api/client/status/statusunbookmark.go b/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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 <http://www.gnu.org/licenses/>. +*/ + 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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 <http://www.gnu.org/licenses/>. +*/ + +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 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 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 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 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 <http://www.gnu.org/licenses/>. +*/ + +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 <http://www.gnu.org/licenses/>. +*/ + +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 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 <http://www.gnu.org/licenses/>. -*/ - -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 <http://www.gnu.org/licenses/>. -*/ - -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 <http://www.gnu.org/licenses/>. -*/ - -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/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 <http://www.gnu.org/licenses/>. -*/ - -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 <http://www.gnu.org/licenses/>. -*/ - -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 <http://www.gnu.org/licenses/>. -*/ - -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 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 <http://www.gnu.org/licenses/>. */ -package api +package util import ( "context" diff --git a/internal/api/mime.go b/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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -package api +package util import ( "errors" diff --git a/internal/api/security/flocblock.go b/internal/api/util/signaturectx.go index 0b61f4ef5..ec7d2c816 100644 --- a/internal/api/security/flocblock.go +++ b/internal/api/util/signaturectx.go @@ -16,16 +16,26 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ -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=()") +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 <http://www.gnu.org/licenses/>. +*/ + +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 <http://www.gnu.org/licenses/>. +*/ + +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 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 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 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 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 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 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 <http://www.gnu.org/licenses/>. */ -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 <http://www.gnu.org/licenses/>. +*/ + +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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 <http://www.gnu.org/licenses/>. +*/ + +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 <http://www.gnu.org/licenses/>. +*/ + +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 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 <http://www.gnu.org/licenses/>. */ -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 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 <http://www.gnu.org/licenses/>. */ -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 <http://www.gnu.org/licenses/>. +*/ + +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 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 <http://www.gnu.org/licenses/>. */ -package auth +package middleware import ( - "github.com/gin-contrib/sessions" -) + "errors" + "net/http" -func (m *Module) clearSession(s sessions.Session) { - s.Clear() + "github.com/gin-gonic/gin" +) - 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) - } -} - -// AttachMiddleware attaches a gin middleware to the router that will be used globally -func (r *router) AttachMiddleware(middleware gin.HandlerFunc) { - r.engine.Use(middleware) +func (r *router) AttachGlobalMiddleware(handlers ...gin.HandlerFunc) gin.IRoutes { + return r.engine.Use(handlers...) } -// 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 <http://www.gnu.org/licenses/>. -*/ - -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 <http://www.gnu.org/licenses/>. -*/ - -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 router's Attach functions should be used *before* the router is Started. // -// 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) +// 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 +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + const ( - robotsAllowSome = "nofollow, noarchive, nositelinkssearchbox, max-image-preview:standard" + 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) - - s.AttachHandler(http.MethodGet, "/settings", m.SettingsPanelHandler) - s.AttachHandler(http.MethodGet, "/settings/*panel", m.SettingsPanelHandler) - - // User panel redirects - // used by clients - s.AttachHandler(http.MethodGet, "/auth/edit", func(c *gin.Context) { - c.Redirect(http.StatusMovedPermanently, userPanelPath) - }) - - // old version of settings panel - s.AttachHandler(http.MethodGet, "/user", func(c *gin.Context) { - c.Redirect(http.StatusMovedPermanently, userPanelPath) - }) - - // Admin panel redirects - // old version of settings panel - s.AttachHandler(http.MethodGet, "/admin", func(c *gin.Context) { - c.Redirect(http.StatusMovedPermanently, adminPanelPath) - }) - - // serve front-page - s.AttachHandler(http.MethodGet, "/", m.baseHandler) + assetsGroup := r.AttachGroup(assetsPathPrefix) + webAssetsAbsFilePath, err := filepath.Abs(config.GetWebAssetBaseDir()) + if err != nil { + log.Panicf("error getting absolute path of assets dir: %s", err) + } - // serve profile pages at /@username - s.AttachHandler(http.MethodGet, profilePath, m.profileGETHandler) + fs := fileSystem{http.Dir(webAssetsAbsFilePath)} - // serve custom css at /@username/custom.css - s.AttachHandler(http.MethodGet, customCSSPath, m.customCSSGETHandler) + // use the cache middleware on all handlers in this group + assetsGroup.Use(m.assetsCacheControlMiddleware(fs)) - s.AttachHandler(http.MethodGet, rssFeedPath, m.rssFeedGETHandler) + // serve static file system in the root of this group, + // will end up being something like "/assets/" + assetsGroup.StaticFS("/", fs) - // serve statuses - s.AttachHandler(http.MethodGet, statusPath, m.threadGETHandler) + /* + Attach individual web handlers which require no specific middlewares + */ - // serve email confirmation page at /confirm_email?token=whatever - s.AttachHandler(http.MethodGet, confirmEmailPath, m.confirmEmailGETHandler) + 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) - // 404 handler - s.AttachNoRouteHandler(func(c *gin.Context) { - api.ErrorHandler(c, gtserror.NewErrorNotFound(errors.New(http.StatusText(http.StatusNotFound))), m.processor.InstanceGet) - }) + /* + Attach redirects from old endpoints to current ones for backwards compatibility + */ - 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) }) } |