From 265b680098166bfbea85f7ea9d4d97b586a2bbdf Mon Sep 17 00:00:00 2001 From: 0hlov3 <0hlov3@protonmail.com> Date: Wed, 1 Jun 2022 18:18:25 +0200 Subject: [PATCH 01/13] [chore] Adds Issue templates to Github (#626) * Adds ISSUE Templates * Corrects the ISSUE_Templates --- .github/ISSUE_TEMPLATE/bug_report.yaml | 66 +++++++++++++++++++++ .github/ISSUE_TEMPLATE/custom.md | 8 +++ .github/ISSUE_TEMPLATE/feature_request.yaml | 44 ++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yaml create mode 100644 .github/ISSUE_TEMPLATE/custom.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yaml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 000000000..02c275e27 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,66 @@ +name: Bug Report +description: Create a report to help us improve +title: "[bug] Issue Title" +labels: [bug] +assignees: + - +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! Please be cautious with the sensitive information/logs while filing the issue. + - type: textarea + id: desc + attributes: + label: Describe the bug with a clear and concise description of what the bug is. + validations: + required: true + + - type: input + id: GoToSocial-Version + attributes: + label: What's your GoToSocial Version? + description: Enter the Version of your GoToSocial Installation + placeholder: e.g. v0.3.4 + validations: + required: true + + - type: input + id: GoToSocial-Arch + attributes: + label: GoToSocial Arch + description: What architecture do you use and did you do a Binary or Docker installation? + placeholder: e.g. arm64 Docker or arm64 Binary + validations: + required: false + + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Enter exactly what happened. + validations: + required: false + + - type: textarea + id: what-expected + attributes: + label: What you expected to happen? + description: Enter what you expected to happen. + validations: + required: false + + - type: textarea + id: how-to-reproduce + attributes: + label: How to reproduce it? + description: As minimally and precisely as possible. + validations: + required: false + + - type: textarea + id: anything-else + attributes: + label: Anything else we need to know? + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 000000000..003a76f2d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,8 @@ +--- +name: Custom issue template +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 000000000..0b07560ac --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,44 @@ +name: Feature request +description: Suggest an idea for this project +title: "[feature] Issue Title" +labels: [enhancement] +assignees: + - +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this feature request! + + - type: textarea + id: desc + attributes: + label: Is your feature request related to a problem ? + description: Give a clear and concise description of what the problem is. + placeholder: ex. I'm always frustrated when [...] + validations: + required: true + + - type: textarea + id: prop-solution + attributes: + label: Describe the solution you'd like. + description: A clear and concise description of what you want to happen. + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Describe alternatives you've considered. + description: A clear and concise description of any alternative solutions or features you've considered. If nothing, please enter `NONE` + validations: + required: true + + - type: textarea + id: additional-ctxt + attributes: + label: Additional context. + description: Add any other context or screenshots about the feature request here. + validations: + required: false \ No newline at end of file From 327d3f001f1cc219c4a718edf23b976c29c19487 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Fri, 3 Jun 2022 15:40:38 +0200 Subject: [PATCH 02/13] [feature] Start adding advanced configuration options, starting with `samesite` (#628) * fix incorrect port being used for db * start adding advanced config flags * use samesite lax by default --- docs/configuration/advanced.md | 38 ++++++++++++++++++++++++++++++++++ example/config.yaml | 27 ++++++++++++++++++++++++ internal/config/config.go | 2 ++ internal/config/defaults.go | 2 ++ internal/config/flags.go | 3 +++ internal/config/helpers.gen.go | 25 ++++++++++++++++++++++ internal/db/bundb/bundb.go | 2 +- internal/router/session.go | 29 ++++++++++++++++++++------ mkdocs.yml | 1 + test/cliparsing.sh | 22 ++++++++++---------- testrig/config.go | 2 ++ 11 files changed, 135 insertions(+), 18 deletions(-) create mode 100644 docs/configuration/advanced.md diff --git a/docs/configuration/advanced.md b/docs/configuration/advanced.md new file mode 100644 index 000000000..48b1b569d --- /dev/null +++ b/docs/configuration/advanced.md @@ -0,0 +1,38 @@ +# Advanced + +Advanced settings options are provided for the sake of allowing admins to tune their instance to their liking. + +These are set to sensible defaults, so most server admins won't need to touch them or think about them. + +**Changing these settings if you don't know what you're doing may break your instance**. + +## Settings + +```yaml +############################# +##### ADVANCED SETTINGS ##### +############################# + +# Advanced settings pertaining to http timeouts, security, cookies, and more. +# +# ONLY ADJUST THESE SETTINGS IF YOU KNOW WHAT YOU ARE DOING! +# +# Most users will not need to (and should not) touch these settings, since +# they are set to sensible defaults, and may break if they are changed. +# +# Nevertheless, they are provided for the sake of allowing server admins to +# tweak their instance for performance or security reasons. + +# String. Value of the SameSite attribute of cookies set by GoToSocial. +# Defaults to 'lax' to ensure that the OIDC flow does not break, which is +# fine in most cases. If you want to harden your instance against CSRF attacks +# and don't mind if some login-related things might break, you can set this +# to 'strict' instead. +# +# For an overview of what this does, see: +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite +# +# Options: ["lax", "strict"] +# Default: "lax" +advanced-cookies-samesite: "lax" +``` diff --git a/example/config.yaml b/example/config.yaml index 1a8eb8939..f442b62bf 100644 --- a/example/config.yaml +++ b/example/config.yaml @@ -428,3 +428,30 @@ syslog-protocol: "udp" # String. Address:port to send syslog logs to. Leave empty to connect to local syslog. # Default: "localhost:514" syslog-address: "localhost:514" + +############################# +##### ADVANCED SETTINGS ##### +############################# + +# Advanced settings pertaining to http timeouts, security, cookies, and more. +# +# ONLY ADJUST THESE SETTINGS IF YOU KNOW WHAT YOU ARE DOING! +# +# Most users will not need to (and should not) touch these settings, since +# they are set to sensible defaults, and may break if they are changed. +# +# Nevertheless, they are provided for the sake of allowing server admins to +# tweak their instance for performance or security reasons. + +# String. Value of the SameSite attribute of cookies set by GoToSocial. +# Defaults to 'lax' to ensure that the OIDC flow does not break, which is +# fine in most cases. If you want to harden your instance against CSRF attacks +# and don't mind if some login-related things might break, you can set this +# to 'strict' instead. +# +# For an overview of what this does, see: +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite +# +# Options: ["lax", "strict"] +# Default: "lax" +advanced-cookies-samesite: "lax" diff --git a/internal/config/config.go b/internal/config/config.go index 431fbbe4d..573f2b3a2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -114,6 +114,8 @@ type Configuration struct { AdminAccountEmail string `name:"email" usage:"the email address of this account"` AdminAccountPassword string `name:"password" usage:"the password to set for this account"` AdminTransPath string `name:"path" usage:"the path of the file to import from/export to"` + + AdvancedCookiesSamesite string `name:"advanced-cookies-samesite" usage:"'strict' or 'lax', see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite"` } // MarshalMap will marshal current Configuration into a map structure (useful for JSON). diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 75af21222..f9f0bdba9 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -87,4 +87,6 @@ SyslogEnabled: false, SyslogProtocol: "udp", SyslogAddress: "localhost:514", + + AdvancedCookiesSamesite: "lax", } diff --git a/internal/config/flags.go b/internal/config/flags.go index 0be372551..6f946f1d6 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -109,6 +109,9 @@ func AddServerFlags(cmd *cobra.Command) { cmd.Flags().Bool(SyslogEnabledFlag(), cfg.SyslogEnabled, fieldtag("SyslogEnabled", "usage")) cmd.Flags().String(SyslogProtocolFlag(), cfg.SyslogProtocol, fieldtag("SyslogProtocol", "usage")) cmd.Flags().String(SyslogAddressFlag(), cfg.SyslogAddress, fieldtag("SyslogAddress", "usage")) + + // Advanced flags + cmd.Flags().String(AdvancedCookiesSamesiteFlag(), cfg.AdvancedCookiesSamesite, fieldtag("AdvancedCookiesSamesite", "usage")) }) } diff --git a/internal/config/helpers.gen.go b/internal/config/helpers.gen.go index a90199495..a48828059 100644 --- a/internal/config/helpers.gen.go +++ b/internal/config/helpers.gen.go @@ -1492,3 +1492,28 @@ func GetAdminTransPath() string { return global.GetAdminTransPath() } // SetAdminTransPath safely sets the value for global configuration 'AdminTransPath' field func SetAdminTransPath(v string) { global.SetAdminTransPath(v) } + +// GetAdvancedCookiesSamesite safely fetches the Configuration value for state's 'AdvancedCookiesSamesite' field +func (st *ConfigState) GetAdvancedCookiesSamesite() (v string) { + st.mutex.Lock() + v = st.config.AdvancedCookiesSamesite + st.mutex.Unlock() + return +} + +// SetAdvancedCookiesSamesite safely sets the Configuration value for state's 'AdvancedCookiesSamesite' field +func (st *ConfigState) SetAdvancedCookiesSamesite(v string) { + st.mutex.Lock() + defer st.mutex.Unlock() + st.config.AdvancedCookiesSamesite = v + st.reloadToViper() +} + +// AdvancedCookiesSamesiteFlag returns the flag name for the 'AdvancedCookiesSamesite' field +func AdvancedCookiesSamesiteFlag() string { return "advanced-cookies-samesite" } + +// GetAdvancedCookiesSamesite safely fetches the value for global configuration 'AdvancedCookiesSamesite' field +func GetAdvancedCookiesSamesite() string { return global.GetAdvancedCookiesSamesite() } + +// SetAdvancedCookiesSamesite safely sets the value for global configuration 'AdvancedCookiesSamesite' field +func SetAdvancedCookiesSamesite(v string) { global.SetAdvancedCookiesSamesite(v) } diff --git a/internal/db/bundb/bundb.go b/internal/db/bundb/bundb.go index fef62a55f..6328ca34f 100644 --- a/internal/db/bundb/bundb.go +++ b/internal/db/bundb/bundb.go @@ -348,7 +348,7 @@ func deriveBunDBPGOptions() (*pgx.ConnConfig, error) { if address != "" { cfg.Host = address } - if port := config.GetPort(); port > 0 { + if port := config.GetDbPort(); port > 0 { cfg.Port = uint16(port) } if u := config.GetDbUser(); u != "" { diff --git a/internal/router/session.go b/internal/router/session.go index f94b0a22a..b49542428 100644 --- a/internal/router/session.go +++ b/internal/router/session.go @@ -24,10 +24,12 @@ "fmt" "net/http" "net/url" + "strings" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/memstore" "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "golang.org/x/net/idna" @@ -35,13 +37,28 @@ // SessionOptions returns the standard set of options to use for each session. func SessionOptions() sessions.Options { + var samesite http.SameSite + switch strings.TrimSpace(strings.ToLower(config.GetAdvancedCookiesSamesite())) { + case "lax": + samesite = http.SameSiteLaxMode + case "strict": + samesite = http.SameSiteStrictMode + default: + logrus.Warnf("%s set to %s which is not recognized, defaulting to 'lax'", config.AdvancedCookiesSamesiteFlag(), config.GetAdvancedCookiesSamesite()) + samesite = http.SameSiteLaxMode + } + return sessions.Options{ - Path: "/", - Domain: config.GetHost(), - MaxAge: 120, // 2 minutes - Secure: config.GetProtocol() == "https", // only use cookie over https - HttpOnly: true, // exclude javascript from inspecting cookie - SameSite: http.SameSiteStrictMode, // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-same-site-00#section-4.1.1 + Path: "/", + Domain: config.GetHost(), + // 2 minutes + MaxAge: 120, + // only set secure over https + Secure: config.GetProtocol() == "https", + // forbid javascript from inspecting cookie + HttpOnly: true, + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-same-site-00#section-4.1.1 + SameSite: samesite, } } diff --git a/mkdocs.yml b/mkdocs.yml index 2f5243333..2ed4edcb7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -35,6 +35,7 @@ nav: - "configuration/oidc.md" - "configuration/smtp.md" - "configuration/syslog.md" + - "configuration/advanced.md" - "Admin": - "admin/admin_panel.md" - "admin/cli.md" diff --git a/test/cliparsing.sh b/test/cliparsing.sh index 98fc6fd0f..0cf83a82d 100755 --- a/test/cliparsing.sh +++ b/test/cliparsing.sh @@ -5,7 +5,7 @@ set -e echo "STARTING CLI TESTS" echo "TEST_1 Make sure defaults are set correctly." -TEST_1_EXPECTED='{"account-domain":"","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"","db-address":"","db-database":"gotosocial","db-password":"","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"","email":"","host":"","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":false,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":2097152,"media-remote-cache-days":30,"media-video-max-size":10485760,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","profile","email","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"GoToSocial","smtp-host":"","smtp-password":"","smtp-port":0,"smtp-username":"","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' +TEST_1_EXPECTED='{"account-domain":"","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"","db-address":"","db-database":"gotosocial","db-password":"","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"","email":"","host":"","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":false,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":2097152,"media-remote-cache-days":30,"media-video-max-size":10485760,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","profile","email","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"GoToSocial","smtp-host":"","smtp-password":"","smtp-port":0,"smtp-username":"","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' TEST_1="$(go run ./cmd/gotosocial/... debug config)" if [ "${TEST_1}" != "${TEST_1_EXPECTED}" ]; then echo "TEST_1 not equal TEST_1_EXPECTED" @@ -15,7 +15,7 @@ else fi echo "TEST_2 Override db-address from default using cli flag." -TEST_2_EXPECTED='{"account-domain":"","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"","db-address":"some.db.address","db-database":"gotosocial","db-password":"","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"","email":"","host":"","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":false,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":2097152,"media-remote-cache-days":30,"media-video-max-size":10485760,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","profile","email","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"GoToSocial","smtp-host":"","smtp-password":"","smtp-port":0,"smtp-username":"","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' +TEST_2_EXPECTED='{"account-domain":"","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"","db-address":"some.db.address","db-database":"gotosocial","db-password":"","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"","email":"","host":"","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":false,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":2097152,"media-remote-cache-days":30,"media-video-max-size":10485760,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","profile","email","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"GoToSocial","smtp-host":"","smtp-password":"","smtp-port":0,"smtp-username":"","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' TEST_2="$(go run ./cmd/gotosocial/... --db-address some.db.address debug config)" if [ "${TEST_2}" != "${TEST_2_EXPECTED}" ]; then echo "TEST_2 not equal TEST_2_EXPECTED" @@ -25,7 +25,7 @@ else fi echo "TEST_3 Override db-address from default using env var." -TEST_3_EXPECTED='{"account-domain":"","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"","db-address":"some.db.address","db-database":"gotosocial","db-password":"","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"","email":"","host":"","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":false,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":2097152,"media-remote-cache-days":30,"media-video-max-size":10485760,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","profile","email","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"GoToSocial","smtp-host":"","smtp-password":"","smtp-port":0,"smtp-username":"","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' +TEST_3_EXPECTED='{"account-domain":"","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"","db-address":"some.db.address","db-database":"gotosocial","db-password":"","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"","email":"","host":"","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":false,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":2097152,"media-remote-cache-days":30,"media-video-max-size":10485760,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","profile","email","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"GoToSocial","smtp-host":"","smtp-password":"","smtp-port":0,"smtp-username":"","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' TEST_3="$(GTS_DB_ADDRESS=some.db.address go run ./cmd/gotosocial/... debug config)" if [ "${TEST_3}" != "${TEST_3_EXPECTED}" ]; then echo "TEST_3 not equal TEST_3_EXPECTED" @@ -35,7 +35,7 @@ else fi echo "TEST_4 Override db-address from default using both env var and cli flag. The cli flag should take priority." -TEST_4_EXPECTED='{"account-domain":"","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"","db-address":"some.other.db.address","db-database":"gotosocial","db-password":"","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"","email":"","host":"","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":false,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":2097152,"media-remote-cache-days":30,"media-video-max-size":10485760,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","profile","email","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"GoToSocial","smtp-host":"","smtp-password":"","smtp-port":0,"smtp-username":"","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' +TEST_4_EXPECTED='{"account-domain":"","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"","db-address":"some.other.db.address","db-database":"gotosocial","db-password":"","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"","email":"","host":"","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":false,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":2097152,"media-remote-cache-days":30,"media-video-max-size":10485760,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","profile","email","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"GoToSocial","smtp-host":"","smtp-password":"","smtp-port":0,"smtp-username":"","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' TEST_4="$(GTS_DB_ADDRESS=some.db.address go run ./cmd/gotosocial/... --db-address some.other.db.address debug config)" if [ "${TEST_4}" != "${TEST_4_EXPECTED}" ]; then echo "TEST_4 not equal TEST_4_EXPECTED" @@ -45,7 +45,7 @@ else fi echo "TEST_5 Test loading a config file by passing an env var." -TEST_5_EXPECTED='{"account-domain":"example.org","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test.yaml","db-address":"127.0.0.1","db-database":"postgres","db-password":"postgres","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"postgres","email":"","host":"gts.example.org","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":2097152,"media-remote-cache-days":30,"media-video-max-size":10485760,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","email","profile","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"someone@example.org","smtp-host":"verycoolemailhost.mail","smtp-password":"smtp-password","smtp-port":8888,"smtp-username":"smtp-username","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' +TEST_5_EXPECTED='{"account-domain":"example.org","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test.yaml","db-address":"127.0.0.1","db-database":"postgres","db-password":"postgres","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"postgres","email":"","host":"gts.example.org","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":2097152,"media-remote-cache-days":30,"media-video-max-size":10485760,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","email","profile","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"someone@example.org","smtp-host":"verycoolemailhost.mail","smtp-password":"smtp-password","smtp-port":8888,"smtp-username":"smtp-username","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' TEST_5="$(GTS_CONFIG_PATH=./test/test.yaml go run ./cmd/gotosocial/... debug config)" if [ "${TEST_5}" != "${TEST_5_EXPECTED}" ]; then echo "TEST_5 not equal TEST_5_EXPECTED" @@ -55,7 +55,7 @@ else fi echo "TEST_6 Test loading a config file by passing cli flag." -TEST_6_EXPECTED='{"account-domain":"example.org","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test.yaml","db-address":"127.0.0.1","db-database":"postgres","db-password":"postgres","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"postgres","email":"","host":"gts.example.org","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":2097152,"media-remote-cache-days":30,"media-video-max-size":10485760,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","email","profile","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"someone@example.org","smtp-host":"verycoolemailhost.mail","smtp-password":"smtp-password","smtp-port":8888,"smtp-username":"smtp-username","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' +TEST_6_EXPECTED='{"account-domain":"example.org","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test.yaml","db-address":"127.0.0.1","db-database":"postgres","db-password":"postgres","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"postgres","email":"","host":"gts.example.org","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":2097152,"media-remote-cache-days":30,"media-video-max-size":10485760,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","email","profile","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"someone@example.org","smtp-host":"verycoolemailhost.mail","smtp-password":"smtp-password","smtp-port":8888,"smtp-username":"smtp-username","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' TEST_6="$(go run ./cmd/gotosocial/... --config-path ./test/test.yaml debug config)" if [ "${TEST_6}" != "${TEST_6_EXPECTED}" ]; then echo "TEST_6 not equal TEST_6_EXPECTED" @@ -65,7 +65,7 @@ else fi echo "TEST_7 Test loading a config file and overriding one of the variables with a cli flag." -TEST_7_EXPECTED='{"account-domain":"","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test.yaml","db-address":"127.0.0.1","db-database":"postgres","db-password":"postgres","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"postgres","email":"","host":"gts.example.org","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":2097152,"media-remote-cache-days":30,"media-video-max-size":10485760,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","email","profile","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"someone@example.org","smtp-host":"verycoolemailhost.mail","smtp-password":"smtp-password","smtp-port":8888,"smtp-username":"smtp-username","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' +TEST_7_EXPECTED='{"account-domain":"","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test.yaml","db-address":"127.0.0.1","db-database":"postgres","db-password":"postgres","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"postgres","email":"","host":"gts.example.org","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":2097152,"media-remote-cache-days":30,"media-video-max-size":10485760,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","email","profile","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"someone@example.org","smtp-host":"verycoolemailhost.mail","smtp-password":"smtp-password","smtp-port":8888,"smtp-username":"smtp-username","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' TEST_7="$(go run ./cmd/gotosocial/... --config-path ./test/test.yaml --account-domain '' debug config)" if [ "${TEST_7}" != "${TEST_7_EXPECTED}" ]; then echo "TEST_7 not equal TEST_7_EXPECTED" @@ -75,7 +75,7 @@ else fi echo "TEST_8 Test loading a config file and overriding one of the variables with an env var." -TEST_8_EXPECTED='{"account-domain":"peepee","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test.yaml","db-address":"127.0.0.1","db-database":"postgres","db-password":"postgres","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"postgres","email":"","host":"gts.example.org","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":2097152,"media-remote-cache-days":30,"media-video-max-size":10485760,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","email","profile","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"someone@example.org","smtp-host":"verycoolemailhost.mail","smtp-password":"smtp-password","smtp-port":8888,"smtp-username":"smtp-username","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' +TEST_8_EXPECTED='{"account-domain":"peepee","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test.yaml","db-address":"127.0.0.1","db-database":"postgres","db-password":"postgres","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"postgres","email":"","host":"gts.example.org","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":2097152,"media-remote-cache-days":30,"media-video-max-size":10485760,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","email","profile","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"someone@example.org","smtp-host":"verycoolemailhost.mail","smtp-password":"smtp-password","smtp-port":8888,"smtp-username":"smtp-username","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' TEST_8="$(GTS_ACCOUNT_DOMAIN='peepee' go run ./cmd/gotosocial/... --config-path ./test/test.yaml debug config)" if [ "${TEST_8}" != "${TEST_8_EXPECTED}" ]; then echo "TEST_8 not equal TEST_8_EXPECTED" @@ -85,7 +85,7 @@ else fi echo "TEST_9 Test loading a config file and overriding one of the variables with both an env var and a cli flag. The cli flag should have priority." -TEST_9_EXPECTED='{"account-domain":"","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test.yaml","db-address":"127.0.0.1","db-database":"postgres","db-password":"postgres","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"postgres","email":"","host":"gts.example.org","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":2097152,"media-remote-cache-days":30,"media-video-max-size":10485760,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","email","profile","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"someone@example.org","smtp-host":"verycoolemailhost.mail","smtp-password":"smtp-password","smtp-port":8888,"smtp-username":"smtp-username","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' +TEST_9_EXPECTED='{"account-domain":"","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test.yaml","db-address":"127.0.0.1","db-database":"postgres","db-password":"postgres","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"postgres","email":"","host":"gts.example.org","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":2097152,"media-remote-cache-days":30,"media-video-max-size":10485760,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","email","profile","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"someone@example.org","smtp-host":"verycoolemailhost.mail","smtp-password":"smtp-password","smtp-port":8888,"smtp-username":"smtp-username","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' TEST_9="$(GTS_ACCOUNT_DOMAIN='peepee' go run ./cmd/gotosocial/... --config-path ./test/test.yaml --account-domain '' debug config)" if [ "${TEST_9}" != "${TEST_9_EXPECTED}" ]; then echo "TEST_9 not equal TEST_9_EXPECTED" @@ -95,7 +95,7 @@ else fi echo "TEST_10 Test loading a config file from json." -TEST_10_EXPECTED='{"account-domain":"example.org","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test.json","db-address":"127.0.0.1","db-database":"postgres","db-password":"postgres","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"postgres","email":"","host":"gts.example.org","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":2097152,"media-remote-cache-days":30,"media-video-max-size":10485760,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","email","profile","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"someone@example.org","smtp-host":"verycoolemailhost.mail","smtp-password":"smtp-password","smtp-port":8888,"smtp-username":"smtp-username","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' +TEST_10_EXPECTED='{"account-domain":"example.org","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test.json","db-address":"127.0.0.1","db-database":"postgres","db-password":"postgres","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"postgres","email":"","host":"gts.example.org","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":2097152,"media-remote-cache-days":30,"media-video-max-size":10485760,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","email","profile","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"someone@example.org","smtp-host":"verycoolemailhost.mail","smtp-password":"smtp-password","smtp-port":8888,"smtp-username":"smtp-username","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' TEST_10="$(go run ./cmd/gotosocial/... --config-path ./test/test.json debug config)" if [ "${TEST_10}" != "${TEST_10_EXPECTED}" ]; then echo "TEST_10 not equal TEST_10_EXPECTED" @@ -105,7 +105,7 @@ else fi echo "TEST_11 Test loading a partial config file. Default values should be used apart from those set in the config file." -TEST_11_EXPECTED='{"account-domain":"peepee.poopoo","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test2.yaml","db-address":"","db-database":"gotosocial","db-password":"","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"","email":"","host":"","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":false,"letsencrypt-port":80,"log-db-queries":false,"log-level":"trace","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":2097152,"media-remote-cache-days":30,"media-video-max-size":10485760,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","profile","email","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"GoToSocial","smtp-host":"","smtp-password":"","smtp-port":0,"smtp-username":"","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' +TEST_11_EXPECTED='{"account-domain":"peepee.poopoo","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test2.yaml","db-address":"","db-database":"gotosocial","db-password":"","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"","email":"","host":"","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":false,"letsencrypt-port":80,"log-db-queries":false,"log-level":"trace","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":2097152,"media-remote-cache-days":30,"media-video-max-size":10485760,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","profile","email","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"GoToSocial","smtp-host":"","smtp-password":"","smtp-port":0,"smtp-username":"","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' TEST_11="$(go run ./cmd/gotosocial/... --config-path ./test/test2.yaml debug config)" if [ "${TEST_11}" != "${TEST_11_EXPECTED}" ]; then echo "TEST_11 not equal TEST_11_EXPECTED" diff --git a/testrig/config.go b/testrig/config.go index 64b8649aa..e424670fa 100644 --- a/testrig/config.go +++ b/testrig/config.go @@ -94,4 +94,6 @@ func InitTestConfig() { SyslogEnabled: false, SyslogProtocol: "udp", SyslogAddress: "localhost:514", + + AdvancedCookiesSamesite: "lax", } From f3b44426f4bdc18ef14f68bb8381646fdc2ef241 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Sat, 4 Jun 2022 20:03:01 +0200 Subject: [PATCH 03/13] [bugfix] Update time marshalling format to provide 3 digits of ms (#630) --- internal/api/client/followrequest/get_test.go | 2 +- internal/api/client/instance/instancepatch_test.go | 2 +- internal/processing/streaming/notification_test.go | 4 ++-- internal/typeutils/internaltofrontend_test.go | 6 +++--- internal/util/time.go | 2 +- internal/util/time_test.go | 6 +++--- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/api/client/followrequest/get_test.go b/internal/api/client/followrequest/get_test.go index 63e4d0818..2af9c7744 100644 --- a/internal/api/client/followrequest/get_test.go +++ b/internal/api/client/followrequest/get_test.go @@ -70,7 +70,7 @@ func (suite *GetTestSuite) TestGet() { b, err := ioutil.ReadAll(result.Body) assert.NoError(suite.T(), err) - suite.Equal(`[{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","username":"some_user","acct":"some_user@example.org","display_name":"some user","locked":true,"bot":false,"created_at":"2020-08-10T12:13:28.00Z","note":"i'm a real son of a gun","url":"http://example.org/@some_user","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":0,"following_count":0,"statuses_count":0,"last_status_at":"","emojis":[],"fields":[]}]`, string(b)) + suite.Equal(`[{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","username":"some_user","acct":"some_user@example.org","display_name":"some user","locked":true,"bot":false,"created_at":"2020-08-10T12:13:28.000Z","note":"i'm a real son of a gun","url":"http://example.org/@some_user","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":0,"following_count":0,"statuses_count":0,"last_status_at":"","emojis":[],"fields":[]}]`, string(b)) } func TestGetTestSuite(t *testing.T) { diff --git a/internal/api/client/instance/instancepatch_test.go b/internal/api/client/instance/instancepatch_test.go index 68243ded8..891a9da21 100644 --- a/internal/api/client/instance/instancepatch_test.go +++ b/internal/api/client/instance/instancepatch_test.go @@ -62,7 +62,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() { b, err := io.ReadAll(result.Body) suite.NoError(err) - suite.Equal(`{"uri":"http://localhost:8080","title":"Example Instance","description":"","short_description":"","email":"someone@example.org","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":0,"status_count":16,"user_count":4},"thumbnail":"","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.00Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.00Z","emojis":[],"fields":[]},"max_toot_chars":5000}`, string(b)) + suite.Equal(`{"uri":"http://localhost:8080","title":"Example Instance","description":"","short_description":"","email":"someone@example.org","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":0,"status_count":16,"user_count":4},"thumbnail":"","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[]},"max_toot_chars":5000}`, string(b)) } func (suite *InstancePatchTestSuite) TestInstancePatch2() { diff --git a/internal/processing/streaming/notification_test.go b/internal/processing/streaming/notification_test.go index 206fab7b8..960d9408d 100644 --- a/internal/processing/streaming/notification_test.go +++ b/internal/processing/streaming/notification_test.go @@ -44,7 +44,7 @@ func (suite *NotificationTestSuite) TestStreamNotification() { notification := &apimodel.Notification{ ID: "01FH57SJCMDWQGEAJ0X08CE3WV", Type: "follow", - CreatedAt: "2021-10-04T08:52:36.00Z", + CreatedAt: "2021-10-04T08:52:36.000Z", Account: followAccountAPIModel, } @@ -52,7 +52,7 @@ func (suite *NotificationTestSuite) TestStreamNotification() { suite.NoError(err) msg := <-openStream.Messages - suite.Equal(`{"id":"01FH57SJCMDWQGEAJ0X08CE3WV","type":"follow","created_at":"2021-10-04T08:52:36.00Z","account":{"id":"01F8MH5ZK5VRH73AKHQM6Y9VNX","username":"foss_satan","acct":"foss_satan@fossbros-anonymous.io","display_name":"big gerald","locked":false,"bot":false,"created_at":"2021-09-26T10:52:36.00Z","note":"i post about like, i dunno, stuff, or whatever!!!!","url":"http://fossbros-anonymous.io/@foss_satan","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":0,"following_count":0,"statuses_count":1,"last_status_at":"2021-09-20T10:40:37.00Z","emojis":[],"fields":[]}}`, msg.Payload) + suite.Equal(`{"id":"01FH57SJCMDWQGEAJ0X08CE3WV","type":"follow","created_at":"2021-10-04T08:52:36.000Z","account":{"id":"01F8MH5ZK5VRH73AKHQM6Y9VNX","username":"foss_satan","acct":"foss_satan@fossbros-anonymous.io","display_name":"big gerald","locked":false,"bot":false,"created_at":"2021-09-26T10:52:36.000Z","note":"i post about like, i dunno, stuff, or whatever!!!!","url":"http://fossbros-anonymous.io/@foss_satan","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":0,"following_count":0,"statuses_count":1,"last_status_at":"2021-09-20T10:40:37.000Z","emojis":[],"fields":[]}}`, msg.Payload) } func TestNotificationTestSuite(t *testing.T) { diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go index 86b39451e..0142436a5 100644 --- a/internal/typeutils/internaltofrontend_test.go +++ b/internal/typeutils/internaltofrontend_test.go @@ -40,7 +40,7 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontend() { b, err := json.Marshal(apiAccount) suite.NoError(err) - suite.Equal(`{"id":"01F8MH1H7YV1Z7D2C8K2730QBF","username":"the_mighty_zork","acct":"the_mighty_zork","display_name":"original zork (he/they)","locked":false,"bot":false,"created_at":"2022-05-20T11:09:18.00Z","note":"\u003cp\u003ehey yo this is my profile!\u003c/p\u003e","url":"http://localhost:8080/@the_mighty_zork","avatar":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpeg","avatar_static":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpeg","header":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg","header_static":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg","followers_count":2,"following_count":2,"statuses_count":5,"last_status_at":"2022-05-20T11:37:55.00Z","emojis":[],"fields":[]}`, string(b)) + suite.Equal(`{"id":"01F8MH1H7YV1Z7D2C8K2730QBF","username":"the_mighty_zork","acct":"the_mighty_zork","display_name":"original zork (he/they)","locked":false,"bot":false,"created_at":"2022-05-20T11:09:18.000Z","note":"\u003cp\u003ehey yo this is my profile!\u003c/p\u003e","url":"http://localhost:8080/@the_mighty_zork","avatar":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpeg","avatar_static":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpeg","header":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg","header_static":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg","followers_count":2,"following_count":2,"statuses_count":5,"last_status_at":"2022-05-20T11:37:55.000Z","emojis":[],"fields":[]}`, string(b)) } func (suite *InternalToFrontendTestSuite) TestStatusToFrontend() { @@ -52,7 +52,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontend() { b, err := json.Marshal(apiStatus) suite.NoError(err) - suite.Equal(`{"id":"01F8MH75CBF9JFX4ZAD54N0W0R","created_at":"2021-10-20T11:36:45.00Z","in_reply_to_id":"","in_reply_to_account_id":"","sensitive":false,"spoiler_text":"","visibility":"public","language":"en","uri":"http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R","url":"http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R","replies_count":0,"reblogs_count":0,"favourites_count":1,"favourited":true,"reblogged":false,"muted":false,"bookmarked":false,"pinned":false,"content":"hello world! #welcome ! first post on the instance :rainbow: !","reblog":null,"application":{"name":"superseriousbusiness","website":"https://superserious.business"},"account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.00Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.00Z","emojis":[],"fields":[]},"media_attachments":[{"id":"01F8MH6NEM8D7527KZAECTCR76","type":"image","url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpeg","text_url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpeg","preview_url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/small/01F8MH6NEM8D7527KZAECTCR76.jpeg","remote_url":"","preview_remote_url":"","meta":{"original":{"width":1200,"height":630,"size":"1200x630","aspect":1.9047619},"small":{"width":256,"height":134,"size":"256x134","aspect":1.9104477},"focus":{"x":0,"y":0}},"description":"Black and white image of some 50's style text saying: Welcome On Board","blurhash":"LNJRdVM{00Rj%Mayt7j[4nWBofRj"}],"mentions":[],"tags":[{"name":"welcome","url":"http://localhost:8080/tags/welcome"}],"emojis":[{"shortcode":"rainbow","url":"http://localhost:8080/fileserver/01F8MH261H1KSV3GW3016GZRY3/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png","static_url":"http://localhost:8080/fileserver/01F8MH261H1KSV3GW3016GZRY3/emoji/static/01F8MH9H8E4VG3KDYJR9EGPXCQ.png","visible_in_picker":true}],"card":null,"poll":null,"text":""}`, string(b)) + suite.Equal(`{"id":"01F8MH75CBF9JFX4ZAD54N0W0R","created_at":"2021-10-20T11:36:45.000Z","in_reply_to_id":"","in_reply_to_account_id":"","sensitive":false,"spoiler_text":"","visibility":"public","language":"en","uri":"http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R","url":"http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R","replies_count":0,"reblogs_count":0,"favourites_count":1,"favourited":true,"reblogged":false,"muted":false,"bookmarked":false,"pinned":false,"content":"hello world! #welcome ! first post on the instance :rainbow: !","reblog":null,"application":{"name":"superseriousbusiness","website":"https://superserious.business"},"account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[]},"media_attachments":[{"id":"01F8MH6NEM8D7527KZAECTCR76","type":"image","url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpeg","text_url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpeg","preview_url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/small/01F8MH6NEM8D7527KZAECTCR76.jpeg","remote_url":"","preview_remote_url":"","meta":{"original":{"width":1200,"height":630,"size":"1200x630","aspect":1.9047619},"small":{"width":256,"height":134,"size":"256x134","aspect":1.9104477},"focus":{"x":0,"y":0}},"description":"Black and white image of some 50's style text saying: Welcome On Board","blurhash":"LNJRdVM{00Rj%Mayt7j[4nWBofRj"}],"mentions":[],"tags":[{"name":"welcome","url":"http://localhost:8080/tags/welcome"}],"emojis":[{"shortcode":"rainbow","url":"http://localhost:8080/fileserver/01F8MH261H1KSV3GW3016GZRY3/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png","static_url":"http://localhost:8080/fileserver/01F8MH261H1KSV3GW3016GZRY3/emoji/static/01F8MH9H8E4VG3KDYJR9EGPXCQ.png","visible_in_picker":true}],"card":null,"poll":null,"text":""}`, string(b)) } func (suite *InternalToFrontendTestSuite) TestInstanceToFrontend() { @@ -97,7 +97,7 @@ func (suite *InternalToFrontendTestSuite) TestInstanceToFrontendWithAdminAccount b, err := json.Marshal(apiInstance) suite.NoError(err) - suite.Equal(`{"uri":"https://example.org","title":"example instance","description":"a much longer description","short_description":"a little description","email":"someone@example.org","version":"software-from-hell 0.666","registrations":false,"approval_required":false,"invites_enabled":false,"thumbnail":"","contact_account":{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","username":"some_user","acct":"some_user@example.org","display_name":"some user","locked":true,"bot":false,"created_at":"2020-08-10T12:13:28.00Z","note":"i'm a real son of a gun","url":"http://example.org/@some_user","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":0,"following_count":0,"statuses_count":0,"last_status_at":"","emojis":[],"fields":[]},"max_toot_chars":0}`, string(b)) + suite.Equal(`{"uri":"https://example.org","title":"example instance","description":"a much longer description","short_description":"a little description","email":"someone@example.org","version":"software-from-hell 0.666","registrations":false,"approval_required":false,"invites_enabled":false,"thumbnail":"","contact_account":{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","username":"some_user","acct":"some_user@example.org","display_name":"some user","locked":true,"bot":false,"created_at":"2020-08-10T12:13:28.000Z","note":"i'm a real son of a gun","url":"http://example.org/@some_user","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":0,"following_count":0,"statuses_count":0,"last_status_at":"","emojis":[],"fields":[]},"max_toot_chars":0}`, string(b)) } func TestInternalToFrontendTestSuite(t *testing.T) { diff --git a/internal/util/time.go b/internal/util/time.go index ec24853fb..f969908a7 100644 --- a/internal/util/time.go +++ b/internal/util/time.go @@ -21,7 +21,7 @@ import "time" // ISO8601 is a formatter for serializing times that forces ISO8601 behavior. -const ISO8601 = "2006-01-02T15:04:05.00Z" +const ISO8601 = "2006-01-02T15:04:05.000Z" // FormatISO8601 converts the given time to UTC and then formats it // using the ISO8601 const, which the Mastodon API is able to understand. diff --git a/internal/util/time_test.go b/internal/util/time_test.go index 5343c21a4..5f75b09bc 100644 --- a/internal/util/time_test.go +++ b/internal/util/time_test.go @@ -33,19 +33,19 @@ type TimeSuite struct { func (suite *TimeSuite) TestISO8601Format1() { testTime := testrig.TimeMustParse("2022-05-17T13:10:59Z") testTimeString := util.FormatISO8601(testTime) - suite.Equal("2022-05-17T13:10:59.00Z", testTimeString) + suite.Equal("2022-05-17T13:10:59.000Z", testTimeString) } func (suite *TimeSuite) TestISO8601Format2() { testTime := testrig.TimeMustParse("2022-05-09T07:34:35+02:00") testTimeString := util.FormatISO8601(testTime) - suite.Equal("2022-05-09T05:34:35.00Z", testTimeString) + suite.Equal("2022-05-09T05:34:35.000Z", testTimeString) } func (suite *TimeSuite) TestISO8601Format3() { testTime := testrig.TimeMustParse("2021-10-04T10:52:36+02:00") testTimeString := util.FormatISO8601(testTime) - suite.Equal("2021-10-04T08:52:36.00Z", testTimeString) + suite.Equal("2021-10-04T08:52:36.000Z", testTimeString) } func TestTimeSuite(t *testing.T) { From 99fab9c6b62a58e5d93e9b7cbcd17215a93139a1 Mon Sep 17 00:00:00 2001 From: f0x52 Date: Wed, 8 Jun 2022 02:22:52 +0200 Subject: [PATCH 04/13] [frontend] linkify header mascot+title (#633) --- web/assets/base.css | 1 + web/gotosocial-styling/templates/base.css | 1 + web/template/header.tmpl | 20 +++++++++++--------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/web/assets/base.css b/web/assets/base.css index eb49cc3ab..ba593b474 100644 --- a/web/assets/base.css +++ b/web/assets/base.css @@ -65,6 +65,7 @@ header div { header div h1 { align-self: center; + color: #fafaff; } h1 { diff --git a/web/gotosocial-styling/templates/base.css b/web/gotosocial-styling/templates/base.css index 1afc2be6f..0e1ea23eb 100644 --- a/web/gotosocial-styling/templates/base.css +++ b/web/gotosocial-styling/templates/base.css @@ -63,6 +63,7 @@ header { h1 { align-self: center; + color: $fg; } } } diff --git a/web/template/header.tmpl b/web/template/header.tmpl index 059d353e0..72f17d2a4 100644 --- a/web/template/header.tmpl +++ b/web/template/header.tmpl @@ -16,12 +16,14 @@ {{.instance.Title}} - GoToSocial -
- Instance Logo -
-

- {{.instance.Title}} -

-
-
-
\ No newline at end of file + +
+ Instance Logo +
+

+ {{.instance.Title}} +

+
+
+
+
\ No newline at end of file From 6f6e89e2715c9ecbadda6b8dbe5227995348dae8 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Wed, 8 Jun 2022 20:22:49 +0200 Subject: [PATCH 05/13] [feature] Add paging via `Link` header for notifications and account statuses (#629) * test link headers * page get account statuses properly * page get notifications * add util func for packaging timeline responses * return timelined stuff from accountstatusesget * rename timeline response * use new convenience function * go fmt --- internal/api/client/account/statuses.go | 7 +- internal/api/client/account/statuses_test.go | 44 ++++++- .../api/client/favourites/favouritesget.go | 2 +- .../client/notification/notificationsget.go | 7 +- internal/api/client/timeline/home.go | 2 +- internal/api/client/timeline/public.go | 2 +- internal/api/model/notification.go | 21 ++++ internal/api/model/timeline.go | 8 +- internal/processing/account.go | 2 +- internal/processing/account/account.go | 2 +- internal/processing/account/getstatuses.go | 39 +++++-- internal/processing/notification.go | 21 +++- internal/processing/notification_test.go | 12 +- internal/processing/processor.go | 10 +- internal/processing/statustimeline.go | 107 ++++++++---------- internal/util/timeline.go | 104 +++++++++++++++++ internal/web/profile.go | 11 +- 17 files changed, 303 insertions(+), 98 deletions(-) create mode 100644 internal/util/timeline.go diff --git a/internal/api/client/account/statuses.go b/internal/api/client/account/statuses.go index b440e582a..18b551fcc 100644 --- a/internal/api/client/account/statuses.go +++ b/internal/api/client/account/statuses.go @@ -222,12 +222,15 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { publicOnly = i } - statuses, errWithCode := m.processor.AccountStatusesGet(c.Request.Context(), authed, targetAcctID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly) + resp, errWithCode := m.processor.AccountStatusesGet(c.Request.Context(), authed, targetAcctID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly) if errWithCode != nil { l.Debugf("error from processor account statuses get: %s", errWithCode) c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) return } - c.JSON(http.StatusOK, statuses) + if resp.LinkHeader != "" { + c.Header("Link", resp.LinkHeader) + } + c.JSON(http.StatusOK, resp.Items) } diff --git a/internal/api/client/account/statuses_test.go b/internal/api/client/account/statuses_test.go index 0e95d47fc..1f935896c 100644 --- a/internal/api/client/account/statuses_test.go +++ b/internal/api/client/account/statuses_test.go @@ -37,7 +37,47 @@ type AccountStatusesTestSuite struct { AccountStandardTestSuite } -func (suite *AccountStatusesTestSuite) TestGetStatusesMediaOnly() { +func (suite *AccountStatusesTestSuite) TestGetStatusesPublicOnly() { + // set up the request + // we're getting statuses of admin + targetAccount := suite.testAccounts["admin_account"] + recorder := httptest.NewRecorder() + 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, + Value: targetAccount.ID, + }, + } + + // call the handler + suite.accountModule.AccountStatusesGETHandler(ctx) + + // 1. we should have OK because our request was valid + suite.Equal(http.StatusOK, recorder.Code) + + // 2. we should have no error message in the result body + result := recorder.Result() + defer result.Body.Close() + + // check the response + b, err := ioutil.ReadAll(result.Body) + assert.NoError(suite.T(), err) + + // unmarshal the returned statuses + apimodelStatuses := []*apimodel.Status{} + err = json.Unmarshal(b, &apimodelStatuses) + suite.NoError(err) + suite.NotEmpty(apimodelStatuses) + + for _, s := range apimodelStatuses { + suite.Equal(apimodel.VisibilityPublic, s.Visibility) + } + + suite.Equal(`; rel="next", ; rel="prev"`, result.Header.Get("link")) +} + +func (suite *AccountStatusesTestSuite) TestGetStatusesPublicOnlyMediaOnly() { // set up the request // we're getting statuses of admin targetAccount := suite.testAccounts["admin_account"] @@ -74,6 +114,8 @@ func (suite *AccountStatusesTestSuite) TestGetStatusesMediaOnly() { suite.NotEmpty(s.MediaAttachments) suite.Equal(apimodel.VisibilityPublic, s.Visibility) } + + suite.Equal(`; rel="next", ; rel="prev"`, result.Header.Get("link")) } func TestAccountStatusesTestSuite(t *testing.T) { diff --git a/internal/api/client/favourites/favouritesget.go b/internal/api/client/favourites/favouritesget.go index d112e9b95..5a317b2ea 100644 --- a/internal/api/client/favourites/favouritesget.go +++ b/internal/api/client/favourites/favouritesget.go @@ -61,5 +61,5 @@ func (m *Module) FavouritesGETHandler(c *gin.Context) { if resp.LinkHeader != "" { c.Header("Link", resp.LinkHeader) } - c.JSON(http.StatusOK, resp.Statuses) + c.JSON(http.StatusOK, resp.Items) } diff --git a/internal/api/client/notification/notificationsget.go b/internal/api/client/notification/notificationsget.go index 0a56ee80b..b6f7cdd01 100644 --- a/internal/api/client/notification/notificationsget.go +++ b/internal/api/client/notification/notificationsget.go @@ -74,12 +74,15 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) { sinceID = sinceIDString } - notifs, errWithCode := m.processor.NotificationsGet(c.Request.Context(), authed, limit, maxID, sinceID) + resp, errWithCode := m.processor.NotificationsGet(c.Request.Context(), authed, limit, maxID, sinceID) if errWithCode != nil { l.Debugf("error processing notifications get: %s", errWithCode.Error()) c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) return } - c.JSON(http.StatusOK, notifs) + if resp.LinkHeader != "" { + c.Header("Link", resp.LinkHeader) + } + c.JSON(http.StatusOK, resp.Items) } diff --git a/internal/api/client/timeline/home.go b/internal/api/client/timeline/home.go index dfe1a0db0..47bc2943e 100644 --- a/internal/api/client/timeline/home.go +++ b/internal/api/client/timeline/home.go @@ -171,5 +171,5 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) { if resp.LinkHeader != "" { c.Header("Link", resp.LinkHeader) } - c.JSON(http.StatusOK, resp.Statuses) + c.JSON(http.StatusOK, resp.Items) } diff --git a/internal/api/client/timeline/public.go b/internal/api/client/timeline/public.go index 075d9267c..06da6e947 100644 --- a/internal/api/client/timeline/public.go +++ b/internal/api/client/timeline/public.go @@ -171,5 +171,5 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) { if resp.LinkHeader != "" { c.Header("Link", resp.LinkHeader) } - c.JSON(http.StatusOK, resp.Statuses) + c.JSON(http.StatusOK, resp.Items) } diff --git a/internal/api/model/notification.go b/internal/api/model/notification.go index 8f929cab4..efcd2431b 100644 --- a/internal/api/model/notification.go +++ b/internal/api/model/notification.go @@ -43,3 +43,24 @@ type Notification struct { // Status that was the object of the notification, e.g. in mentions, reblogs, favourites, or polls. Status *Status `json:"status,omitempty"` } + +/* + The below functions are added onto the apimodel notification so that it satisfies + the Timelineable interface in internal/timeline. +*/ + +func (n *Notification) GetID() string { + return n.ID +} + +func (n *Notification) GetAccountID() string { + return "" +} + +func (n *Notification) GetBoostOfID() string { + return "" +} + +func (n *Notification) GetBoostOfAccountID() string { + return "" +} diff --git a/internal/api/model/timeline.go b/internal/api/model/timeline.go index 66a5263b8..71d839ed2 100644 --- a/internal/api/model/timeline.go +++ b/internal/api/model/timeline.go @@ -18,9 +18,11 @@ package model -// StatusTimelineResponse wraps a slice of statuses, ready to be serialized, along with the Link +import "github.com/superseriousbusiness/gotosocial/internal/timeline" + +// TimelineResponse wraps a slice of timelineables, ready to be serialized, along with the Link // header for the previous and next queries, to be returned to the client. -type StatusTimelineResponse struct { - Statuses []*Status +type TimelineResponse struct { + Items []timeline.Timelineable LinkHeader string } diff --git a/internal/processing/account.go b/internal/processing/account.go index 25f024785..43c4f65e6 100644 --- a/internal/processing/account.go +++ b/internal/processing/account.go @@ -46,7 +46,7 @@ func (p *processor) AccountUpdate(ctx context.Context, authed *oauth.Auth, form return p.accountProcessor.Update(ctx, authed.Account, form) } -func (p *processor) AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode) { +func (p *processor) AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) (*apimodel.TimelineResponse, gtserror.WithCode) { return p.accountProcessor.StatusesGet(ctx, authed.Account, targetAccountID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly) } diff --git a/internal/processing/account/account.go b/internal/processing/account/account.go index 7668da02c..2a27fc743 100644 --- a/internal/processing/account/account.go +++ b/internal/processing/account/account.go @@ -55,7 +55,7 @@ type Processor interface { Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) // StatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for // the account given in authed. - StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode) + StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) (*apimodel.TimelineResponse, gtserror.WithCode) // FollowersGet fetches a list of the target account's followers. FollowersGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) // FollowingGet fetches a list of the accounts that target account is following. diff --git a/internal/processing/account/getstatuses.go b/internal/processing/account/getstatuses.go index c185302c5..90bf8e06e 100644 --- a/internal/processing/account/getstatuses.go +++ b/internal/processing/account/getstatuses.go @@ -26,9 +26,11 @@ "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/timeline" + "github.com/superseriousbusiness/gotosocial/internal/util" ) -func (p *processor) StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode) { +func (p *processor) StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) (*apimodel.TimelineResponse, gtserror.WithCode) { if requestingAccount != nil { if blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil { return nil, gtserror.NewErrorInternalError(err) @@ -37,29 +39,48 @@ func (p *processor) StatusesGet(ctx context.Context, requestingAccount *gtsmodel } } - apiStatuses := []apimodel.Status{} - statuses, err := p.db.GetAccountStatuses(ctx, targetAccountID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly) if err != nil { if err == db.ErrNoEntries { - return apiStatuses, nil + return util.EmptyTimelineResponse(), nil } return nil, gtserror.NewErrorInternalError(err) } + var filtered []*gtsmodel.Status for _, s := range statuses { visible, err := p.filter.StatusVisible(ctx, s, requestingAccount) - if err != nil || !visible { - continue + if err == nil && visible { + filtered = append(filtered, s) } + } - apiStatus, err := p.tc.StatusToAPIStatus(ctx, s, requestingAccount) + if len(filtered) == 0 { + return util.EmptyTimelineResponse(), nil + } + + timelineables := []timeline.Timelineable{} + for _, i := range filtered { + apiStatus, err := p.tc.StatusToAPIStatus(ctx, i, requestingAccount) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to api: %s", err)) } - apiStatuses = append(apiStatuses, *apiStatus) + timelineables = append(timelineables, apiStatus) } - return apiStatuses, nil + return util.PackageTimelineableResponse(util.TimelineableResponseParams{ + Items: timelineables, + Path: fmt.Sprintf("/api/v1/accounts/%s/statuses", targetAccountID), + NextMaxIDValue: timelineables[len(timelineables)-1].GetID(), + PrevMinIDValue: timelineables[0].GetID(), + Limit: limit, + ExtraQueryParams: []string{ + fmt.Sprintf("exclude_replies=%t", excludeReplies), + fmt.Sprintf("exclude_reblogs=%t", excludeReblogs), + fmt.Sprintf("pinned_only=%t", pinnedOnly), + fmt.Sprintf("only_media=%t", mediaOnly), + fmt.Sprintf("only_public=%t", publicOnly), + }, + }) } diff --git a/internal/processing/notification.go b/internal/processing/notification.go index e976952e8..ee0e4ae34 100644 --- a/internal/processing/notification.go +++ b/internal/processing/notification.go @@ -26,9 +26,11 @@ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" + "github.com/superseriousbusiness/gotosocial/internal/timeline" + "github.com/superseriousbusiness/gotosocial/internal/util" ) -func (p *processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, limit int, maxID string, sinceID string) ([]*apimodel.Notification, gtserror.WithCode) { +func (p *processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, limit int, maxID string, sinceID string) (*apimodel.TimelineResponse, gtserror.WithCode) { l := logrus.WithField("func", "NotificationsGet") notifs, err := p.db.GetNotifications(ctx, authed.Account.ID, limit, maxID, sinceID) @@ -36,15 +38,26 @@ func (p *processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, li return nil, gtserror.NewErrorInternalError(err) } - apiNotifs := []*apimodel.Notification{} + if len(notifs) == 0 { + return util.EmptyTimelineResponse(), nil + } + + timelineables := []timeline.Timelineable{} for _, n := range notifs { apiNotif, err := p.tc.NotificationToAPINotification(ctx, n) if err != nil { l.Debugf("got an error converting a notification to api, will skip it: %s", err) continue } - apiNotifs = append(apiNotifs, apiNotif) + timelineables = append(timelineables, apiNotif) } - return apiNotifs, nil + return util.PackageTimelineableResponse(util.TimelineableResponseParams{ + Items: timelineables, + Path: "api/v1/notifications", + NextMaxIDValue: timelineables[len(timelineables)-1].GetID(), + PrevMinIDKey: "since_id", + PrevMinIDValue: timelineables[0].GetID(), + Limit: limit, + }) } diff --git a/internal/processing/notification_test.go b/internal/processing/notification_test.go index 6f2d44c5c..6ecca92a9 100644 --- a/internal/processing/notification_test.go +++ b/internal/processing/notification_test.go @@ -23,6 +23,7 @@ "testing" "github.com/stretchr/testify/suite" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" ) type NotificationTestSuite struct { @@ -32,14 +33,19 @@ type NotificationTestSuite struct { // get a notification where someone has liked our status func (suite *NotificationTestSuite) TestGetNotifications() { receivingAccount := suite.testAccounts["local_account_1"] - notifs, err := suite.processor.NotificationsGet(context.Background(), suite.testAutheds["local_account_1"], 10, "", "") + notifsResponse, err := suite.processor.NotificationsGet(context.Background(), suite.testAutheds["local_account_1"], 10, "", "") suite.NoError(err) - suite.Len(notifs, 1) - notif := notifs[0] + suite.Len(notifsResponse.Items, 1) + notif, ok := notifsResponse.Items[0].(*apimodel.Notification) + if !ok { + panic("notif in response wasn't *apimodel.Notification") + } + suite.NotNil(notif.Status) suite.NotNil(notif.Status) suite.NotNil(notif.Status.Account) suite.Equal(receivingAccount.ID, notif.Status.Account.ID) + suite.Equal(`; rel="next", ; rel="prev"`, notifsResponse.LinkHeader) } func TestNotificationTestSuite(t *testing.T) { diff --git a/internal/processing/processor.go b/internal/processing/processor.go index 225606b70..a10e0d6b3 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -83,7 +83,7 @@ type Processor interface { AccountUpdate(ctx context.Context, authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) // AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for // the account given in authed. - AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode) + AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) (*apimodel.TimelineResponse, gtserror.WithCode) // AccountFollowersGet fetches a list of the target account's followers. AccountFollowersGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) // AccountFollowingGet fetches a list of the accounts that target account is following. @@ -150,7 +150,7 @@ type Processor interface { MediaUpdate(ctx context.Context, authed *oauth.Auth, attachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) // NotificationsGet - NotificationsGet(ctx context.Context, authed *oauth.Auth, limit int, maxID string, sinceID string) ([]*apimodel.Notification, gtserror.WithCode) + NotificationsGet(ctx context.Context, authed *oauth.Auth, limit int, maxID string, sinceID string) (*apimodel.TimelineResponse, gtserror.WithCode) // 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) @@ -177,11 +177,11 @@ type Processor interface { StatusGetContext(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Context, gtserror.WithCode) // HomeTimelineGet returns statuses from the home timeline, with the given filters/parameters. - HomeTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.StatusTimelineResponse, gtserror.WithCode) + HomeTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.TimelineResponse, gtserror.WithCode) // PublicTimelineGet returns statuses from the public/local timeline, with the given filters/parameters. - PublicTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.StatusTimelineResponse, gtserror.WithCode) + PublicTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.TimelineResponse, gtserror.WithCode) // FavedTimelineGet returns faved statuses, with the given filters/parameters. - FavedTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.StatusTimelineResponse, gtserror.WithCode) + FavedTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.TimelineResponse, gtserror.WithCode) // AuthorizeStreamingRequest returns a gotosocial account in exchange for an access token, or an error if the given token is not valid. AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, error) diff --git a/internal/processing/statustimeline.go b/internal/processing/statustimeline.go index d4388f381..081709302 100644 --- a/internal/processing/statustimeline.go +++ b/internal/processing/statustimeline.go @@ -22,18 +22,17 @@ "context" "errors" "fmt" - "net/url" "github.com/sirupsen/logrus" 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" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/timeline" "github.com/superseriousbusiness/gotosocial/internal/typeutils" + "github.com/superseriousbusiness/gotosocial/internal/util" "github.com/superseriousbusiness/gotosocial/internal/visibility" ) @@ -139,114 +138,100 @@ func StatusSkipInsertFunction() timeline.SkipInsertFunction { } } -func (p *processor) packageStatusResponse(statuses []*apimodel.Status, path string, nextMaxID string, prevMinID string, limit int) (*apimodel.StatusTimelineResponse, gtserror.WithCode) { - resp := &apimodel.StatusTimelineResponse{ - Statuses: []*apimodel.Status{}, - } - resp.Statuses = statuses - - // prepare the next and previous links - if len(statuses) != 0 { - protocol := config.GetProtocol() - host := config.GetHost() - - nextLink := &url.URL{ - Scheme: protocol, - Host: host, - Path: path, - RawQuery: fmt.Sprintf("limit=%d&max_id=%s", limit, nextMaxID), - } - next := fmt.Sprintf("<%s>; rel=\"next\"", nextLink.String()) - - prevLink := &url.URL{ - Scheme: protocol, - Host: host, - Path: path, - RawQuery: fmt.Sprintf("limit=%d&min_id=%s", limit, prevMinID), - } - prev := fmt.Sprintf("<%s>; rel=\"prev\"", prevLink.String()) - resp.LinkHeader = fmt.Sprintf("%s, %s", next, prev) - } - - return resp, nil -} - -func (p *processor) HomeTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.StatusTimelineResponse, gtserror.WithCode) { +func (p *processor) HomeTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.TimelineResponse, gtserror.WithCode) { preparedItems, err := p.statusTimelines.GetTimeline(ctx, authed.Account.ID, maxID, sinceID, minID, limit, local) if err != nil { return nil, gtserror.NewErrorInternalError(err) } if len(preparedItems) == 0 { - return &apimodel.StatusTimelineResponse{ - Statuses: []*apimodel.Status{}, - }, nil + return util.EmptyTimelineResponse(), nil } - statuses := []*apimodel.Status{} + timelineables := []timeline.Timelineable{} for _, i := range preparedItems { status, ok := i.(*apimodel.Status) if !ok { return nil, gtserror.NewErrorInternalError(errors.New("error converting prepared timeline entry to api status")) } - statuses = append(statuses, status) + timelineables = append(timelineables, status) } - return p.packageStatusResponse(statuses, "api/v1/timelines/home", statuses[len(preparedItems)-1].ID, statuses[0].ID, limit) + return util.PackageTimelineableResponse(util.TimelineableResponseParams{ + Items: timelineables, + Path: "api/v1/timelines/home", + NextMaxIDValue: timelineables[len(timelineables)-1].GetID(), + PrevMinIDValue: timelineables[0].GetID(), + Limit: limit, + }) } -func (p *processor) PublicTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.StatusTimelineResponse, gtserror.WithCode) { +func (p *processor) PublicTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.TimelineResponse, gtserror.WithCode) { statuses, err := p.db.GetPublicTimeline(ctx, authed.Account.ID, maxID, sinceID, minID, limit, local) if err != nil { if err == db.ErrNoEntries { // there are just no entries left - return &apimodel.StatusTimelineResponse{ - Statuses: []*apimodel.Status{}, - }, nil + return util.EmptyTimelineResponse(), nil } // there's an actual error return nil, gtserror.NewErrorInternalError(err) } - s, err := p.filterPublicStatuses(ctx, authed, statuses) + filtered, err := p.filterPublicStatuses(ctx, authed, statuses) if err != nil { return nil, gtserror.NewErrorInternalError(err) } - if len(s) == 0 { - return &apimodel.StatusTimelineResponse{ - Statuses: []*apimodel.Status{}, - }, nil + if len(filtered) == 0 { + return util.EmptyTimelineResponse(), nil } - return p.packageStatusResponse(s, "api/v1/timelines/public", s[len(s)-1].ID, s[0].ID, limit) + timelineables := []timeline.Timelineable{} + for _, i := range filtered { + timelineables = append(timelineables, i) + } + + return util.PackageTimelineableResponse(util.TimelineableResponseParams{ + Items: timelineables, + Path: "api/v1/timelines/public", + NextMaxIDValue: timelineables[len(timelineables)-1].GetID(), + PrevMinIDValue: timelineables[0].GetID(), + Limit: limit, + }) } -func (p *processor) FavedTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.StatusTimelineResponse, gtserror.WithCode) { +func (p *processor) FavedTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.TimelineResponse, gtserror.WithCode) { statuses, nextMaxID, prevMinID, err := p.db.GetFavedTimeline(ctx, authed.Account.ID, maxID, minID, limit) if err != nil { if err == db.ErrNoEntries { // there are just no entries left - return &apimodel.StatusTimelineResponse{ - Statuses: []*apimodel.Status{}, - }, nil + return util.EmptyTimelineResponse(), nil } // there's an actual error return nil, gtserror.NewErrorInternalError(err) } - s, err := p.filterFavedStatuses(ctx, authed, statuses) + filtered, err := p.filterFavedStatuses(ctx, authed, statuses) if err != nil { return nil, gtserror.NewErrorInternalError(err) } - if len(s) == 0 { - return &apimodel.StatusTimelineResponse{ - Statuses: []*apimodel.Status{}, - }, nil + if len(filtered) == 0 { + return util.EmptyTimelineResponse(), nil } - return p.packageStatusResponse(s, "api/v1/favourites", nextMaxID, prevMinID, limit) + timelineables := []timeline.Timelineable{} + for _, i := range filtered { + timelineables = append(timelineables, i) + } + + return util.PackageTimelineableResponse(util.TimelineableResponseParams{ + Items: timelineables, + Path: "api/v1/favourites", + NextMaxIDValue: nextMaxID, + PrevMinIDValue: prevMinID, + Limit: limit, + }) } func (p *processor) filterPublicStatuses(ctx context.Context, authed *oauth.Auth, statuses []*gtsmodel.Status) ([]*apimodel.Status, error) { diff --git a/internal/util/timeline.go b/internal/util/timeline.go new file mode 100644 index 000000000..929464add --- /dev/null +++ b/internal/util/timeline.go @@ -0,0 +1,104 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package util + +import ( + "fmt" + "net/url" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/timeline" +) + +// TimelineableResponseParams models the parameters to pass to PackageTimelineableResponse. +// +// The given items will be provided in the timeline response. +// +// The other values are all used to create the Link header so that callers know +// which endpoint to query next and previously in order to do paging. +type TimelineableResponseParams struct { + Items []timeline.Timelineable // Sorted slice of Timelineables (statuses, notifications, etc) + Path string // path to use for next/prev queries in the link header + NextMaxIDKey string // key to use for the next max id query param in the link header, defaults to 'max_id' + NextMaxIDValue string // value to use for next max id + PrevMinIDKey string // key to use for the prev min id query param in the link header, defaults to 'min_id' + PrevMinIDValue string // value to use for prev min id + Limit int // limit number of entries to return + ExtraQueryParams []string // any extra query parameters to provide in the link header, should be in the format 'example=value' +} + +// PackageTimelineableResponse is a convenience function for returning +// a bunch of timelineable items (notifications, statuses, etc), as well +// as a Link header to inform callers of where to find next/prev items. +func PackageTimelineableResponse(params TimelineableResponseParams) (*apimodel.TimelineResponse, gtserror.WithCode) { + if params.NextMaxIDKey == "" { + params.NextMaxIDKey = "max_id" + } + + if params.PrevMinIDKey == "" { + params.PrevMinIDKey = "min_id" + } + + timelineResponse := &apimodel.TimelineResponse{ + Items: params.Items, + } + + // prepare the next and previous links + if len(params.Items) != 0 { + protocol := config.GetProtocol() + host := config.GetHost() + + nextRaw := fmt.Sprintf("limit=%d&%s=%s", params.Limit, params.NextMaxIDKey, params.NextMaxIDValue) + for _, p := range params.ExtraQueryParams { + nextRaw = nextRaw + "&" + p + } + nextLink := &url.URL{ + Scheme: protocol, + Host: host, + Path: params.Path, + RawQuery: nextRaw, + } + next := fmt.Sprintf("<%s>; rel=\"next\"", nextLink.String()) + + prevRaw := fmt.Sprintf("limit=%d&%s=%s", params.Limit, params.PrevMinIDKey, params.PrevMinIDValue) + for _, p := range params.ExtraQueryParams { + prevRaw = prevRaw + "&" + p + } + prevLink := &url.URL{ + Scheme: protocol, + Host: host, + Path: params.Path, + RawQuery: prevRaw, + } + prev := fmt.Sprintf("<%s>; rel=\"prev\"", prevLink.String()) + timelineResponse.LinkHeader = next + ", " + prev + } + + return timelineResponse, nil +} + +// EmptyTimelineResponse just returns an empty +// TimelineResponse with no link header or items. +func EmptyTimelineResponse() *apimodel.TimelineResponse { + return &apimodel.TimelineResponse{ + Items: []timeline.Timelineable{}, + } +} diff --git a/internal/web/profile.go b/internal/web/profile.go index 66419dcc7..3155c022d 100644 --- a/internal/web/profile.go +++ b/internal/web/profile.go @@ -29,6 +29,7 @@ "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/api" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -79,7 +80,7 @@ func (m *Module) profileTemplateHandler(c *gin.Context) { // get latest 10 top-level public statuses; // ie., exclude replies and boosts, public only, // with or without media - statuses, errWithCode := m.processor.AccountStatusesGet(ctx, authed, account.ID, 10, true, true, "", "", false, false, true) + statusResp, errWithCode := m.processor.AccountStatusesGet(ctx, authed, account.ID, 10, true, true, "", "", false, false, true) if errWithCode != nil { l.Debugf("error getting statuses from processor: %s", errWithCode.Error()) c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) @@ -92,7 +93,11 @@ func (m *Module) profileTemplateHandler(c *gin.Context) { randomIndex := rand.Intn(len(m.defaultAvatars)) dummyAvatar := m.defaultAvatars[randomIndex] account.Avatar = dummyAvatar - for _, s := range statuses { + for _, i := range statusResp.Items { + s, ok := i.(*apimodel.Status) + if !ok { + panic("timelineable was not *apimodel.Status") + } s.Account.Avatar = dummyAvatar } } @@ -100,7 +105,7 @@ func (m *Module) profileTemplateHandler(c *gin.Context) { c.HTML(http.StatusOK, "profile.tmpl", gin.H{ "instance": instance, "account": account, - "statuses": statuses, + "statuses": statusResp.Items, "stylesheets": []string{ "/assets/Fork-Awesome/css/fork-awesome.min.css", "/assets/status.css", From 4e6625ee2d0ccd036626287e3a929f10fc109e40 Mon Sep 17 00:00:00 2001 From: f0x52 Date: Wed, 8 Jun 2022 20:26:44 +0200 Subject: [PATCH 06/13] [feature] add vscode workspace config for linting + debugging (#588) * vscode workspace config for debug run + linting * ignore debug build file --- .gitignore | 5 ++++- .vscode/launch.json | 16 ++++++++++++++++ .vscode/settings.json | 6 ++++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 0ccc883b2..b1f97a828 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,7 @@ dist/ web/assets/swagger.yaml # exludes docker-volume from exemple/docker-compose -example/docker-compose/docker-volume \ No newline at end of file +example/docker-compose/docker-volume + +# excludes debug build +cmd/gotosocial/__debug_bin \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..906262d2f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/cmd/gotosocial", + "args": [ + "testrig", "start" + ], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..a0304179d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "go.lintTool":"golangci-lint", + "go.lintFlags": [ + "--fast" + ] +} \ No newline at end of file From 91c0ed863a7d514b65db79ae4eb85822f0f0f508 Mon Sep 17 00:00:00 2001 From: kim <89579420+NyaaaWhatsUpDoc@users.noreply.github.com> Date: Wed, 8 Jun 2022 19:28:28 +0100 Subject: [PATCH 07/13] [bugfix] #621: add weak type handing to mapstructure decode (#625) * Drone sig (#623) * accept weakly typed input on mapstructure decode i.e. .UnmarshalMap() Signed-off-by: kim * add envparsing script to test for panics during environment variable parsing Signed-off-by: kim * add envparsing.sh script to drone commands Signed-off-by: kim * update drone signature Co-authored-by: kim * compare expected with output * update expected output of envparsing * update expected output to correct value * use viper's unmarshal function instead There were problems with marshalling string slices from viper into the st.config struct with the other function. Now, we can use viper's unmarshal function and pass in the custom decoder config that we need as a hook. This ensures that we marshal string slices from viper into our config struct correctly. Co-authored-by: tobi <31960611+tsmethurst@users.noreply.github.com> Co-authored-by: tsmethurst --- .drone.yml | 3 +- internal/config/config.go | 9 ----- internal/config/state.go | 7 ++-- test/envparsing.sh | 70 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 12 deletions(-) create mode 100755 test/envparsing.sh diff --git a/.drone.yml b/.drone.yml index a96d54649..bec186175 100644 --- a/.drone.yml +++ b/.drone.yml @@ -38,6 +38,7 @@ steps: - apk update --no-cache && apk add git - CGO_ENABLED=0 GTS_DB_TYPE="sqlite" GTS_DB_ADDRESS=":memory:" go test ./... - CGO_ENABLED=0 ./test/cliparsing.sh + - CGO_ENABLED=0 ./test/envparsing.sh when: event: include: @@ -145,6 +146,6 @@ steps: --- kind: signature -hmac: f3cf4e422d9ce7dc0a881da429db628232e2f9e91383ee5a33cf4f13542c0a23 +hmac: adfcc11559717e4e371e714f3ac19ab528208f678961436f316f491bf82de8ad ... diff --git a/internal/config/config.go b/internal/config/config.go index 573f2b3a2..3a1775778 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -130,12 +130,3 @@ func (cfg *Configuration) MarshalMap() (map[string]interface{}, error) { } return dst, nil } - -// UnmarshalMap will unmarshal a map structure into the receiving Configuration. -func (cfg *Configuration) UnmarshalMap(src map[string]interface{}) error { - dec, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ - TagName: "name", - Result: cfg, - }) - return dec.Decode(src) -} diff --git a/internal/config/state.go b/internal/config/state.go index 70972a835..1364fb84e 100644 --- a/internal/config/state.go +++ b/internal/config/state.go @@ -22,6 +22,7 @@ "strings" "sync" + "github.com/mitchellh/mapstructure" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -129,8 +130,10 @@ func (st *ConfigState) reloadToViper() { // reloadFromViper will reload Configuration{} values from viper. func (st *ConfigState) reloadFromViper() { - err := st.config.UnmarshalMap(st.viper.AllSettings()) - if err != nil { + if err := st.viper.Unmarshal(&st.config, func(c *mapstructure.DecoderConfig) { + c.TagName = "name" + c.ZeroFields = true // empty the config struct before we marshal values into it + }); err != nil { panic(err) } } diff --git a/test/envparsing.sh b/test/envparsing.sh new file mode 100755 index 000000000..8bced72e9 --- /dev/null +++ b/test/envparsing.sh @@ -0,0 +1,70 @@ +#!/bin/sh + +set -eu + +EXPECTED='{"account-domain":"peepee","accounts-approval-required":false,"accounts-reason-required":false,"accounts-registration-open":true,"advanced-cookies-samesite":"strict","application-name":"gts","bind-address":"127.0.0.1","config-path":"./test/test.yaml","db-address":":memory:","db-database":"gotosocial_prod","db-password":"hunter2","db-port":6969,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"sqlite","db-user":"sex-haver","email":"","host":"example.com","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":true,"log-level":"info","media-description-max-chars":5000,"media-description-min-chars":69,"media-image-max-size":420,"media-remote-cache-days":30,"media-video-max-size":420,"oidc-client-id":"1234","oidc-client-secret":"shhhh its a secret","oidc-enabled":true,"oidc-idp-name":"sex-haver","oidc-issuer":"whoknows","oidc-scopes":["read","write"],"oidc-skip-verification":true,"password":"","path":"","port":6969,"protocol":"http","smtp-from":"queen@terfisland.org","smtp-host":"example.com","smtp-password":"hunter2","smtp-port":4269,"smtp-username":"sex-haver","software-version":"","statuses-cw-max-chars":420,"statuses-max-chars":69,"statuses-media-max-files":1,"statuses-poll-max-options":1,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/root/store","syslog-address":"127.0.0.1:6969","syslog-enabled":true,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"/root","web-template-base-dir":"/root"}' + +# Set all the environment variables to +# ensure that these are parsed without panic +OUTPUT=$(GTS_LOG_LEVEL='info' \ +GTS_LOG_DB_QUERIES=true \ +GTS_APPLICATION_NAME=gts \ +GTS_HOST=example.com \ +GTS_ACCOUNT_DOMAIN='peepee' \ +GTS_PROTOCOL=http \ +GTS_BIND_ADDRESS='127.0.0.1' \ +GTS_PORT=6969 \ +GTS_TRUSTED_PROXIES='' \ +GTS_DB_TYPE='sqlite' \ +GTS_DB_ADDRESS=':memory:' \ +GTS_DB_PORT=6969 \ +GTS_DB_USER='sex-haver' \ +GTS_DB_PASSWORD='hunter2' \ +GTS_DB_DATABASE='gotosocial_prod' \ +GTS_TLS_MODE='' \ +GTS_DB_TLS_CA_CERT='' \ +GTS_WEB_TEMPLATE_BASE_DIR='/root' \ +GTS_WEB_ASSET_BASE_DIR='/root' \ +GTS_ACCOUNTS_REGISTRATION_OPEN=true \ +GTS_ACCOUNTS_APPROVAL_REQUIRED=false \ +GTS_ACCOUNTS_REASON_REQUIRED=false \ +GTS_MEDIA_IMAGE_MAX_SIZE=420 \ +GTS_MEDIA_VIDEO_MAX_SIZE=420 \ +GTS_MEDIA_DESCRIPTION_MIN_CHARS=69 \ +GTS_MEDIA_DESCRIPTION_MAX_CHARS=5000 \ +GTS_MEDIA_REMOTE_CACHE_DAYS=30 \ +GTS_STORAGE_BACKEND='local' \ +GTS_STORAGE_LOCAL_BASE_PATH='/root/store' \ +GTS_STATUSES_MAX_CHARS=69 \ +GTS_STATUSES_CW_MAX_CHARS=420 \ +GTS_STATUSES_POLL_MAX_OPTIONS=1 \ +GTS_STATUSES_POLL_OPTIONS_MAX_CHARS=69 \ +GTS_STATUSES_MEDIA_MAX_FILES=1 \ +GTS_LETS_ENCRYPT_ENABLED=false \ +GTS_LETS_ENCRYPT_PORT=8080 \ +GTS_LETS_ENCRYPT_CERT_DIR='/root/certs' \ +GTS_LETS_ENCRYPT_EMAIL_ADDRESS='le@example.com' \ +GTS_OIDC_ENABLED=true \ +GTS_OIDC_IDP_NAME='sex-haver' \ +GTS_OIDC_SKIP_VERIFICATION=true \ +GTS_OIDC_ISSUER='whoknows' \ +GTS_OIDC_CLIENT_ID='1234' \ +GTS_OIDC_CLIENT_SECRET='shhhh its a secret' \ +GTS_OIDC_SCOPES='read,write' \ +GTS_SMTP_HOST='example.com' \ +GTS_SMTP_PORT=4269 \ +GTS_SMTP_USERNAME='sex-haver' \ +GTS_SMTP_PASSWORD='hunter2' \ +GTS_SMTP_FROM='queen@terfisland.org' \ +GTS_SYSLOG_ENABLED=true \ +GTS_SYSLOG_PROTOCOL='udp' \ +GTS_SYSLOG_ADDRESS='127.0.0.1:6969' \ +GTS_ADVANCED_COOKIES_SAMESITE='strict' \ +go run ./cmd/gotosocial/... --config-path $(dirname ${0})/test.yaml debug config) + +if [ "${OUTPUT}" != "${EXPECTED}" ]; then + echo "OUTPUT not equal EXPECTED" + exit 1 +else + echo "OK" +fi From 1ede54ddf6dfd2d4ba039eb7e23b74bcac65b643 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Wed, 8 Jun 2022 20:38:03 +0200 Subject: [PATCH 08/13] [feature] More consistent API error handling (#637) * update templates * start reworking api error handling * update template * return AP status at web endpoint if negotiated * start making api error handling much more consistent * update account endpoints to new error handling * use new api error handling in admin endpoints * go fmt ./... * use api error logic in app * use generic error handling in auth * don't export generic error handler * don't defer clearing session * user nicer error handling on oidc callback handler * tidy up the sign in handler * tidy up the token handler * use nicer error handling in blocksget * auth emojis endpoint * fix up remaining api endpoints * fix whoopsie during login flow * regenerate swagger docs * change http error logging to debug --- cmd/gotosocial/action/server/server.go | 2 +- cmd/gotosocial/action/testrig/testrig.go | 2 +- docs/api/swagger.yaml | 216 ++++++++++++++++-- internal/api/client/account/accountcreate.go | 44 ++-- internal/api/client/account/accountdelete.go | 24 +- internal/api/client/account/accountget.go | 23 +- internal/api/client/account/accountupdate.go | 67 +++--- .../api/client/account/accountupdate_test.go | 17 +- internal/api/client/account/accountverify.go | 26 +-- internal/api/client/account/block.go | 19 +- internal/api/client/account/follow.go | 22 +- internal/api/client/account/followers.go | 19 +- internal/api/client/account/following.go | 19 +- internal/api/client/account/relationships.go | 27 +-- internal/api/client/account/statuses.go | 51 +++-- internal/api/client/account/unblock.go | 19 +- internal/api/client/account/unfollow.go | 25 +- internal/api/client/admin/accountaction.go | 40 ++-- .../api/client/admin/domainblockcreate.go | 72 +++--- .../api/client/admin/domainblockdelete.go | 38 +-- internal/api/client/admin/domainblockget.go | 46 ++-- internal/api/client/admin/domainblocksget.go | 50 ++-- internal/api/client/admin/emojicreate.go | 48 ++-- internal/api/client/admin/emojicreate_test.go | 2 +- internal/api/client/admin/mediacleanup.go | 39 ++-- internal/api/client/app/appcreate.go | 71 +++--- internal/api/client/auth/auth.go | 17 +- internal/api/client/auth/auth_test.go | 35 ++- internal/api/client/auth/authorize.go | 213 ++++++++++------- internal/api/client/auth/callback.go | 107 +++++---- internal/api/client/auth/signin.go | 110 ++++----- internal/api/client/auth/token.go | 55 ++--- internal/api/client/blocks/blocksget.go | 26 +-- internal/api/client/emoji/emojisget.go | 11 +- .../api/client/favourites/favouritesget.go | 18 +- internal/api/client/fileserver/servefile.go | 39 ++-- internal/api/client/filter/filtersget.go | 9 +- .../api/client/followrequest/authorize.go | 27 +-- .../client/followrequest/authorize_test.go | 28 +++ internal/api/client/followrequest/get.go | 24 +- internal/api/client/followrequest/reject.go | 26 +-- internal/api/client/instance/instanceget.go | 17 +- internal/api/client/instance/instancepatch.go | 43 ++-- .../api/client/instance/instancepatch_test.go | 62 +++++ internal/api/client/list/listsgets.go | 9 +- internal/api/client/media/mediacreate.go | 32 +-- internal/api/client/media/mediacreate_test.go | 5 +- internal/api/client/media/mediaget.go | 25 +- internal/api/client/media/mediaupdate.go | 42 ++-- internal/api/client/media/mediaupdate_test.go | 2 +- .../client/notification/notificationsget.go | 25 +- internal/api/client/search/searchget.go | 72 +++--- internal/api/client/status/statusboost.go | 26 +-- .../api/client/status/statusboost_test.go | 12 +- internal/api/client/status/statusboostedby.go | 8 +- internal/api/client/status/statuscontext.go | 26 +-- internal/api/client/status/statuscreate.go | 53 ++--- .../api/client/status/statuscreate_test.go | 2 +- internal/api/client/status/statusdelete.go | 36 ++- internal/api/client/status/statusfave.go | 30 ++- internal/api/client/status/statusfave_test.go | 4 +- internal/api/client/status/statusfavedby.go | 32 ++- internal/api/client/status/statusget.go | 38 ++- internal/api/client/status/statusunboost.go | 26 +-- internal/api/client/status/statusunfave.go | 30 ++- internal/api/client/streaming/stream.go | 111 ++++----- internal/api/client/timeline/home.go | 22 +- internal/api/client/timeline/public.go | 22 +- internal/api/client/user/passwordchange.go | 47 ++-- .../api/client/user/passwordchange_test.go | 14 +- internal/api/client/user/user_test.go | 2 +- internal/api/errorhandling.go | 127 ++++++++++ .../{gtserror/unauthorized.go => api/mime.go} | 17 +- internal/api/negotiate.go | 37 +-- internal/api/s2s/nodeinfo/nodeinfoget.go | 25 +- internal/api/s2s/nodeinfo/wellknownget.go | 15 +- internal/api/s2s/user/followers.go | 39 ++-- internal/api/s2s/user/following.go | 39 ++-- internal/api/s2s/user/inboxpost.go | 38 ++- internal/api/s2s/user/outboxget.go | 52 ++--- internal/api/s2s/user/publickeyget.go | 39 ++-- internal/api/s2s/user/repliesget.go | 62 ++--- internal/api/s2s/user/statusget.go | 38 ++- internal/api/s2s/user/userget.go | 39 ++-- internal/api/s2s/webfinger/webfingerget.go | 8 +- internal/api/security/extraheaders.go | 18 ++ internal/api/security/useragentblock.go | 14 +- internal/federation/authenticate.go | 22 +- internal/gtserror/withcode.go | 42 +++- internal/oidc/handlecallback.go | 18 +- internal/oidc/idp.go | 3 +- internal/processing/account.go | 4 +- internal/processing/account/account.go | 4 +- internal/processing/account/create.go | 17 +- internal/processing/account/get.go | 1 + internal/processing/account/update.go | 21 +- internal/processing/account/update_test.go | 8 +- internal/processing/admin/emoji.go | 2 +- internal/processing/app.go | 16 +- .../processing/federation/getfollowers.go | 4 +- .../processing/federation/getfollowing.go | 4 +- internal/processing/federation/getoutbox.go | 4 +- internal/processing/federation/getstatus.go | 4 +- .../processing/federation/getstatusreplies.go | 4 +- internal/processing/federation/getuser.go | 4 +- internal/processing/media.go | 2 +- internal/processing/media/create.go | 15 +- internal/processing/media/media.go | 2 +- internal/processing/processor.go | 22 +- internal/processing/status.go | 12 +- internal/processing/status/create.go | 4 +- internal/processing/status/status.go | 2 +- internal/processing/status/util.go | 32 +-- internal/processing/streaming.go | 2 +- internal/processing/streaming/authorize.go | 27 ++- .../processing/streaming/authorize_test.go | 2 +- internal/processing/streaming/streaming.go | 2 +- .../processing/user/changepassword_test.go | 4 +- internal/transport/deliver.go | 3 +- internal/transport/dereference.go | 4 +- internal/transport/derefinstance.go | 7 +- internal/transport/finger.go | 4 +- internal/web/base.go | 28 +-- internal/web/confirmemail.go | 16 +- internal/web/profile.go | 61 +++-- internal/web/thread.go | 98 ++++++-- testrig/router.go | 24 +- web/template/404.tmpl | 12 +- web/template/error.tmpl | 1 - web/template/footer.tmpl | 4 + 130 files changed, 2154 insertions(+), 1673 deletions(-) create mode 100644 internal/api/errorhandling.go rename internal/{gtserror/unauthorized.go => api/mime.go} (59%) diff --git a/cmd/gotosocial/action/server/server.go b/cmd/gotosocial/action/server/server.go index f266331f2..dd41f7708 100644 --- a/cmd/gotosocial/action/server/server.go +++ b/cmd/gotosocial/action/server/server.go @@ -165,7 +165,7 @@ } // build client api modules - authModule := auth.New(dbService, oauthServer, idp) + authModule := auth.New(dbService, oauthServer, idp, processor) accountModule := account.New(processor) instanceModule := instance.New(processor) appsModule := app.New(processor) diff --git a/cmd/gotosocial/action/testrig/testrig.go b/cmd/gotosocial/action/testrig/testrig.go index cb587c51d..cb0557771 100644 --- a/cmd/gotosocial/action/testrig/testrig.go +++ b/cmd/gotosocial/action/testrig/testrig.go @@ -108,7 +108,7 @@ } // build client api modules - authModule := auth.New(dbService, oauthServer, idp) + authModule := auth.New(dbService, oauthServer, idp, processor) accountModule := account.New(processor) instanceModule := instance.New(processor) appsModule := app.New(processor) diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml index f61bcbaea..fa1bd7499 100644 --- a/docs/api/swagger.yaml +++ b/docs/api/swagger.yaml @@ -692,7 +692,7 @@ definitions: text_url: description: |- A shorter URL for the attachment. - Not currently used. + In our case, we just give the URL again since we don't create smaller URLs. type: string x-go-name: TextURL type: @@ -1894,8 +1894,10 @@ paths: description: unauthorized "404": description: not found + "406": + description: not acceptable "500": - description: internal error + description: internal server error security: - OAuth2 Application: - write:accounts @@ -1924,6 +1926,10 @@ paths: description: unauthorized "404": description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - read:accounts @@ -1952,6 +1958,10 @@ paths: description: unauthorized "404": description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - write:blocks @@ -1999,6 +2009,10 @@ paths: description: unauthorized "404": description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - write:follows @@ -2029,6 +2043,10 @@ paths: description: unauthorized "404": description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - read:accounts @@ -2059,6 +2077,10 @@ paths: description: unauthorized "404": description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - read:accounts @@ -2134,6 +2156,10 @@ paths: description: unauthorized "404": description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - read:accounts @@ -2162,6 +2188,10 @@ paths: description: unauthorized "404": description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - write:blocks @@ -2190,6 +2220,10 @@ paths: description: unauthorized "404": description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - write:follows @@ -2215,6 +2249,12 @@ paths: description: bad request "401": description: unauthorized + "404": + description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - write:accounts @@ -2247,6 +2287,10 @@ paths: description: unauthorized "404": description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - read:accounts @@ -2313,6 +2357,12 @@ paths: description: bad request "401": description: unauthorized + "404": + description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - write:accounts @@ -2335,6 +2385,10 @@ paths: description: unauthorized "404": description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - read:accounts @@ -2372,6 +2426,12 @@ paths: description: unauthorized "403": description: forbidden + "404": + description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - admin @@ -2406,10 +2466,18 @@ paths: $ref: '#/definitions/emoji' "400": description: bad request + "401": + description: unauthorized "403": description: forbidden + "404": + description: not found + "406": + description: not acceptable "409": description: conflict -- domain/shortcode combo for emoji already exists + "500": + description: internal server error security: - OAuth2 Bearer: - admin @@ -2439,10 +2507,16 @@ paths: type: array "400": description: bad request + "401": + description: unauthorized "403": description: forbidden "404": description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - admin @@ -2511,8 +2585,16 @@ paths: $ref: '#/definitions/domainBlock' "400": description: bad request + "401": + description: unauthorized "403": description: forbidden + "404": + description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - admin @@ -2537,10 +2619,16 @@ paths: $ref: '#/definitions/domainBlock' "400": description: bad request + "401": + description: unauthorized "403": description: forbidden "404": description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - admin @@ -2564,10 +2652,16 @@ paths: $ref: '#/definitions/domainBlock' "400": description: bad request + "401": + description: unauthorized "403": description: forbidden "404": description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - admin @@ -2599,8 +2693,16 @@ paths: asynchronously after the request completes. "400": description: bad request + "401": + description: unauthorized "403": description: forbidden + "404": + description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - admin @@ -2660,10 +2762,14 @@ paths: description: bad request "401": description: unauthorized - "422": - description: unprocessable + "403": + description: forbidden + "404": + description: not found + "406": + description: not acceptable "500": - description: internal error + description: internal server error summary: Register a new application on this instance. tags: - apps @@ -2714,6 +2820,10 @@ paths: description: unauthorized "404": description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - read:blocks @@ -2753,10 +2863,12 @@ paths: description: bad request "401": description: unauthorized - "403": - description: forbidden "404": description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - read:follows @@ -2785,10 +2897,10 @@ paths: description: bad request "401": description: unauthorized - "403": - description: forbidden "404": description: not found + "406": + description: not acceptable "500": description: internal server error security: @@ -2817,10 +2929,10 @@ paths: description: bad request "401": description: unauthorized - "403": - description: forbidden "404": description: not found + "406": + description: not acceptable "500": description: internal server error security: @@ -2843,6 +2955,8 @@ paths: description: Instance information. schema: $ref: '#/definitions/instance' + "406": + description: not acceptable "500": description: internal error summary: View instance information. @@ -2909,6 +3023,14 @@ paths: description: bad request "401": description: unauthorized + "403": + description: forbidden + "404": + description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - admin @@ -2952,10 +3074,10 @@ paths: description: bad request "401": description: unauthorized - "403": - description: forbidden "422": description: unprocessable + "500": + description: internal server error security: - OAuth2 Bearer: - write:media @@ -2982,10 +3104,12 @@ paths: description: bad request "401": description: unauthorized - "403": - description: forbidden - "422": - description: unprocessable + "404": + description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - read:media @@ -3036,10 +3160,12 @@ paths: description: bad request "401": description: unauthorized - "403": - description: forbidden - "422": - description: unprocessable + "404": + description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - write:media @@ -3141,6 +3267,12 @@ paths: description: bad request "401": description: unauthorized + "404": + description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - read:search @@ -3226,10 +3358,14 @@ paths: description: bad request "401": description: unauthorized + "403": + description: forbidden "404": description: not found + "406": + description: not acceptable "500": - description: internal error + description: internal server error security: - OAuth2 Bearer: - write:statuses @@ -3263,6 +3399,10 @@ paths: description: forbidden "404": description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - write:statuses @@ -3288,10 +3428,14 @@ paths: description: bad request "401": description: unauthorized + "403": + description: forbidden "404": description: not found + "406": + description: not acceptable "500": - description: internal error + description: internal server error security: - OAuth2 Bearer: - read:statuses @@ -3324,6 +3468,10 @@ paths: description: forbidden "404": description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - read:statuses @@ -3354,6 +3502,10 @@ paths: description: forbidden "404": description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - write:statuses @@ -3386,6 +3538,10 @@ paths: description: forbidden "404": description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - read:accounts @@ -3419,6 +3575,10 @@ paths: description: forbidden "404": description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - write:statuses @@ -3481,6 +3641,10 @@ paths: description: forbidden "404": description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - write:statuses @@ -3511,6 +3675,10 @@ paths: description: forbidden "404": description: not found + "406": + description: not acceptable + "500": + description: internal server error security: - OAuth2 Bearer: - write:statuses @@ -3778,6 +3946,8 @@ paths: description: unauthorized "403": description: forbidden + "406": + description: not acceptable "500": description: internal error security: diff --git a/internal/api/client/account/accountcreate.go b/internal/api/client/account/accountcreate.go index 35eb36216..c00d4f1be 100644 --- a/internal/api/client/account/accountcreate.go +++ b/internal/api/client/account/accountcreate.go @@ -23,12 +23,11 @@ "net" "net/http" - "github.com/sirupsen/logrus" - "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/validate" ) @@ -61,58 +60,51 @@ // description: "An OAuth2 access token for the newly-created account." // schema: // "$ref": "#/definitions/oauthToken" -// '401': -// description: unauthorized // '400': // description: bad request +// '401': +// description: unauthorized // '404': // description: not found +// '406': +// description: not acceptable // '500': -// description: internal error +// description: internal server error func (m *Module) AccountCreatePOSTHandler(c *gin.Context) { - l := logrus.WithField("func", "accountCreatePOSTHandler") authed, err := oauth.Authed(c, true, true, false, false) if err != nil { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - l.Trace("parsing request form") form := &model.AccountCreateRequest{} - if err := c.ShouldBind(form); err != nil || form == nil { - l.Debugf("could not parse form from request: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"}) + if err := c.ShouldBind(form); err != nil { + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - l.Tracef("validating form %+v", form) if err := validateCreateAccount(form); err != nil { - l.Debugf("error validating form: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } clientIP := c.ClientIP() - l.Tracef("attempting to parse client ip address %s", clientIP) signUpIP := net.ParseIP(clientIP) if signUpIP == nil { - l.Debugf("error validating sign up ip address %s", clientIP) - c.JSON(http.StatusBadRequest, gin.H{"error": "ip address could not be parsed from request"}) + err := errors.New("ip address could not be parsed from request") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - form.IP = signUpIP - ti, err := m.processor.AccountCreate(c.Request.Context(), authed, form) - if err != nil { - l.Errorf("internal server error while creating new account: %s", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + ti, errWithCode := m.processor.AccountCreate(c.Request.Context(), authed, form) + if errWithCode != nil { + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } @@ -122,6 +114,10 @@ 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 { + if form == nil { + return errors.New("form was nil") + } + if !config.GetAccountsRegistrationOpen() { return errors.New("registration is not open for this server") } diff --git a/internal/api/client/account/accountdelete.go b/internal/api/client/account/accountdelete.go index 2aae2afa0..448eaeb7c 100644 --- a/internal/api/client/account/accountdelete.go +++ b/internal/api/client/account/accountdelete.go @@ -19,12 +19,13 @@ package account import ( + "errors" "net/http" - "github.com/sirupsen/logrus" - "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -57,32 +58,35 @@ // description: bad request // '401': // description: unauthorized +// '404': +// description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) AccountDeletePOSTHandler(c *gin.Context) { - l := logrus.WithField("func", "AccountDeletePOSTHandler") authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - l.Tracef("retrieved account %+v", authed.Account.ID) form := &model.AccountDeleteRequest{} if err := c.ShouldBind(&form); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } if form.Password == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no password provided in account delete request"}) + err = errors.New("no password provided in account delete request") + api.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 { - l.Debugf("could not delete account: %s", errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/account/accountget.go b/internal/api/client/account/accountget.go index 1fa7014eb..2a060e175 100644 --- a/internal/api/client/account/accountget.go +++ b/internal/api/client/account/accountget.go @@ -19,11 +19,12 @@ package account import ( + "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -53,34 +54,38 @@ // '200': // schema: // "$ref": "#/definitions/account" -// '401': -// description: unauthorized // '400': // description: bad request +// '401': +// description: unauthorized // '404': // description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) AccountGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetAcctID := c.Param(IDKey) if targetAcctID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) + err := errors.New("no account id specified") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } acctInfo, errWithCode := m.processor.AccountGet(c.Request.Context(), authed, targetAcctID) - if err != nil { - logrus.Debug(errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + if errWithCode != nil { + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/account/accountupdate.go b/internal/api/client/account/accountupdate.go index 0317fbac0..7fae681d2 100644 --- a/internal/api/client/account/accountupdate.go +++ b/internal/api/client/account/accountupdate.go @@ -19,15 +19,15 @@ package account import ( + "errors" "fmt" "net/http" "strconv" - "github.com/sirupsen/logrus" - "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -98,68 +98,67 @@ // description: "The newly updated account." // schema: // "$ref": "#/definitions/account" -// '401': -// description: unauthorized // '400': // description: bad request +// '401': +// description: unauthorized +// '404': +// description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) AccountUpdateCredentialsPATCHHandler(c *gin.Context) { - l := logrus.WithField("func", "accountUpdateCredentialsPATCHHandler") authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - l.Tracef("retrieved account %+v", authed.Account.ID) if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } form, err := parseUpdateAccountForm(c) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - // if everything on the form is nil, then nothing has been set and we shouldn't continue - if form.Discoverable == nil && - form.Bot == nil && - form.DisplayName == nil && - form.Note == nil && - form.Avatar == nil && - form.Header == nil && - form.Locked == nil && - form.Source.Privacy == nil && - form.Source.Sensitive == nil && - form.Source.Language == nil && - form.FieldsAttributes == nil { - l.Debugf("could not parse form from request") - c.JSON(http.StatusBadRequest, gin.H{"error": "empty form submitted"}) + acctSensitive, errWithCode := m.processor.AccountUpdate(c.Request.Context(), authed, form) + if errWithCode != nil { + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } - acctSensitive, err := m.processor.AccountUpdate(c.Request.Context(), authed, form) - if err != nil { - l.Debugf("could not update account: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - l.Tracef("conversion successful, returning OK and apisensitive account %+v", acctSensitive) c.JSON(http.StatusOK, acctSensitive) } func parseUpdateAccountForm(c *gin.Context) (*model.UpdateCredentialsRequest, error) { - // parse main fields from request form := &model.UpdateCredentialsRequest{ Source: &model.UpdateSource{}, } - if err := c.ShouldBind(&form); err != nil || form == nil { + + if err := c.ShouldBind(&form); err != nil { return nil, fmt.Errorf("could not parse form from request: %s", err) } + if form == nil || + (form.Discoverable == nil && + form.Bot == nil && + form.DisplayName == nil && + form.Note == nil && + form.Avatar == nil && + form.Header == nil && + form.Locked == nil && + form.Source.Privacy == nil && + form.Source.Sensitive == nil && + form.Source.Language == nil && + form.FieldsAttributes == nil) { + return nil, errors.New("empty form submitted") + } + // parse source field-by-field sourceMap := c.PostFormMap("source") diff --git a/internal/api/client/account/accountupdate_test.go b/internal/api/client/account/accountupdate_test.go index c6d07d9dc..91f886721 100644 --- a/internal/api/client/account/accountupdate_test.go +++ b/internal/api/client/account/accountupdate_test.go @@ -26,7 +26,6 @@ "net/http/httptest" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/api/client/account" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" @@ -65,7 +64,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandler() // check the response b, err := ioutil.ReadAll(result.Body) - assert.NoError(suite.T(), err) + suite.NoError(err) // unmarshal the returned account apimodelAccount := &apimodel.Account{} @@ -104,7 +103,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUnl // check the response b1, err := ioutil.ReadAll(result1.Body) - assert.NoError(suite.T(), err) + suite.NoError(err) // unmarshal the returned account apimodelAccount1 := &apimodel.Account{} @@ -185,7 +184,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerGet // check the response b, err := ioutil.ReadAll(result.Body) - assert.NoError(suite.T(), err) + suite.NoError(err) // unmarshal the returned account apimodelAccount := &apimodel.Account{} @@ -227,7 +226,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerTwo // check the response b, err := ioutil.ReadAll(result.Body) - assert.NoError(suite.T(), err) + suite.NoError(err) // unmarshal the returned account apimodelAccount := &apimodel.Account{} @@ -271,7 +270,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerWit // check the response b, err := ioutil.ReadAll(result.Body) - assert.NoError(suite.T(), err) + suite.NoError(err) // unmarshal the returned account apimodelAccount := &apimodel.Account{} @@ -313,8 +312,8 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerEmp // check the response b, err := ioutil.ReadAll(result.Body) - assert.NoError(suite.T(), err) - suite.Equal(`{"error":"empty form submitted"}`, string(b)) + suite.NoError(err) + suite.Equal(`{"error":"Bad Request: empty form submitted"}`, string(b)) } func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpdateSource() { @@ -348,7 +347,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpd // check the response b, err := ioutil.ReadAll(result.Body) - assert.NoError(suite.T(), err) + suite.NoError(err) // unmarshal the returned account apimodelAccount := &apimodel.Account{} diff --git a/internal/api/client/account/accountverify.go b/internal/api/client/account/accountverify.go index f3d0643cc..e6cb65efd 100644 --- a/internal/api/client/account/accountverify.go +++ b/internal/api/client/account/accountverify.go @@ -21,10 +21,9 @@ import ( "net/http" - "github.com/sirupsen/logrus" - "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -47,30 +46,31 @@ // '200': // schema: // "$ref": "#/definitions/account" -// '401': -// description: unauthorized // '400': // description: bad request +// '401': +// description: unauthorized // '404': // description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) AccountVerifyGETHandler(c *gin.Context) { - l := logrus.WithField("func", "accountVerifyGETHandler") - authed, err := oauth.Authed(c, true, false, false, true) + authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - acctSensitive, err := m.processor.AccountGet(c.Request.Context(), authed, authed.Account.ID) - if err != nil { - l.Debugf("error getting account from processor: %s", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + acctSensitive, errWithCode := m.processor.AccountGet(c.Request.Context(), authed, authed.Account.ID) + if errWithCode != nil { + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/account/block.go b/internal/api/client/account/block.go index ed602aefc..b5f7fdda8 100644 --- a/internal/api/client/account/block.go +++ b/internal/api/client/account/block.go @@ -19,10 +19,12 @@ package account import ( + "errors" "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" ) @@ -54,33 +56,38 @@ // description: Your relationship to this account. // schema: // "$ref": "#/definitions/accountRelationship" -// '401': -// description: unauthorized // '400': // description: bad request +// '401': +// description: unauthorized // '404': // description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) AccountBlockPOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetAcctID := c.Param(IDKey) if targetAcctID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) + err := errors.New("no account id specified") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } relationship, errWithCode := m.processor.AccountBlockCreate(c.Request.Context(), authed, targetAcctID) if errWithCode != nil { - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/account/follow.go b/internal/api/client/account/follow.go index b17dd9636..11bfbf965 100644 --- a/internal/api/client/account/follow.go +++ b/internal/api/client/account/follow.go @@ -19,11 +19,13 @@ package account import ( + "errors" "net/http" "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -75,39 +77,45 @@ // description: Your relationship to this account. // schema: // "$ref": "#/definitions/accountRelationship" -// '401': -// description: unauthorized // '400': // description: bad request +// '401': +// description: unauthorized // '404': // description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) AccountFollowPOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetAcctID := c.Param(IDKey) if targetAcctID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) + err := errors.New("no account id specified") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } + form := &model.AccountFollowRequest{} if err := c.ShouldBind(form); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + api.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 { - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/account/followers.go b/internal/api/client/account/followers.go index 9addb2ca7..beb82a34e 100644 --- a/internal/api/client/account/followers.go +++ b/internal/api/client/account/followers.go @@ -19,10 +19,12 @@ package account import ( + "errors" "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" ) @@ -56,33 +58,38 @@ // type: array // items: // "$ref": "#/definitions/account" -// '401': -// description: unauthorized // '400': // description: bad request +// '401': +// description: unauthorized // '404': // description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) AccountFollowersGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetAcctID := c.Param(IDKey) if targetAcctID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) + err := errors.New("no account id specified") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } followers, errWithCode := m.processor.AccountFollowersGet(c.Request.Context(), authed, targetAcctID) if errWithCode != nil { - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/account/following.go b/internal/api/client/account/following.go index 6a857f43d..d03bf143d 100644 --- a/internal/api/client/account/following.go +++ b/internal/api/client/account/following.go @@ -19,10 +19,12 @@ package account import ( + "errors" "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" ) @@ -56,33 +58,38 @@ // type: array // items: // "$ref": "#/definitions/account" -// '401': -// description: unauthorized // '400': // description: bad request +// '401': +// description: unauthorized // '404': // description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) AccountFollowingGETHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetAcctID := c.Param(IDKey) if targetAcctID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) + err := errors.New("no account id specified") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } following, errWithCode := m.processor.AccountFollowingGet(c.Request.Context(), authed, targetAcctID) if errWithCode != nil { - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/account/relationships.go b/internal/api/client/account/relationships.go index 22ae835f0..8facc118c 100644 --- a/internal/api/client/account/relationships.go +++ b/internal/api/client/account/relationships.go @@ -1,13 +1,13 @@ package account import ( + "errors" "net/http" - "github.com/sirupsen/logrus" - "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -43,24 +43,25 @@ // type: array // items: // "$ref": "#/definitions/accountRelationship" -// '401': -// description: unauthorized // '400': // description: bad request +// '401': +// description: unauthorized // '404': // description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) { - l := logrus.WithField("func", "AccountRelationshipsGETHandler") - authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debugf("error authing: %s", err) - c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } @@ -69,8 +70,8 @@ func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) { // check fallback -- let's be generous and see if maybe it's just set as 'id'? id := c.Query("id") if id == "" { - l.Debug("no account id specified in query") - c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) + err = errors.New("no account id(s) specified in query") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } targetAccountIDs = append(targetAccountIDs, id) @@ -80,8 +81,8 @@ func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) { for _, targetAccountID := range targetAccountIDs { r, errWithCode := m.processor.AccountRelationshipGet(c.Request.Context(), authed, targetAccountID) - if err != nil { - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + if errWithCode != nil { + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } relationships = append(relationships, *r) diff --git a/internal/api/client/account/statuses.go b/internal/api/client/account/statuses.go index 18b551fcc..2f0b804d3 100644 --- a/internal/api/client/account/statuses.go +++ b/internal/api/client/account/statuses.go @@ -19,13 +19,14 @@ package account import ( + "errors" + "fmt" "net/http" "strconv" - "github.com/sirupsen/logrus" - "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -110,31 +111,32 @@ // type: array // items: // "$ref": "#/definitions/status" -// '401': -// description: unauthorized // '400': // description: bad request +// '401': +// description: unauthorized // '404': // description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) AccountStatusesGETHandler(c *gin.Context) { - l := logrus.WithField("func", "AccountStatusesGETHandler") - authed, err := oauth.Authed(c, false, false, false, false) if err != nil { - l.Debugf("error authing: %s", err) - c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetAcctID := c.Param(IDKey) if targetAcctID == "" { - l.Debug("no account id specified in query") - c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) + err := errors.New("no account id specified") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } @@ -143,8 +145,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { if limitString != "" { i, err := strconv.ParseInt(limitString, 10, 64) if err != nil { - l.Debugf("error parsing limit string: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"}) + err := fmt.Errorf("error parsing %s: %s", LimitKey, err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } limit = int(i) @@ -155,8 +157,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { if excludeRepliesString != "" { i, err := strconv.ParseBool(excludeRepliesString) if err != nil { - l.Debugf("error parsing exclude replies string: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse exclude replies query param"}) + err := fmt.Errorf("error parsing %s: %s", ExcludeRepliesKey, err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } excludeReplies = i @@ -167,8 +169,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { if excludeReblogsString != "" { i, err := strconv.ParseBool(excludeReblogsString) if err != nil { - l.Debugf("error parsing exclude reblogs string: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse exclude reblogs query param"}) + err := fmt.Errorf("error parsing %s: %s", ExcludeReblogsKey, err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } excludeReblogs = i @@ -191,8 +193,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { if pinnedString != "" { i, err := strconv.ParseBool(pinnedString) if err != nil { - l.Debugf("error parsing pinned string: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse pinned query param"}) + err := fmt.Errorf("error parsing %s: %s", PinnedKey, err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } pinnedOnly = i @@ -203,8 +205,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { if mediaOnlyString != "" { i, err := strconv.ParseBool(mediaOnlyString) if err != nil { - l.Debugf("error parsing media only string: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse media only query param"}) + err := fmt.Errorf("error parsing %s: %s", OnlyMediaKey, err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } mediaOnly = i @@ -215,8 +217,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { if publicOnlyString != "" { i, err := strconv.ParseBool(publicOnlyString) if err != nil { - l.Debugf("error parsing public only string: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse public only query param"}) + err := fmt.Errorf("error parsing %s: %s", OnlyPublicKey, err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } publicOnly = i @@ -224,8 +226,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 { - l.Debugf("error from processor account statuses get: %s", errWithCode) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/account/unblock.go b/internal/api/client/account/unblock.go index a8ae0cf21..44f3a722c 100644 --- a/internal/api/client/account/unblock.go +++ b/internal/api/client/account/unblock.go @@ -19,10 +19,12 @@ package account import ( + "errors" "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" ) @@ -54,33 +56,38 @@ // description: Your relationship to this account. // schema: // "$ref": "#/definitions/accountRelationship" -// '401': -// description: unauthorized // '400': // description: bad request +// '401': +// description: unauthorized // '404': // description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) AccountUnblockPOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetAcctID := c.Param(IDKey) if targetAcctID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) + err := errors.New("no account id specified") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } relationship, errWithCode := m.processor.AccountBlockRemove(c.Request.Context(), authed, targetAcctID) if errWithCode != nil { - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/account/unfollow.go b/internal/api/client/account/unfollow.go index e681e2773..d92278f95 100644 --- a/internal/api/client/account/unfollow.go +++ b/internal/api/client/account/unfollow.go @@ -19,12 +19,12 @@ package account import ( + "errors" "net/http" - "github.com/sirupsen/logrus" - "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -56,37 +56,38 @@ // description: Your relationship to this account. // schema: // "$ref": "#/definitions/accountRelationship" -// '401': -// description: unauthorized // '400': // description: bad request +// '401': +// description: unauthorized // '404': // description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) AccountUnfollowPOSTHandler(c *gin.Context) { - l := logrus.WithField("func", "AccountUnfollowPOSTHandler") authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debug(err) - c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetAcctID := c.Param(IDKey) if targetAcctID == "" { - l.Debug(err) - c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) + err := errors.New("no account id specified") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } relationship, errWithCode := m.processor.AccountFollowRemove(c.Request.Context(), authed, targetAcctID) if errWithCode != nil { - l.Debug(errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/admin/accountaction.go b/internal/api/client/admin/accountaction.go index 46473fa73..072a60a9d 100644 --- a/internal/api/client/admin/accountaction.go +++ b/internal/api/client/admin/accountaction.go @@ -19,12 +19,14 @@ package admin import ( + "errors" "fmt" "net/http" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -72,53 +74,47 @@ // description: unauthorized // '403': // description: forbidden +// '404': +// description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) AccountActionPOSTHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "AccountActionPOSTHandler", - "request_uri": c.Request.RequestURI, - "user_agent": c.Request.UserAgent(), - "origin_ip": c.ClientIP(), - }) - - // make sure we're authed... authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - // with an admin account if !authed.User.Admin { - l.Debugf("user %s not an admin", authed.User.ID) - c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) + err := fmt.Errorf("user %s not an admin", authed.User.ID) + api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) return } - // extract the form from the request context - l.Tracef("parsing request form: %+v", c.Request.Form) form := &model.AdminAccountActionRequest{} if err := c.ShouldBind(form); err != nil { - l.Debugf("error parsing form %+v: %s", c.Request.Form, err) - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)}) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } if form.Type == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no type specified"}) + err := errors.New("no type specified") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } targetAcctID := c.Param(IDKey) if targetAcctID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) + err := errors.New("no account id specified") + api.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 { - l.Debugf("error performing account action: %s", errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/admin/domainblockcreate.go b/internal/api/client/admin/domainblockcreate.go index dd5623a1c..e23376a91 100644 --- a/internal/api/client/admin/domainblockcreate.go +++ b/internal/api/client/admin/domainblockcreate.go @@ -7,9 +7,9 @@ "strconv" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -86,33 +86,33 @@ // Note that if a list has been imported, then an `array` of newly created domain blocks will be returned instead. // schema: // "$ref": "#/definitions/domainBlock" -// '403': -// description: forbidden // '400': // description: bad request +// '401': +// description: unauthorized +// '403': +// description: forbidden +// '404': +// description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "DomainBlocksPOSTHandler", - "request_uri": c.Request.RequestURI, - "user_agent": c.Request.UserAgent(), - "origin_ip": c.ClientIP(), - }) - - // make sure we're authed with an admin account authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } + if !authed.User.Admin { - l.Debugf("user %s not an admin", authed.User.ID) - c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) + err := fmt.Errorf("user %s not an admin", authed.User.ID) + api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } @@ -121,49 +121,43 @@ func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) { if importString != "" { i, err := strconv.ParseBool(importString) if err != nil { - l.Debugf("error parsing import string: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse import query param"}) + err := fmt.Errorf("error parsing %s: %s", ImportQueryKey, err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } imp = i } - // extract the media create form from the request context - l.Tracef("parsing request form: %+v", c.Request.Form) form := &model.DomainBlockCreateRequest{} if err := c.ShouldBind(form); err != nil { - l.Debugf("error parsing form %+v: %s", c.Request.Form, err) - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)}) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - // Give the fields on the request form a first pass to make sure the request is superficially valid. - l.Tracef("validating form %+v", form) if err := validateCreateDomainBlock(form, imp); err != nil { - l.Debugf("error validating form: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + err := fmt.Errorf("error validating form: %s", err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } if imp { // we're importing multiple blocks - domainBlocks, err := m.processor.AdminDomainBlocksImport(c.Request.Context(), authed, form) - if err != nil { - l.Debugf("error importing domain blocks: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + domainBlocks, errWithCode := m.processor.AdminDomainBlocksImport(c.Request.Context(), authed, form) + if errWithCode != nil { + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } c.JSON(http.StatusOK, domainBlocks) - } else { - // we're just creating one block - domainBlock, err := m.processor.AdminDomainBlockCreate(c.Request.Context(), authed, form) - if err != nil { - l.Debugf("error creating domain block: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - c.JSON(http.StatusOK, domainBlock) + return } + + // 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) + return + } + c.JSON(http.StatusOK, domainBlock) } func validateCreateDomainBlock(form *model.DomainBlockCreateRequest, imp bool) error { diff --git a/internal/api/client/admin/domainblockdelete.go b/internal/api/client/admin/domainblockdelete.go index 8d41ec072..416db002b 100644 --- a/internal/api/client/admin/domainblockdelete.go +++ b/internal/api/client/admin/domainblockdelete.go @@ -1,11 +1,13 @@ package admin import ( + "errors" + "fmt" "net/http" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -36,48 +38,46 @@ // description: The domain block that was just deleted. // schema: // "$ref": "#/definitions/domainBlock" -// '403': -// description: forbidden // '400': // description: bad request +// '401': +// description: unauthorized +// '403': +// description: forbidden // '404': // description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) DomainBlockDELETEHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "DomainBlockDELETEHandler", - "request_uri": c.Request.RequestURI, - "user_agent": c.Request.UserAgent(), - "origin_ip": c.ClientIP(), - }) - - // make sure we're authed with an admin account authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } + if !authed.User.Admin { - l.Debugf("user %s not an admin", authed.User.ID) - c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) + err := fmt.Errorf("user %s not an admin", authed.User.ID) + api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } domainBlockID := c.Param(IDKey) if domainBlockID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no domain block id provided"}) + err := errors.New("no domain block id specified") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } domainBlock, errWithCode := m.processor.AdminDomainBlockDelete(c.Request.Context(), authed, domainBlockID) if errWithCode != nil { - l.Debugf("error deleting domain block: %s", errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/admin/domainblockget.go b/internal/api/client/admin/domainblockget.go index 49a0795d7..15456242c 100644 --- a/internal/api/client/admin/domainblockget.go +++ b/internal/api/client/admin/domainblockget.go @@ -1,12 +1,14 @@ package admin import ( + "errors" + "fmt" "net/http" "strconv" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -37,41 +39,40 @@ // description: The requested domain block. // schema: // "$ref": "#/definitions/domainBlock" -// '403': -// description: forbidden // '400': // description: bad request +// '401': +// description: unauthorized +// '403': +// description: forbidden // '404': // description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) DomainBlockGETHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "DomainBlockGETHandler", - "request_uri": c.Request.RequestURI, - "user_agent": c.Request.UserAgent(), - "origin_ip": c.ClientIP(), - }) - - // make sure we're authed with an admin account authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } + if !authed.User.Admin { - l.Debugf("user %s not an admin", authed.User.ID) - c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) + err := fmt.Errorf("user %s not an admin", authed.User.ID) + api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } domainBlockID := c.Param(IDKey) if domainBlockID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no domain block id provided"}) + err := errors.New("no domain block id specified") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } @@ -80,17 +81,16 @@ func (m *Module) DomainBlockGETHandler(c *gin.Context) { if exportString != "" { i, err := strconv.ParseBool(exportString) if err != nil { - l.Debugf("error parsing export string: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse export query param"}) + err := fmt.Errorf("error parsing %s: %s", ExportQueryKey, err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } export = i } - domainBlock, err := m.processor.AdminDomainBlockGet(c.Request.Context(), authed, domainBlockID, export) - if err != nil { - l.Debugf("error getting domain block: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + domainBlock, errWithCode := m.processor.AdminDomainBlockGet(c.Request.Context(), authed, domainBlockID, export) + if errWithCode != nil { + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/admin/domainblocksget.go b/internal/api/client/admin/domainblocksget.go index 94b1c89ce..d585e837d 100644 --- a/internal/api/client/admin/domainblocksget.go +++ b/internal/api/client/admin/domainblocksget.go @@ -1,12 +1,14 @@ package admin import ( + "errors" + "fmt" "net/http" "strconv" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -43,35 +45,40 @@ // type: array // items: // "$ref": "#/definitions/domainBlock" -// '403': -// description: forbidden // '400': // description: bad request +// '401': +// description: unauthorized +// '403': +// description: forbidden // '404': // description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) DomainBlocksGETHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "DomainBlocksGETHandler", - "request_uri": c.Request.RequestURI, - "user_agent": c.Request.UserAgent(), - "origin_ip": c.ClientIP(), - }) - - // make sure we're authed with an admin account authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } + if !authed.User.Admin { - l.Debugf("user %s not an admin", authed.User.ID) - c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) + err := fmt.Errorf("user %s not an admin", authed.User.ID) + api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.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) return } @@ -80,17 +87,16 @@ func (m *Module) DomainBlocksGETHandler(c *gin.Context) { if exportString != "" { i, err := strconv.ParseBool(exportString) if err != nil { - l.Debugf("error parsing export string: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse export query param"}) + err := fmt.Errorf("error parsing %s: %s", ExportQueryKey, err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } export = i } - domainBlocks, err := m.processor.AdminDomainBlocksGet(c.Request.Context(), authed, export) - if err != nil { - l.Debugf("error getting domain blocks: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + domainBlocks, errWithCode := m.processor.AdminDomainBlocksGet(c.Request.Context(), authed, export) + if errWithCode != nil { + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/admin/emojicreate.go b/internal/api/client/admin/emojicreate.go index ef42d0a13..c5d613312 100644 --- a/internal/api/client/admin/emojicreate.go +++ b/internal/api/client/admin/emojicreate.go @@ -24,9 +24,9 @@ "net/http" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/validate" ) @@ -69,59 +69,52 @@ // description: The newly-created emoji. // schema: // "$ref": "#/definitions/emoji" -// '403': -// description: forbidden // '400': // description: bad request +// '401': +// description: unauthorized +// '403': +// description: forbidden +// '404': +// description: not found +// '406': +// description: not acceptable // '409': // description: conflict -- domain/shortcode combo for emoji already exists +// '500': +// description: internal server error func (m *Module) EmojiCreatePOSTHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "emojiCreatePOSTHandler", - "request_uri": c.Request.RequestURI, - "user_agent": c.Request.UserAgent(), - "origin_ip": c.ClientIP(), - }) - - // make sure we're authed with an admin account - authed, err := oauth.Authed(c, true, true, true, true) // posting a status is serious business so we want *everything* + authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } + if !authed.User.Admin { - l.Debugf("user %s not an admin", authed.User.ID) - c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) + err := fmt.Errorf("user %s not an admin", authed.User.ID) + api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - // extract the media create form from the request context - l.Tracef("parsing request form: %+v", c.Request.Form) form := &model.EmojiCreateRequest{} if err := c.ShouldBind(form); err != nil { - l.Debugf("error parsing form %+v: %s", c.Request.Form, err) - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)}) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - // Give the fields on the request form a first pass to make sure the request is superficially valid. - l.Tracef("validating form %+v", form) if err := validateCreateEmoji(form); err != nil { - l.Debugf("error validating form: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } apiEmoji, errWithCode := m.processor.AdminEmojiCreate(c.Request.Context(), authed, form) if errWithCode != nil { - l.Debugf("error creating emoji: %s", errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } @@ -129,7 +122,6 @@ func (m *Module) EmojiCreatePOSTHandler(c *gin.Context) { } func validateCreateEmoji(form *model.EmojiCreateRequest) error { - // check there actually is an image attached and it's not size 0 if form.Image == nil || form.Image.Size == 0 { return errors.New("no emoji given") } diff --git a/internal/api/client/admin/emojicreate_test.go b/internal/api/client/admin/emojicreate_test.go index 2b7476da1..1b6ddf96f 100644 --- a/internal/api/client/admin/emojicreate_test.go +++ b/internal/api/client/admin/emojicreate_test.go @@ -120,7 +120,7 @@ func (suite *EmojiCreateTestSuite) TestEmojiCreateAlreadyExists() { suite.NoError(err) suite.NotEmpty(b) - suite.Equal(`{"error":"conflict: emoji with shortcode rainbow already exists"}`, string(b)) + suite.Equal(`{"error":"Conflict: emoji with shortcode rainbow already exists"}`, string(b)) } func TestEmojiCreateTestSuite(t *testing.T) { diff --git a/internal/api/client/admin/mediacleanup.go b/internal/api/client/admin/mediacleanup.go index 02eec82f3..6065acd3b 100644 --- a/internal/api/client/admin/mediacleanup.go +++ b/internal/api/client/admin/mediacleanup.go @@ -23,9 +23,10 @@ "net/http" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -54,39 +55,34 @@ // '200': // description: |- // Echos the number of days requested. The cleanup is performed asynchronously after the request completes. -// '403': -// description: forbidden // '400': // description: bad request +// '401': +// description: unauthorized +// '403': +// description: forbidden +// '404': +// description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) MediaCleanupPOSTHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "MediaCleanupPOSTHandler", - "request_uri": c.Request.RequestURI, - "user_agent": c.Request.UserAgent(), - "origin_ip": c.ClientIP(), - }) - - // make sure we're authed... authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - // with an admin account if !authed.User.Admin { - l.Debugf("user %s not an admin", authed.User.ID) - c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) + err := fmt.Errorf("user %s not an admin", authed.User.ID) + api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet) return } - // extract the form from the request context - l.Tracef("parsing request form: %+v", c.Request.Form) form := &model.MediaCleanupRequest{} if err := c.ShouldBind(form); err != nil { - l.Debugf("error parsing form %+v: %s", c.Request.Form, err) - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)}) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } @@ -101,8 +97,7 @@ func (m *Module) MediaCleanupPOSTHandler(c *gin.Context) { } if errWithCode := m.processor.AdminMediaPrune(c.Request.Context(), remoteCacheDays); errWithCode != nil { - l.Debugf("error starting prune of remote media: %s", errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/app/appcreate.go b/internal/api/client/app/appcreate.go index 2e101547c..d402628de 100644 --- a/internal/api/client/app/appcreate.go +++ b/internal/api/client/app/appcreate.go @@ -22,18 +22,16 @@ "fmt" "net/http" - "github.com/sirupsen/logrus" - "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) +// these consts are used to ensure users can't spam huge entries into our database const ( - // permitted length for most fields - formFieldLen = 64 - // redirect can be a bit bigger because we probably need to encode data in the redirect uri + formFieldLen = 64 formRedirectLen = 512 ) @@ -64,56 +62,63 @@ // description: "The newly-created application." // schema: // "$ref": "#/definitions/application" -// '401': -// description: unauthorized // '400': // description: bad request -// '422': -// description: unprocessable +// '401': +// description: unauthorized +// '403': +// description: forbidden +// '404': +// description: not found +// '406': +// description: not acceptable // '500': -// description: internal error +// description: internal server error func (m *Module) AppsPOSTHandler(c *gin.Context) { - l := logrus.WithField("func", "AppsPOSTHandler") - l.Trace("entering AppsPOSTHandler") - authed, err := oauth.Authed(c, false, false, false, false) if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } form := &model.ApplicationCreateRequest{} if err := c.ShouldBind(form); err != nil { - c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - // check lengths of fields before proceeding so the user can't spam huge entries into the database if len(form.ClientName) > formFieldLen { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("client_name must be less than %d bytes", formFieldLen)}) - return - } - if len(form.Website) > formFieldLen { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("website must be less than %d bytes", formFieldLen)}) - return - } - if len(form.RedirectURIs) > formRedirectLen { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("redirect_uris must be less than %d bytes", formRedirectLen)}) - return - } - if len(form.Scopes) > formFieldLen { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("scopes must be less than %d bytes", formFieldLen)}) + err := fmt.Errorf("client_name must be less than %d bytes", formFieldLen) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - apiApp, err := m.processor.AppCreate(c.Request.Context(), authed, form) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + if len(form.RedirectURIs) > formRedirectLen { + err := fmt.Errorf("redirect_uris must be less than %d bytes", formRedirectLen) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + return + } + + if len(form.Scopes) > formFieldLen { + err := fmt.Errorf("scopes must be less than %d bytes", formFieldLen) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + return + } + + if len(form.Website) > formFieldLen { + err := fmt.Errorf("website must be less than %d bytes", formFieldLen) + api.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) return } diff --git a/internal/api/client/auth/auth.go b/internal/api/client/auth/auth.go index 717d997a3..10d374838 100644 --- a/internal/api/client/auth/auth.go +++ b/internal/api/client/auth/auth.go @@ -25,6 +25,7 @@ "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" ) @@ -66,17 +67,19 @@ // Module implements the ClientAPIModule interface for type Module struct { - db db.DB - server oauth.Server - idp oidc.IDP + db db.DB + server oauth.Server + idp oidc.IDP + processor processing.Processor } // New returns a new auth module -func New(db db.DB, server oauth.Server, idp oidc.IDP) api.ClientModule { +func New(db db.DB, server oauth.Server, idp oidc.IDP, processor processing.Processor) api.ClientModule { return &Module{ - db: db, - server: server, - idp: idp, + db: db, + server: server, + idp: idp, + processor: processor, } } diff --git a/internal/api/client/auth/auth_test.go b/internal/api/client/auth/auth_test.go index bd1a1948e..f222f714f 100644 --- a/internal/api/client/auth/auth_test.go +++ b/internal/api/client/auth/auth_test.go @@ -23,25 +23,37 @@ "fmt" "net/http/httptest" + "codeberg.org/gruf/go-store/kv" "github.com/gin-contrib/sessions" "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/concurrency" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/email" + "github.com/superseriousbusiness/gotosocial/internal/federation" "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/oidc" + "github.com/superseriousbusiness/gotosocial/internal/processing" "github.com/superseriousbusiness/gotosocial/internal/router" "github.com/superseriousbusiness/gotosocial/testrig" ) type AuthStandardTestSuite struct { suite.Suite - db db.DB - idp oidc.IDP - oauthServer oauth.Server + db db.DB + storage *kv.KVStore + mediaManager media.Manager + federator federation.Federator + processor processing.Processor + emailSender email.Sender + idp oidc.IDP + oauthServer oauth.Server // standard suite models testTokens map[string]*gtsmodel.Token @@ -69,17 +81,26 @@ func (suite *AuthStandardTestSuite) SetupSuite() { func (suite *AuthStandardTestSuite) SetupTest() { testrig.InitTestConfig() - suite.db = testrig.NewTestDB() testrig.InitTestLog() + fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) + clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1) + + suite.db = testrig.NewTestDB() + suite.storage = testrig.NewTestStorage() + suite.mediaManager = testrig.NewTestMediaManager(suite.db, suite.storage) + suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), 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.oauthServer, suite.idp).(*auth.Module) - testrig.StandardDBSetup(suite.db, nil) + suite.authModule = auth.New(suite.db, suite.oauthServer, suite.idp, suite.processor).(*auth.Module) + testrig.StandardDBSetup(suite.db, suite.testAccounts) } func (suite *AuthStandardTestSuite) TearDownTest() { @@ -92,7 +113,7 @@ func (suite *AuthStandardTestSuite) newContext(requestMethod string, requestPath ctx, engine := gin.CreateTestContext(recorder) // load templates into the engine - testrig.ConfigureTemplatesWithGin(engine) + testrig.ConfigureTemplatesWithGin(engine, "../../../../web/template") // create the request protocol := config.GetProtocol() diff --git a/internal/api/client/auth/authorize.go b/internal/api/client/auth/authorize.go index 387b83c1e..6f96484a8 100644 --- a/internal/api/client/auth/authorize.go +++ b/internal/api/client/auth/authorize.go @@ -23,9 +23,6 @@ "fmt" "net/http" "net/url" - "strings" - - "github.com/sirupsen/logrus" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" @@ -33,18 +30,22 @@ "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) +// helpfulAdvice is a handy hint to users; +// particularly important during the login flow +var helpfulAdvice = "If you arrived at this error during a login/oauth flow, please try clearing your session cookies and logging in again; if problems persist, make sure you're using the correct credentials" + // AuthorizeGETHandler should be served as GET at https://example.org/oauth/authorize // The idea here is to present an oauth authorize page to the user, with a button // that they have to click to accept. func (m *Module) AuthorizeGETHandler(c *gin.Context) { - l := logrus.WithField("func", "AuthorizeGETHandler") s := sessions.Default(c) if _, err := api.NegotiateAccept(c, api.HTMLAcceptHeaders...); err != nil { - c.HTML(http.StatusNotAcceptable, "error.tmpl", gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } @@ -52,56 +53,75 @@ 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 == "" { - l.Trace("userid was empty, parsing form then redirecting to sign in page") form := &model.OAuthAuthorize{} - if err := c.Bind(form); err != nil { - l.Debugf("invalid auth form: %s", err) + if err := c.ShouldBind(form); err != nil { m.clearSession(s) - c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet) return } - l.Debugf("parsed auth form: %+v", form) - if err := extractAuthForm(s, form); err != nil { - l.Debugf(fmt.Sprintf("error parsing form at /oauth/authorize: %s", err)) + if errWithCode := saveAuthFormToSession(s, form); errWithCode != nil { m.clearSession(s) - c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"error": err.Error()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } + c.Redirect(http.StatusSeeOther, AuthSignInPath) return } - // We can use the client_id on the session to retrieve info about the app associated with the client_id + // use session information to validate app, user, and account for this request clientID, ok := s.Get(sessionClientID).(string) if !ok || clientID == "" { - c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": "no client_id found in session"}) - return - } - app := >smodel.Application{} - if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil { m.clearSession(s) - c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{ - "error": fmt.Sprintf("no application found for client id %s", clientID), - }) + err := fmt.Errorf("key %s was not found in session", sessionClientID) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet) + return + } + + app := >smodel.Application{} + if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil { + m.clearSession(s) + safe := fmt.Sprintf("application for %s %s could not be retrieved", sessionClientID, clientID) + var errWithCode gtserror.WithCode + if err == db.ErrNoEntries { + errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice) + } else { + errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice) + } + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } - // redirect the user if they have not confirmed their email yet, thier account has not been approved yet, - // or thier account has been disabled. user := >smodel.User{} if err := m.db.GetByID(c.Request.Context(), userID, user); err != nil { m.clearSession(s) - c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": err.Error()}) + safe := fmt.Sprintf("user with id %s could not be retrieved", userID) + var errWithCode gtserror.WithCode + if err == db.ErrNoEntries { + errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice) + } else { + errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice) + } + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } + acct, err := m.db.GetAccountByID(c.Request.Context(), user.AccountID) if err != nil { m.clearSession(s) - c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": err.Error()}) + safe := fmt.Sprintf("account with id %s could not be retrieved", user.AccountID) + var errWithCode gtserror.WithCode + if err == db.ErrNoEntries { + errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice) + } else { + errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice) + } + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } - if !ensureUserIsAuthorizedOrRedirect(c, user, acct) { + + if ensureUserIsAuthorizedOrRedirect(c, user, acct) { return } @@ -109,25 +129,27 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) { redirect, ok := s.Get(sessionRedirectURI).(string) if !ok || redirect == "" { m.clearSession(s) - c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": "no redirect_uri found in session"}) + err := fmt.Errorf("key %s was not found in session", sessionRedirectURI) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet) return } + scope, ok := s.Get(sessionScope).(string) if !ok || scope == "" { m.clearSession(s) - c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": "no scope found in session"}) + err := fmt.Errorf("key %s was not found in session", sessionScope) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet) return } // the authorize template will display a form to the user where they can get some information // about the app that's trying to authorize, and the scope of the request. // They can then approve it if it looks OK to them, which will POST to the AuthorizePOSTHandler - l.Trace("serving authorize html") c.HTML(http.StatusOK, "authorize.tmpl", gin.H{ "appname": app.Name, "appwebsite": app.Website, "redirect": redirect, - sessionScope: scope, + "scope": scope, "user": acct.Username, }) } @@ -136,13 +158,10 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) { // At this point we assume that the user has A) logged in and B) accepted that the app should act for them, // so we should proceed with the authentication flow and generate an oauth token for them if we can. func (m *Module) AuthorizePOSTHandler(c *gin.Context) { - l := logrus.WithField("func", "AuthorizePOSTHandler") s := sessions.Default(c) // We need to retrieve the original form submitted to the authorizeGEThandler, and // recreate it on the request so that it can be used further by the oauth2 library. - // So first fetch all the values from the session. - errs := []string{} forceLogin, ok := s.Get(sessionForceLogin).(string) @@ -152,77 +171,107 @@ func (m *Module) AuthorizePOSTHandler(c *gin.Context) { responseType, ok := s.Get(sessionResponseType).(string) if !ok || responseType == "" { - errs = append(errs, "session missing response_type") + errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionResponseType)) } clientID, ok := s.Get(sessionClientID).(string) if !ok || clientID == "" { - errs = append(errs, "session missing client_id") + errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionClientID)) } redirectURI, ok := s.Get(sessionRedirectURI).(string) if !ok || redirectURI == "" { - errs = append(errs, "session missing redirect_uri") + errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionRedirectURI)) } scope, ok := s.Get(sessionScope).(string) if !ok { - errs = append(errs, "session missing scope") + errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionScope)) } userID, ok := s.Get(sessionUserID).(string) if !ok { - errs = append(errs, "session missing userid") + errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionUserID)) + } + + if len(errs) != 0 { + errs = append(errs, helpfulAdvice) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(errors.New("one or more missing keys on session during AuthorizePOSTHandler"), errs...), m.processor.InstanceGet) + return } - // redirect the user if they have not confirmed their email yet, thier account has not been approved yet, - // or thier account has been disabled. user := >smodel.User{} if err := m.db.GetByID(c.Request.Context(), userID, user); err != nil { m.clearSession(s) - c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": err.Error()}) + safe := fmt.Sprintf("user with id %s could not be retrieved", userID) + var errWithCode gtserror.WithCode + if err == db.ErrNoEntries { + errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice) + } else { + errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice) + } + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } + acct, err := m.db.GetAccountByID(c.Request.Context(), user.AccountID) if err != nil { m.clearSession(s) - c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": err.Error()}) - return - } - if !ensureUserIsAuthorizedOrRedirect(c, user, acct) { + safe := fmt.Sprintf("account with id %s could not be retrieved", user.AccountID) + var errWithCode gtserror.WithCode + if err == db.ErrNoEntries { + errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice) + } else { + errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice) + } + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } + if ensureUserIsAuthorizedOrRedirect(c, user, acct) { + return + } + + // we're done with the session now, so just clear it out m.clearSession(s) - if len(errs) != 0 { - c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"error": strings.Join(errs, ": ")}) - return + // we have to set the values on the request form + // so that they're picked up by the oauth server + c.Request.Form = url.Values{ + sessionForceLogin: {forceLogin}, + sessionResponseType: {responseType}, + sessionClientID: {clientID}, + sessionRedirectURI: {redirectURI}, + sessionScope: {scope}, + sessionUserID: {userID}, } - // now set the values on the request - values := url.Values{} - values.Set(sessionForceLogin, forceLogin) - values.Set(sessionResponseType, responseType) - values.Set(sessionClientID, clientID) - values.Set(sessionRedirectURI, redirectURI) - values.Set(sessionScope, scope) - values.Set(sessionUserID, userID) - c.Request.Form = values - l.Tracef("values on request set to %+v", c.Request.Form) - - // and proceed with authorization using the oauth2 library if err := m.server.HandleAuthorizeRequest(c.Writer, c.Request); err != nil { - c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice), m.processor.InstanceGet) } } -// extractAuthForm checks the given OAuthAuthorize form, and stores -// the values in the form into the session. -func extractAuthForm(s sessions.Session, form *model.OAuthAuthorize) error { - // these fields are *required* so check 'em - if form.ResponseType == "" || form.ClientID == "" || form.RedirectURI == "" { - return errors.New("missing one of: response_type, client_id or redirect_uri") +// 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 { + if form == nil { + err := errors.New("OAuthAuthorize form was nil") + return gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice) + } + + if form.ResponseType == "" { + err := errors.New("field response_type was not set on OAuthAuthorize form") + return gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice) + } + + if form.ClientID == "" { + err := errors.New("field client_id was not set on OAuthAuthorize form") + return gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice) + } + + if form.RedirectURI == "" { + err := errors.New("field redirect_uri was not set on OAuthAuthorize form") + return gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice) } // set default scope to read @@ -237,29 +286,33 @@ func extractAuthForm(s sessions.Session, form *model.OAuthAuthorize) error { s.Set(sessionRedirectURI, form.RedirectURI) s.Set(sessionScope, form.Scope) s.Set(sessionState, uuid.NewString()) - return s.Save() + + if err := s.Save(); err != nil { + err := fmt.Errorf("error saving form values onto session: %s", err) + return gtserror.NewErrorInternalError(err, helpfulAdvice) + } + + return nil } -func ensureUserIsAuthorizedOrRedirect(ctx *gin.Context, user *gtsmodel.User, account *gtsmodel.Account) bool { +func ensureUserIsAuthorizedOrRedirect(ctx *gin.Context, user *gtsmodel.User, account *gtsmodel.Account) (redirected bool) { if user.ConfirmedAt.IsZero() { ctx.Redirect(http.StatusSeeOther, CheckYourEmailPath) - return false + redirected = true + return } if !user.Approved { ctx.Redirect(http.StatusSeeOther, WaitForApprovalPath) - return false + redirected = true + return } - if user.Disabled { + if user.Disabled || !account.SuspendedAt.IsZero() { ctx.Redirect(http.StatusSeeOther, AccountDisabledPath) - return false + redirected = true + return } - if !account.SuspendedAt.IsZero() { - ctx.Redirect(http.StatusSeeOther, AccountDisabledPath) - return false - } - - return true + return } diff --git a/internal/api/client/auth/callback.go b/internal/api/client/auth/callback.go index a5c58647c..34a4995c8 100644 --- a/internal/api/client/auth/callback.go +++ b/internal/api/client/auth/callback.go @@ -30,7 +30,9 @@ "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/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/oidc" "github.com/superseriousbusiness/gotosocial/internal/validate" @@ -40,11 +42,14 @@ func (m *Module) CallbackGETHandler(c *gin.Context) { s := sessions.Default(c) - // first make sure the state set in the cookie is the same as the state returned from the external provider + // check the query vs session state parameter to mitigate csrf + // https://auth0.com/docs/secure/attack-protection/state-parameters + state := c.Query(callbackStateParam) if state == "" { m.clearSession(s) - c.JSON(http.StatusForbidden, gin.H{"error": "state query not found on callback"}) + err := fmt.Errorf("%s parameter not found on callback query", callbackStateParam) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } @@ -52,84 +57,104 @@ func (m *Module) CallbackGETHandler(c *gin.Context) { savedState, ok := savedStateI.(string) if !ok { m.clearSession(s) - c.JSON(http.StatusForbidden, gin.H{"error": "state not found in session"}) + err := fmt.Errorf("key %s was not found in session", sessionState) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } if state != savedState { m.clearSession(s) - c.JSON(http.StatusForbidden, gin.H{"error": "state mismatch"}) + err := errors.New("mismatch between query state and session state") + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } + // retrieve stored claims using code code := c.Query(callbackCodeParam) - - claims, err := m.idp.HandleCallback(c.Request.Context(), code) - if err != nil { + if code == "" { m.clearSession(s) - c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + err := fmt.Errorf("%s parameter not found on callback query", callbackCodeParam) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - // We can use the client_id on the session to retrieve info about the app associated with the client_id + claims, errWithCode := m.idp.HandleCallback(c.Request.Context(), code) + if errWithCode != nil { + m.clearSession(s) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + return + } + + // We can use the client_id on the session to retrieve + // info about the app associated with the client_id clientID, ok := s.Get(sessionClientID).(string) if !ok || clientID == "" { m.clearSession(s) - c.JSON(http.StatusInternalServerError, gin.H{"error": "no client_id found in session during callback"}) - return - } - app := >smodel.Application{} - if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil { - m.clearSession(s) - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("no application found for client id %s", clientID)}) + err := fmt.Errorf("key %s was not found in session", sessionClientID) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet) return } - user, err := m.parseUserFromClaims(c.Request.Context(), claims, net.IP(c.ClientIP()), app.ID) - if err != nil { + app := >smodel.Application{} + if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil { m.clearSession(s) - c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + safe := fmt.Sprintf("application for %s %s could not be retrieved", sessionClientID, clientID) + var errWithCode gtserror.WithCode + if err == db.ErrNoEntries { + errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice) + } else { + errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice) + } + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + return + } + + user, errWithCode := m.parseUserFromClaims(c.Request.Context(), claims, net.IP(c.ClientIP()), app.ID) + if errWithCode != nil { + m.clearSession(s) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } s.Set(sessionUserID, user.ID) if err := s.Save(); err != nil { m.clearSession(s) - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } c.Redirect(http.StatusFound, OauthAuthorizePath) } -func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, ip net.IP, appID string) (*gtsmodel.User, error) { +func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, ip net.IP, appID string) (*gtsmodel.User, gtserror.WithCode) { if claims.Email == "" { - return nil, errors.New("no email returned in claims") + err := errors.New("no email returned in claims") + return nil, gtserror.NewErrorBadRequest(err, err.Error()) } // see if we already have a user for this email address + // if so, we don't need to continue + create one user := >smodel.User{} err := m.db.GetWhere(ctx, []db.Where{{Key: "email", Value: claims.Email}}, user) if err == nil { - // we do! so we can just return it return user, nil } if err != db.ErrNoEntries { - // we have an actual error in the database - return nil, fmt.Errorf("error checking database for email %s: %s", claims.Email, err) + err := fmt.Errorf("error checking database for email %s: %s", claims.Email, err) + return nil, gtserror.NewErrorInternalError(err) } // maybe we have an unconfirmed user err = m.db.GetWhere(ctx, []db.Where{{Key: "unconfirmed_email", Value: claims.Email}}, user) if err == nil { - // user is unconfirmed so return an error - return nil, fmt.Errorf("user with email address %s is unconfirmed", claims.Email) + err := fmt.Errorf("user with email address %s is unconfirmed", claims.Email) + return nil, gtserror.NewErrorForbidden(err, err.Error()) } if err != db.ErrNoEntries { - // we have an actual error in the database - return nil, fmt.Errorf("error checking database for email %s: %s", claims.Email, err) + err := fmt.Errorf("error checking database for email %s: %s", claims.Email, err) + return nil, gtserror.NewErrorInternalError(err) } // we don't have a confirmed or unconfirmed user with the claimed email address @@ -138,10 +163,10 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i // check if the email address is available for use; if it's not there's nothing we can so emailAvailable, err := m.db.IsEmailAvailable(ctx, claims.Email) if err != nil { - return nil, fmt.Errorf("email %s not available: %s", claims.Email, err) + return nil, gtserror.NewErrorBadRequest(err) } if !emailAvailable { - return nil, fmt.Errorf("email %s in use", claims.Email) + return nil, gtserror.NewErrorConflict(fmt.Errorf("email address %s is not available", claims.Email)) } // now we need a username @@ -149,12 +174,12 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i // make sure claims.Name is defined since we'll be using that for the username if claims.Name == "" { - return nil, errors.New("no name returned in claims") + err := errors.New("no name returned in claims") + return nil, gtserror.NewErrorBadRequest(err, err.Error()) } // check if we can just use claims.Name as-is - err = validate.Username(claims.Name) - if err == nil { + if err = validate.Username(claims.Name); err == nil { // the name we have on the claims is already a valid username username = claims.Name } else { @@ -166,12 +191,12 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i // lowercase the whole thing lower := strings.ToLower(underscored) // see if this is valid.... - if err := validate.Username(lower); err == nil { - // we managed to get a valid username - username = lower - } else { - return nil, fmt.Errorf("couldn't parse a valid username from claims.Name value of %s", claims.Name) + if err := validate.Username(lower); err != nil { + err := fmt.Errorf("couldn't parse a valid username from claims.Name value of %s: %s", claims.Name, err) + return nil, gtserror.NewErrorBadRequest(err, err.Error()) } + // we managed to get a valid username + username = lower } var iString string @@ -185,7 +210,7 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i for i := 1; !found; i++ { usernameAvailable, err := m.db.IsUsernameAvailable(ctx, username+iString) if err != nil { - return nil, err + return nil, gtserror.NewErrorInternalError(err) } if usernameAvailable { // no error so we've found a username that works @@ -223,7 +248,7 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i // create the user! this will also create an account and store it in the database so we don't need to do that here user, err = m.db.NewSignup(ctx, username, "", requireApproval, claims.Email, password, ip, "", appID, emailVerified, admin) if err != nil { - return nil, fmt.Errorf("error creating user: %s", err) + return nil, gtserror.NewErrorInternalError(err) } return user, nil diff --git a/internal/api/client/auth/signin.go b/internal/api/client/auth/signin.go index bdf00c663..b8f267f54 100644 --- a/internal/api/client/auth/signin.go +++ b/internal/api/client/auth/signin.go @@ -21,14 +21,14 @@ import ( "context" "errors" + "fmt" "net/http" - "github.com/sirupsen/logrus" - "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "golang.org/x/crypto/bcrypt" ) @@ -41,64 +41,62 @@ type login struct { // SignInGETHandler should be served at https://example.org/auth/sign_in. // The idea is to present a sign in page to the user, where they can enter their username and password. -// The form will then POST to the sign in page, which will be handled by SignInPOSTHandler +// 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) { - l := logrus.WithField("func", "SignInGETHandler") - l.Trace("entering sign in handler") - if _, err := api.NegotiateAccept(c, api.HTMLAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - if m.idp != nil { - s := sessions.Default(c) - - stateI := s.Get(sessionState) - state, ok := stateI.(string) - if !ok { - m.clearSession(s) - c.JSON(http.StatusForbidden, gin.H{"error": "state not found in session"}) - return - } - - redirect := m.idp.AuthCodeURL(state) - l.Debugf("redirecting to external idp at %s", redirect) - c.Redirect(http.StatusSeeOther, redirect) + if m.idp == nil { + // no idp provider, use our own funky little sign in page + c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{}) return } - c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{}) + + // idp provider is in use, so redirect to it + s := sessions.Default(c) + + stateI := s.Get(sessionState) + state, ok := stateI.(string) + if !ok { + m.clearSession(s) + err := fmt.Errorf("key %s was not found in session", sessionState) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + return + } + + c.Redirect(http.StatusSeeOther, m.idp.AuthCodeURL(state)) } // SignInPOSTHandler should be served at https://example.org/auth/sign_in. // The idea is to present a sign in page to the user, where they can enter their username and password. // The handler will then redirect to the auth handler served at /auth func (m *Module) SignInPOSTHandler(c *gin.Context) { - l := logrus.WithField("func", "SignInPOSTHandler") s := sessions.Default(c) + form := &login{} if err := c.ShouldBind(form); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) m.clearSession(s) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet) return } - l.Tracef("parsed form: %+v", form) - userid, err := m.ValidatePassword(c.Request.Context(), form.Email, form.Password) - if err != nil { - c.String(http.StatusForbidden, err.Error()) - m.clearSession(s) + userid, errWithCode := m.ValidatePassword(c.Request.Context(), form.Email, form.Password) + 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) return } s.Set(sessionUserID, userid) if err := s.Save(); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - m.clearSession(s) - return + err := fmt.Errorf("error saving user id onto session: %s", err) + api.ErrorHandler(c, gtserror.NewErrorInternalError(err, helpfulAdvice), m.processor.InstanceGet) } - l.Trace("redirecting to auth page") c.Redirect(http.StatusFound, OauthAuthorizePath) } @@ -106,42 +104,34 @@ func (m *Module) SignInPOSTHandler(c *gin.Context) { // The goal is to authenticate the password against the one for that email // address stored in the database. If OK, we return the userid (a ulid) for that user, // so that it can be used in further Oauth flows to generate a token/retreieve an oauth client from the db. -func (m *Module) ValidatePassword(ctx context.Context, email string, password string) (userid string, err error) { - l := logrus.WithField("func", "ValidatePassword") - - // make sure an email/password was provided and bail if not +func (m *Module) ValidatePassword(ctx context.Context, email string, password string) (string, gtserror.WithCode) { if email == "" || password == "" { - l.Debug("email or password was not provided") - return incorrectPassword() + err := errors.New("email or password was not provided") + return incorrectPassword(err) } - // first we select the user from the database based on email address, bail if no user found for that email - gtsUser := >smodel.User{} - - if err := m.db.GetWhere(ctx, []db.Where{{Key: "email", Value: email}}, gtsUser); err != nil { - l.Debugf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err) - return incorrectPassword() + user := >smodel.User{} + if err := m.db.GetWhere(ctx, []db.Where{{Key: "email", Value: email}}, user); err != nil { + err := fmt.Errorf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err) + return incorrectPassword(err) } - // make sure a password is actually set and bail if not - if gtsUser.EncryptedPassword == "" { - l.Warnf("encrypted password for user %s was empty for some reason", gtsUser.Email) - return incorrectPassword() + if user.EncryptedPassword == "" { + err := fmt.Errorf("encrypted password for user %s was empty for some reason", user.Email) + return incorrectPassword(err) } - // compare the provided password with the encrypted one from the db, bail if they don't match - if err := bcrypt.CompareHashAndPassword([]byte(gtsUser.EncryptedPassword), []byte(password)); err != nil { - l.Debugf("password hash didn't match for user %s during login attempt: %s", gtsUser.Email, err) - return incorrectPassword() + if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(password)); err != nil { + err := fmt.Errorf("password hash didn't match for user %s during login attempt: %s", user.Email, err) + return incorrectPassword(err) } - // If we've made it this far the email/password is correct, so we can just return the id of the user. - userid = gtsUser.ID - l.Tracef("returning (%s, %s)", userid, err) - return + return user.ID, nil } -// incorrectPassword is just a little helper function to use in the ValidatePassword function -func incorrectPassword() (string, error) { - return "", errors.New("password/email combination was incorrect") +// incorrectPassword wraps the given error in a gtserror.WithCode, and returns +// only a generic 'safe' error message to the user, to not give any info away. +func incorrectPassword(err error) (string, gtserror.WithCode) { + safeErr := fmt.Errorf("password/email combination was incorrect") + return "", gtserror.NewErrorUnauthorized(err, safeErr.Error(), helpfulAdvice) } diff --git a/internal/api/client/auth/token.go b/internal/api/client/auth/token.go index a0bc7ae93..34fb62940 100644 --- a/internal/api/client/auth/token.go +++ b/internal/api/client/auth/token.go @@ -19,11 +19,10 @@ package auth import ( - "net/http" "net/url" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/gin-gonic/gin" ) @@ -40,38 +39,40 @@ type tokenBody 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) { - l := logrus.WithField("func", "TokenPOSTHandler") - l.Trace("entered TokenPOSTHandler") - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } form := &tokenBody{} - if err := c.ShouldBind(form); err == nil { - c.Request.Form = url.Values{} - if form.ClientID != nil { - c.Request.Form.Set("client_id", *form.ClientID) - } - if form.ClientSecret != nil { - c.Request.Form.Set("client_secret", *form.ClientSecret) - } - if form.Code != nil { - c.Request.Form.Set("code", *form.Code) - } - if form.GrantType != nil { - c.Request.Form.Set("grant_type", *form.GrantType) - } - if form.RedirectURI != nil { - c.Request.Form.Set("redirect_uri", *form.RedirectURI) - } - if form.Scope != nil { - c.Request.Form.Set("scope", *form.Scope) - } + if err := c.ShouldBind(form); err != nil { + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet) + return } + c.Request.Form = url.Values{} + if form.ClientID != nil { + c.Request.Form.Set("client_id", *form.ClientID) + } + if form.ClientSecret != nil { + c.Request.Form.Set("client_secret", *form.ClientSecret) + } + if form.Code != nil { + c.Request.Form.Set("code", *form.Code) + } + if form.GrantType != nil { + c.Request.Form.Set("grant_type", *form.GrantType) + } + if form.RedirectURI != nil { + c.Request.Form.Set("redirect_uri", *form.RedirectURI) + } + if form.Scope != nil { + c.Request.Form.Set("scope", *form.Scope) + } + + // pass the writer and request into the oauth server handler, which will + // take care of writing the oauth token into the response etc if err := m.server.HandleTokenRequest(c.Writer, c.Request); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorInternalError(err, helpfulAdvice), m.processor.InstanceGet) } } diff --git a/internal/api/client/blocks/blocksget.go b/internal/api/client/blocks/blocksget.go index 75741946b..8d3c51d82 100644 --- a/internal/api/client/blocks/blocksget.go +++ b/internal/api/client/blocks/blocksget.go @@ -19,13 +19,13 @@ package blocks import ( + "fmt" "net/http" "strconv" - "github.com/sirupsen/logrus" - "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -80,24 +80,25 @@ // type: array // items: // "$ref": "#/definitions/account" -// '401': -// description: unauthorized // '400': // description: bad request +// '401': +// description: unauthorized // '404': // description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) BlocksGETHandler(c *gin.Context) { - l := logrus.WithField("func", "PublicTimelineGETHandler") - authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debugf("error authing: %s", err) - c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } @@ -118,8 +119,8 @@ func (m *Module) BlocksGETHandler(c *gin.Context) { if limitString != "" { i, err := strconv.ParseInt(limitString, 10, 64) if err != nil { - l.Debugf("error parsing limit string: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"}) + err := fmt.Errorf("error parsing %s: %s", LimitKey, err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } limit = int(i) @@ -127,8 +128,7 @@ func (m *Module) BlocksGETHandler(c *gin.Context) { resp, errWithCode := m.processor.BlocksGet(c.Request.Context(), authed, maxID, sinceID, limit) if errWithCode != nil { - l.Debugf("error from processor BlocksGet: %s", errWithCode) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/emoji/emojisget.go b/internal/api/client/emoji/emojisget.go index 0b8c510eb..488aad78b 100644 --- a/internal/api/client/emoji/emojisget.go +++ b/internal/api/client/emoji/emojisget.go @@ -5,18 +5,25 @@ "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/oauth" ) // EmojisGETHandler returns a list of custom emojis enabled on the instance 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 { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } emojis, errWithCode := m.processor.CustomEmojisGet(c) if errWithCode != nil { - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/favourites/favouritesget.go b/internal/api/client/favourites/favouritesget.go index 5a317b2ea..31fd47c90 100644 --- a/internal/api/client/favourites/favouritesget.go +++ b/internal/api/client/favourites/favouritesget.go @@ -1,29 +1,26 @@ package favourites import ( + "fmt" "net/http" "strconv" - "github.com/sirupsen/logrus" - "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) // FavouritesGETHandler handles GETting favourites. func (m *Module) FavouritesGETHandler(c *gin.Context) { - l := logrus.WithField("func", "PublicTimelineGETHandler") - authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debugf("error authing: %s", err) - c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } @@ -44,8 +41,8 @@ func (m *Module) FavouritesGETHandler(c *gin.Context) { if limitString != "" { i, err := strconv.ParseInt(limitString, 10, 64) if err != nil { - l.Debugf("error parsing limit string: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"}) + err := fmt.Errorf("error parsing %s: %s", LimitKey, err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } limit = int(i) @@ -53,8 +50,7 @@ func (m *Module) FavouritesGETHandler(c *gin.Context) { resp, errWithCode := m.processor.FavedTimelineGet(c.Request.Context(), authed, maxID, minID, limit) if errWithCode != nil { - l.Debugf("error from processor FavedTimelineGet: %s", errWithCode) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/fileserver/servefile.go b/internal/api/client/fileserver/servefile.go index d503c0fe6..0372de9d8 100644 --- a/internal/api/client/fileserver/servefile.go +++ b/internal/api/client/fileserver/servefile.go @@ -19,6 +19,7 @@ package fileserver import ( + "fmt" "io" "net/http" @@ -26,6 +27,7 @@ "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -34,17 +36,9 @@ // 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) { - l := logrus.WithFields(logrus.Fields{ - "func": "ServeFile", - "request_uri": c.Request.RequestURI, - "user_agent": c.Request.UserAgent(), - "origin_ip": c.ClientIP(), - }) - l.Trace("received request") - authed, err := oauth.Authed(c, false, false, false, false) if err != nil { - c.String(http.StatusNotFound, "404 page not found") + api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet) return } @@ -53,29 +47,29 @@ func (m *FileServer) ServeFile(c *gin.Context) { // "FILE_NAME" consists of two parts, the attachment's database id, a period, and the file extension. accountID := c.Param(AccountIDKey) if accountID == "" { - l.Debug("missing accountID from request") - c.String(http.StatusNotFound, "404 page not found") + err := fmt.Errorf("missing %s from request", AccountIDKey) + api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet) return } mediaType := c.Param(MediaTypeKey) if mediaType == "" { - l.Debug("missing mediaType from request") - c.String(http.StatusNotFound, "404 page not found") + err := fmt.Errorf("missing %s from request", MediaTypeKey) + api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet) return } mediaSize := c.Param(MediaSizeKey) if mediaSize == "" { - l.Debug("missing mediaSize from request") - c.String(http.StatusNotFound, "404 page not found") + err := fmt.Errorf("missing %s from request", MediaSizeKey) + api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet) return } fileName := c.Param(FileNameKey) if fileName == "" { - l.Debug("missing fileName from request") - c.String(http.StatusNotFound, "404 page not found") + err := fmt.Errorf("missing %s from request", FileNameKey) + api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet) return } @@ -86,8 +80,7 @@ func (m *FileServer) ServeFile(c *gin.Context) { FileName: fileName, }) if errWithCode != nil { - l.Errorf(errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } @@ -95,7 +88,7 @@ func (m *FileServer) ServeFile(c *gin.Context) { // if the content is a ReadCloser, close it when we're done if closer, ok := content.Content.(io.ReadCloser); ok { if err := closer.Close(); err != nil { - l.Errorf("error closing readcloser: %s", err) + logrus.Errorf("ServeFile: error closing readcloser: %s", err) } } }() @@ -103,9 +96,9 @@ 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.Offer(content.ContentType)) - if errWithCode != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + format, err := api.NegotiateAccept(c, api.MIME(content.ContentType)) + if err != nil { + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } diff --git a/internal/api/client/filter/filtersget.go b/internal/api/client/filter/filtersget.go index f0367d40c..8e0a0bb34 100644 --- a/internal/api/client/filter/filtersget.go +++ b/internal/api/client/filter/filtersget.go @@ -5,12 +5,19 @@ "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/oauth" ) // 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) + return + } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } diff --git a/internal/api/client/followrequest/authorize.go b/internal/api/client/followrequest/authorize.go index 42448846d..528c39f2b 100644 --- a/internal/api/client/followrequest/authorize.go +++ b/internal/api/client/followrequest/authorize.go @@ -19,12 +19,12 @@ package followrequest import ( + "errors" "net/http" - "github.com/sirupsen/logrus" - "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -62,43 +62,34 @@ // description: bad request // '401': // description: unauthorized -// '403': -// description: forbidden // '404': // description: not found +// '406': +// description: not acceptable // '500': // description: internal server error func (m *Module) FollowRequestAuthorizePOSTHandler(c *gin.Context) { - l := logrus.WithField("func", "FollowRequestAuthorizePOSTHandler") - authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) - return - } - - if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } originAccountID := c.Param(IDKey) if originAccountID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no follow request origin account id provided"}) + err := errors.New("no account id specified") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } relationship, errWithCode := m.processor.FollowRequestAccept(c.Request.Context(), authed, originAccountID) if errWithCode != nil { - l.Debug(errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/followrequest/authorize_test.go b/internal/api/client/followrequest/authorize_test.go index c5f4c94f2..693380d91 100644 --- a/internal/api/client/followrequest/authorize_test.go +++ b/internal/api/client/followrequest/authorize_test.go @@ -82,6 +82,34 @@ func (suite *AuthorizeTestSuite) TestAuthorize() { suite.Equal(`{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","following":false,"showing_reblogs":false,"notifying":false,"followed_by":true,"blocking":false,"blocked_by":false,"muting":false,"muting_notifications":false,"requested":false,"domain_blocking":false,"endorsed":false,"note":""}`, string(b)) } +func (suite *AuthorizeTestSuite) TestAuthorizeNoFR() { + requestingAccount := suite.testAccounts["remote_account_2"] + + recorder := httptest.NewRecorder() + ctx := suite.newContext(recorder, http.MethodPost, []byte{}, fmt.Sprintf("/api/v1/follow_requests/%s/authorize", requestingAccount.ID), "") + + ctx.Params = gin.Params{ + gin.Param{ + Key: followrequest.IDKey, + Value: requestingAccount.ID, + }, + } + + // call the handler + suite.followRequestModule.FollowRequestAuthorizePOSTHandler(ctx) + + suite.Equal(http.StatusNotFound, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + + // check the response + b, err := ioutil.ReadAll(result.Body) + assert.NoError(suite.T(), err) + + suite.Equal(`{"error":"Not Found"}`, string(b)) +} + func TestAuthorizeTestSuite(t *testing.T) { suite.Run(t, &AuthorizeTestSuite{}) } diff --git a/internal/api/client/followrequest/get.go b/internal/api/client/followrequest/get.go index a22278ae4..5dd079ddb 100644 --- a/internal/api/client/followrequest/get.go +++ b/internal/api/client/followrequest/get.go @@ -21,10 +21,9 @@ import ( "net/http" - "github.com/sirupsen/logrus" - "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -71,34 +70,27 @@ // description: bad request // '401': // description: unauthorized -// '403': -// description: forbidden // '404': // description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) FollowRequestGETHandler(c *gin.Context) { - l := logrus.WithField("func", "FollowRequestGETHandler") - authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) - return - } - - if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } accts, errWithCode := m.processor.FollowRequestsGet(c.Request.Context(), authed) if errWithCode != nil { - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/followrequest/reject.go b/internal/api/client/followrequest/reject.go index 8f4705d9f..c29ccbf5d 100644 --- a/internal/api/client/followrequest/reject.go +++ b/internal/api/client/followrequest/reject.go @@ -19,11 +19,12 @@ package followrequest import ( + "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -59,43 +60,34 @@ // description: bad request // '401': // description: unauthorized -// '403': -// description: forbidden // '404': // description: not found +// '406': +// description: not acceptable // '500': // description: internal server error func (m *Module) FollowRequestRejectPOSTHandler(c *gin.Context) { - l := logrus.WithField("func", "FollowRequestRejectPOSTHandler") - authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) - return - } - - if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } originAccountID := c.Param(IDKey) if originAccountID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no follow request origin account id provided"}) + err := errors.New("no account id specified") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } relationship, errWithCode := m.processor.FollowRequestReject(c.Request.Context(), authed, originAccountID) if errWithCode != nil { - l.Debug(errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/instance/instanceget.go b/internal/api/client/instance/instanceget.go index cd6dd32cc..35e842102 100644 --- a/internal/api/client/instance/instanceget.go +++ b/internal/api/client/instance/instanceget.go @@ -3,9 +3,9 @@ import ( "net/http" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/gin-gonic/gin" ) @@ -30,22 +30,19 @@ // description: "Instance information." // schema: // "$ref": "#/definitions/instance" +// '406': +// description: not acceptable // '500': // description: internal error func (m *Module) InstanceInformationGETHandler(c *gin.Context) { - l := logrus.WithField("func", "InstanceInformationGETHandler") - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - host := config.GetHost() - - instance, err := m.processor.InstanceGet(c.Request.Context(), host) - if err != nil { - l.Debugf("error getting instance from processor: %s", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + instance, errWithCode := m.processor.InstanceGet(c.Request.Context(), config.GetHost()) + if errWithCode != nil { + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/instance/instancepatch.go b/internal/api/client/instance/instancepatch.go index e0200cb0f..4e3f1e454 100644 --- a/internal/api/client/instance/instancepatch.go +++ b/internal/api/client/instance/instancepatch.go @@ -1,13 +1,13 @@ package instance import ( + "errors" "net/http" - "github.com/sirupsen/logrus" - "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -82,52 +82,51 @@ // description: "The newly updated instance." // schema: // "$ref": "#/definitions/instance" -// '401': -// description: unauthorized // '400': // description: bad request +// '401': +// description: unauthorized +// '403': +// description: forbidden +// '404': +// description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) InstanceUpdatePATCHHandler(c *gin.Context) { - l := logrus.WithField("func", "InstanceUpdatePATCHHandler") authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - // only admins can update instance settings if !authed.User.Admin { - l.Debug("user is not an admin so cannot update instance settings") - c.JSON(http.StatusUnauthorized, gin.H{"error": "not an 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) return } - l.Debug("parsing request form") form := &model.InstanceSettingsUpdateRequest{} - if err := c.ShouldBind(&form); err != nil || form == nil { - l.Debugf("could not parse form from request: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + if err := c.ShouldBind(&form); err != nil { + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - l.Debugf("parsed form: %+v", form) - - // if everything on the form is nil, then nothing has been set and we shouldn't continue if form.Title == nil && form.ContactUsername == nil && form.ContactEmail == nil && form.ShortDescription == nil && form.Description == nil && form.Terms == nil && form.Avatar == nil && form.Header == nil { - l.Debugf("could not parse form from request") - c.JSON(http.StatusBadRequest, gin.H{"error": "empty form submitted"}) + err := errors.New("empty form submitted") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } i, errWithCode := m.processor.InstancePatch(c.Request.Context(), form) if errWithCode != nil { - l.Debugf("error with instance patch request: %s", errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/instance/instancepatch_test.go b/internal/api/client/instance/instancepatch_test.go index 891a9da21..5ca4f2b7a 100644 --- a/internal/api/client/instance/instancepatch_test.go +++ b/internal/api/client/instance/instancepatch_test.go @@ -26,6 +26,7 @@ "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/api/client/instance" + "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/testrig" ) @@ -125,6 +126,67 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() { suite.Equal(`{"uri":"http://localhost:8080","title":"localhost:8080","description":"","short_description":"\u003cp\u003eThis is some html, which is \u003cem\u003eallowed\u003c/em\u003e in short descriptions.\u003c/p\u003e","email":"","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":0,"status_count":16,"user_count":4},"thumbnail":"","max_toot_chars":5000}`, string(b)) } +func (suite *InstancePatchTestSuite) TestInstancePatch4() { + requestBody, w, err := testrig.CreateMultipartFormData( + "", "", + map[string]string{}) + if err != nil { + panic(err) + } + bodyBytes := requestBody.Bytes() + + // set up the request + recorder := httptest.NewRecorder() + ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, instance.InstanceInformationPath, w.FormDataContentType()) + + // call the handler + suite.instanceModule.InstanceUpdatePATCHHandler(ctx) + + suite.Equal(http.StatusBadRequest, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + + b, err := io.ReadAll(result.Body) + suite.NoError(err) + + suite.Equal(`{"error":"Bad Request: empty form submitted"}`, string(b)) +} + +func (suite *InstancePatchTestSuite) TestInstancePatch5() { + requestBody, w, err := testrig.CreateMultipartFormData( + "", "", + map[string]string{ + "short_description": "

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

", + }) + if err != nil { + panic(err) + } + bodyBytes := requestBody.Bytes() + + // set up the request + recorder := httptest.NewRecorder() + ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, instance.InstanceInformationPath, w.FormDataContentType()) + + ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) + ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"])) + ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) + ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) + + // call the handler + suite.instanceModule.InstanceUpdatePATCHHandler(ctx) + + suite.Equal(http.StatusForbidden, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + + b, err := io.ReadAll(result.Body) + suite.NoError(err) + + suite.Equal(`{"error":"Forbidden: user is not an admin so cannot update instance settings"}`, string(b)) +} + func TestInstancePatchTestSuite(t *testing.T) { suite.Run(t, &InstancePatchTestSuite{}) } diff --git a/internal/api/client/list/listsgets.go b/internal/api/client/list/listsgets.go index 184830290..246a1216a 100644 --- a/internal/api/client/list/listsgets.go +++ b/internal/api/client/list/listsgets.go @@ -5,12 +5,19 @@ "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 { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } diff --git a/internal/api/client/media/mediacreate.go b/internal/api/client/media/mediacreate.go index ed6a12d37..f51d272b6 100644 --- a/internal/api/client/media/mediacreate.go +++ b/internal/api/client/media/mediacreate.go @@ -23,12 +23,11 @@ "fmt" "net/http" - "github.com/sirupsen/logrus" - "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -80,46 +79,36 @@ // description: bad request // '401': // description: unauthorized -// '403': -// description: forbidden // '422': // description: unprocessable +// '500': +// description: internal server error func (m *Module) MediaCreatePOSTHandler(c *gin.Context) { - l := logrus.WithField("func", "statusCreatePOSTHandler") - authed, err := oauth.Authed(c, true, true, true, true) // posting new media is serious business so we want *everything* + authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - // extract the media create form from the request context - l.Tracef("parsing request form: %s", c.Request.Form) form := &model.AttachmentRequest{} if err := c.ShouldBind(&form); err != nil { - l.Debugf("error parsing form: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("could not parse form: %s", err)}) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - // Give the fields on the request form a first pass to make sure the request is superficially valid. - l.Tracef("validating form %+v", form) if err := validateCreateMedia(form); err != nil { - l.Debugf("error validating form: %s", err) - c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - l.Debug("calling processor media create func") - apiAttachment, err := m.processor.MediaCreate(c.Request.Context(), authed, form) + apiAttachment, errWithCode := m.processor.MediaCreate(c.Request.Context(), authed, form) if err != nil { - l.Debugf("error creating attachment: %s", err) - c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } @@ -143,6 +132,7 @@ func validateCreateMedia(form *model.AttachmentRequest) error { if maxImageSize > maxSize { maxSize = maxImageSize } + if form.File.Size > int64(maxSize) { return fmt.Errorf("file size limit exceeded: limit is %d bytes but attachment was %d bytes", maxSize, form.File.Size) } diff --git a/internal/api/client/media/mediacreate_test.go b/internal/api/client/media/mediacreate_test.go index f39e756f7..47dd9ac3f 100644 --- a/internal/api/client/media/mediacreate_test.go +++ b/internal/api/client/media/mediacreate_test.go @@ -247,15 +247,14 @@ func (suite *MediaCreateTestSuite) TestMediaCreateLongDescription() { suite.mediaModule.MediaCreatePOSTHandler(ctx) // check response - suite.EqualValues(http.StatusUnprocessableEntity, recorder.Code) + suite.EqualValues(http.StatusBadRequest, recorder.Code) result := recorder.Result() defer result.Body.Close() b, err := ioutil.ReadAll(result.Body) suite.NoError(err) - expectedErr := fmt.Sprintf(`{"error":"image description length must be between 0 and 500 characters (inclusive), but provided image description was %d chars"}`, len(description)) - suite.Equal(expectedErr, string(b)) + suite.Equal(`{"error":"Bad Request: image description length must be between 0 and 500 characters (inclusive), but provided image description was 6667 chars"}`, string(b)) } func (suite *MediaCreateTestSuite) TestMediaCreateTooShortDescription() { diff --git a/internal/api/client/media/mediaget.go b/internal/api/client/media/mediaget.go index 46057d219..c2a0ab7a0 100644 --- a/internal/api/client/media/mediaget.go +++ b/internal/api/client/media/mediaget.go @@ -19,12 +19,12 @@ package media import ( + "errors" "net/http" - "github.com/sirupsen/logrus" - "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -59,33 +59,34 @@ // description: bad request // '401': // description: unauthorized -// '403': -// description: forbidden -// '422': -// description: unprocessable +// '404': +// description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) MediaGETHandler(c *gin.Context) { - l := logrus.WithField("func", "MediaGETHandler") authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } attachmentID := c.Param(IDKey) if attachmentID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no attachment ID given in request"}) + err := errors.New("no attachment id specified") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } attachment, errWithCode := m.processor.MediaGet(c.Request.Context(), authed, attachmentID) if errWithCode != nil { - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/media/mediaupdate.go b/internal/api/client/media/mediaupdate.go index 21b8c9c8e..32aef9cff 100644 --- a/internal/api/client/media/mediaupdate.go +++ b/internal/api/client/media/mediaupdate.go @@ -23,12 +23,11 @@ "fmt" "net/http" - "github.com/sirupsen/logrus" - "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -89,50 +88,45 @@ // description: bad request // '401': // description: unauthorized -// '403': -// description: forbidden -// '422': -// description: unprocessable +// '404': +// description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) MediaPUTHandler(c *gin.Context) { - l := logrus.WithField("func", "MediaGETHandler") authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } attachmentID := c.Param(IDKey) if attachmentID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no attachment ID given in request"}) + err := errors.New("no attachment id specified") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - // extract the media update form from the request context - l.Tracef("parsing request form: %s", c.Request.Form) - var form model.AttachmentUpdateRequest - if err := c.ShouldBind(&form); err != nil { - l.Debugf("could not parse form from request: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"}) + form := &model.AttachmentUpdateRequest{} + if err := c.ShouldBind(form); err != nil { + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - // Give the fields on the request form a first pass to make sure the request is superficially valid. - l.Tracef("validating form %+v", form) - if err := validateUpdateMedia(&form); err != nil { - l.Debugf("error validating form: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + if err := validateUpdateMedia(form); err != nil { + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - attachment, errWithCode := m.processor.MediaUpdate(c.Request.Context(), authed, attachmentID, &form) + attachment, errWithCode := m.processor.MediaUpdate(c.Request.Context(), authed, attachmentID, form) if errWithCode != nil { - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/media/mediaupdate_test.go b/internal/api/client/media/mediaupdate_test.go index fbf794e1d..d25eddd4d 100644 --- a/internal/api/client/media/mediaupdate_test.go +++ b/internal/api/client/media/mediaupdate_test.go @@ -232,7 +232,7 @@ func (suite *MediaUpdateTestSuite) TestUpdateImageShortDescription() { suite.NoError(err) // reply should be an error message - suite.Equal(`{"error":"image description length must be between 50 and 500 characters (inclusive), but provided image description was 16 chars"}`, string(b)) + suite.Equal(`{"error":"Bad Request: image description length must be between 50 and 500 characters (inclusive), but provided image description was 16 chars"}`, string(b)) } func TestMediaUpdateTestSuite(t *testing.T) { diff --git a/internal/api/client/notification/notificationsget.go b/internal/api/client/notification/notificationsget.go index b6f7cdd01..88220107e 100644 --- a/internal/api/client/notification/notificationsget.go +++ b/internal/api/client/notification/notificationsget.go @@ -19,34 +19,26 @@ package notification import ( + "fmt" "net/http" "strconv" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) // NotificationsGETHandler serves a list of notifications to the caller, with the desired query parameters func (m *Module) NotificationsGETHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "NotificationsGETHandler", - "request_uri": c.Request.RequestURI, - "user_agent": c.Request.UserAgent(), - "origin_ip": c.ClientIP(), - }) - l.Debugf("entering function") - - authed, err := oauth.Authed(c, true, true, true, true) // we don't really need an app here but we want everything else + authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Errorf("error authing status faved by request: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } @@ -55,8 +47,8 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) { if limitString != "" { i, err := strconv.ParseInt(limitString, 10, 64) if err != nil { - l.Debugf("error parsing limit string: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"}) + err := fmt.Errorf("error parsing %s: %s", LimitKey, err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } limit = int(i) @@ -76,8 +68,7 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) { resp, errWithCode := m.processor.NotificationsGet(c.Request.Context(), authed, limit, maxID, sinceID) if errWithCode != nil { - l.Debugf("error processing notifications get: %s", errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/search/searchget.go b/internal/api/client/search/searchget.go index f9cbba38a..8db49ebfd 100644 --- a/internal/api/client/search/searchget.go +++ b/internal/api/client/search/searchget.go @@ -19,14 +19,15 @@ package search import ( + "errors" "fmt" "net/http" "strconv" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -52,50 +53,44 @@ // type: array // items: // "$ref": "#/definitions/searchResult" -// '401': -// description: unauthorized // '400': // description: bad request +// '401': +// description: unauthorized +// '404': +// description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) SearchGETHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "SearchGETHandler", - "request_uri": c.Request.RequestURI, - "user_agent": c.Request.UserAgent(), - "origin_ip": c.ClientIP(), - }) - l.Debugf("entering function") - - authed, err := oauth.Authed(c, true, true, true, true) // we don't really need an app here but we want everything else + authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Errorf("error authing search request: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - accountID := c.Query(AccountIDKey) - maxID := c.Query(MaxIDKey) - minID := c.Query(MinIDKey) - searchType := c.Query(TypeKey) - excludeUnreviewed := false excludeUnreviewedString := c.Query(ExcludeUnreviewedKey) if excludeUnreviewedString != "" { var err error excludeUnreviewed, err = strconv.ParseBool(excludeUnreviewedString) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("couldn't parse param %s: %s", excludeUnreviewedString, err)}) + err := fmt.Errorf("error parsing %s: %s", ExcludeUnreviewedKey, err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } } query := c.Query(QueryKey) if query == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter q was empty"}) + err := errors.New("query parameter q was empty") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } @@ -105,18 +100,19 @@ func (m *Module) SearchGETHandler(c *gin.Context) { var err error resolve, err = strconv.ParseBool(resolveString) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("couldn't parse param %s: %s", resolveString, err)}) + err := fmt.Errorf("error parsing %s: %s", ResolveKey, err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } } - limit := 20 + limit := 2 limitString := c.Query(LimitKey) if limitString != "" { i, err := strconv.ParseInt(limitString, 10, 64) if err != nil { - l.Debugf("error parsing limit string: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"}) + err := fmt.Errorf("error parsing %s: %s", LimitKey, err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } limit = int(i) @@ -133,18 +129,12 @@ func (m *Module) SearchGETHandler(c *gin.Context) { if offsetString != "" { i, err := strconv.ParseInt(offsetString, 10, 64) if err != nil { - l.Debugf("error parsing offset string: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse offset query param"}) + err := fmt.Errorf("error parsing %s: %s", OffsetKey, err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } offset = int(i) } - if limit > 40 { - limit = 40 - } - if limit < 1 { - limit = 1 - } following := false followingString := c.Query(FollowingKey) @@ -152,16 +142,17 @@ func (m *Module) SearchGETHandler(c *gin.Context) { var err error following, err = strconv.ParseBool(followingString) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("couldn't parse param %s: %s", followingString, err)}) + err := fmt.Errorf("error parsing %s: %s", FollowingKey, err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } } searchQuery := &model.SearchQuery{ - AccountID: accountID, - MaxID: maxID, - MinID: minID, - Type: searchType, + AccountID: c.Query(AccountIDKey), + MaxID: c.Query(MaxIDKey), + MinID: c.Query(MinIDKey), + Type: c.Query(TypeKey), ExcludeUnreviewed: excludeUnreviewed, Query: query, Resolve: resolve, @@ -172,8 +163,7 @@ func (m *Module) SearchGETHandler(c *gin.Context) { results, errWithCode := m.processor.SearchGet(c.Request.Context(), authed, searchQuery) if errWithCode != nil { - l.Debugf("error searching: %s", errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/status/statusboost.go b/internal/api/client/status/statusboost.go index 25b2cf0e7..0642ad92e 100644 --- a/internal/api/client/status/statusboost.go +++ b/internal/api/client/status/statusboost.go @@ -19,11 +19,12 @@ package status import ( + "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -66,37 +67,32 @@ // description: forbidden // '404': // description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) StatusBoostPOSTHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "StatusBoostPOSTHandler", - "request_uri": c.Request.RequestURI, - "user_agent": c.Request.UserAgent(), - "origin_ip": c.ClientIP(), - }) - l.Debugf("entering function") - authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debug("not authed so can't boost status") - c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetStatusID := c.Param(IDKey) if targetStatusID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) + err := errors.New("no status id specified") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } apiStatus, errWithCode := m.processor.StatusBoost(c.Request.Context(), authed, targetStatusID) if errWithCode != nil { - l.Debugf("error processing status boost: %s", errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/status/statusboost_test.go b/internal/api/client/status/statusboost_test.go index 42693d9e0..e33b582e8 100644 --- a/internal/api/client/status/statusboost_test.go +++ b/internal/api/client/status/statusboost_test.go @@ -134,13 +134,13 @@ func (suite *StatusBoostTestSuite) TestPostUnboostable() { suite.statusModule.StatusBoostPOSTHandler(ctx) // check response - suite.EqualValues(http.StatusForbidden, recorder.Code) // we 403 unboostable statuses + suite.Equal(http.StatusForbidden, recorder.Code) // we 403 unboostable statuses result := recorder.Result() defer result.Body.Close() b, err := ioutil.ReadAll(result.Body) assert.NoError(suite.T(), err) - assert.Equal(suite.T(), `{"error":"forbidden"}`, string(b)) + assert.Equal(suite.T(), `{"error":"Forbidden"}`, string(b)) } // try to boost a status that's not visible to the user @@ -177,13 +177,7 @@ func (suite *StatusBoostTestSuite) TestPostNotVisible() { suite.statusModule.StatusBoostPOSTHandler(ctx) // check response - suite.EqualValues(http.StatusNotFound, recorder.Code) // we 404 statuses that aren't visible - - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - assert.NoError(suite.T(), err) - assert.Equal(suite.T(), `{"error":"404 not found"}`, string(b)) + suite.Equal(http.StatusNotFound, recorder.Code) // we 404 statuses that aren't visible } func TestStatusBoostTestSuite(t *testing.T) { diff --git a/internal/api/client/status/statusboostedby.go b/internal/api/client/status/statusboostedby.go index bd2578a12..382b38f7b 100644 --- a/internal/api/client/status/statusboostedby.go +++ b/internal/api/client/status/statusboostedby.go @@ -23,6 +23,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -84,10 +85,9 @@ func (m *Module) StatusBoostedByGETHandler(c *gin.Context) { return } - apiAccounts, err := m.processor.StatusBoostedBy(c.Request.Context(), authed, targetStatusID) - if err != nil { - l.Debugf("error processing status boosted by request: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) + apiAccounts, errWithCode := m.processor.StatusBoostedBy(c.Request.Context(), authed, targetStatusID) + if errWithCode != nil { + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/status/statuscontext.go b/internal/api/client/status/statuscontext.go index 32a0a89ba..0daefaec1 100644 --- a/internal/api/client/status/statuscontext.go +++ b/internal/api/client/status/statuscontext.go @@ -19,11 +19,12 @@ package status import ( + "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -65,37 +66,32 @@ // description: forbidden // '404': // description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) StatusContextGETHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "StatusContextGETHandler", - "request_uri": c.Request.RequestURI, - "user_agent": c.Request.UserAgent(), - "origin_ip": c.ClientIP(), - }) - l.Debugf("entering function") - authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Errorf("error authing status context request: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetStatusID := c.Param(IDKey) if targetStatusID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) + err := errors.New("no status id specified") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } statusContext, errWithCode := m.processor.StatusGetContext(c.Request.Context(), authed, targetStatusID) if errWithCode != nil { - l.Debugf("error getting status context: %s", errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/status/statuscreate.go b/internal/api/client/status/statuscreate.go index 2813efb90..dce6f1296 100644 --- a/internal/api/client/status/statuscreate.go +++ b/internal/api/client/status/statuscreate.go @@ -23,12 +23,11 @@ "fmt" "net/http" - "github.com/sirupsen/logrus" - "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/validate" ) @@ -61,58 +60,44 @@ // description: "The newly created status." // schema: // "$ref": "#/definitions/status" -// '401': -// description: unauthorized // '400': // description: bad request +// '401': +// description: unauthorized +// '403': +// description: forbidden // '404': // description: not found +// '406': +// description: not acceptable // '500': -// description: internal error +// description: internal server error func (m *Module) StatusCreatePOSTHandler(c *gin.Context) { - l := logrus.WithField("func", "statusCreatePOSTHandler") authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - // First check this user/account is permitted to post new statuses. - // There's no point continuing otherwise. - if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"}) - return - } - - // extract the status create form from the request context - l.Debugf("parsing request form: %s", c.Request.Form) form := &model.AdvancedStatusCreateForm{} - if err := c.ShouldBind(form); err != nil || form == nil { - l.Debugf("could not parse form from request: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"}) + if err := c.ShouldBind(form); err != nil { + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - l.Debugf("handling status request form: %+v", form) - // Give the fields on the request form a first pass to make sure the request is superficially valid. - l.Tracef("validating form %+v", form) if err := validateCreateStatus(form); err != nil { - l.Debugf("error validating form: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - apiStatus, err := m.processor.StatusCreate(c.Request.Context(), authed, form) - if err != nil { - l.Debugf("error processing status create: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) + apiStatus, errWithCode := m.processor.StatusCreate(c.Request.Context(), authed, form) + if errWithCode != nil { + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } @@ -120,7 +105,6 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) { } func validateCreateStatus(form *model.AdvancedStatusCreateForm) error { - // validate that, structurally, we have a valid status/post if form.Status == "" && form.MediaIDs == nil && form.Poll == nil { return errors.New("no status, media, or poll provided") } @@ -135,19 +119,16 @@ func validateCreateStatus(form *model.AdvancedStatusCreateForm) error { maxPollChars := config.GetStatusesPollOptionMaxChars() maxCwChars := config.GetStatusesCWMaxChars() - // validate status if form.Status != "" { if len(form.Status) > maxChars { return fmt.Errorf("status too long, %d characters provided but limit is %d", len(form.Status), maxChars) } } - // validate media attachments if len(form.MediaIDs) > maxMediaFiles { return fmt.Errorf("too many media files attached to status, %d attached but limit is %d", len(form.MediaIDs), maxMediaFiles) } - // validate poll if form.Poll != nil { if form.Poll.Options == nil { return errors.New("poll with no options") @@ -162,14 +143,12 @@ func validateCreateStatus(form *model.AdvancedStatusCreateForm) error { } } - // validate spoiler text/cw if form.SpoilerText != "" { if len(form.SpoilerText) > maxCwChars { return fmt.Errorf("content-warning/spoilertext too long, %d characters provided but limit is %d", len(form.SpoilerText), maxCwChars) } } - // validate post language if form.Language != "" { if err := validate.Language(form.Language); err != nil { return err diff --git a/internal/api/client/status/statuscreate_test.go b/internal/api/client/status/statuscreate_test.go index a1df25547..61d0a5ee4 100644 --- a/internal/api/client/status/statuscreate_test.go +++ b/internal/api/client/status/statuscreate_test.go @@ -256,7 +256,7 @@ func (suite *StatusCreateTestSuite) TestReplyToNonexistentStatus() { defer result.Body.Close() b, err := ioutil.ReadAll(result.Body) suite.NoError(err) - suite.Equal(`{"error":"bad request"}`, string(b)) + suite.Equal(`{"error":"Bad Request: status with id 3759e7ef-8ee1-4c0c-86f6-8b70b9ad3d50 not replyable because it doesn't exist"}`, string(b)) } // Post a reply to the status of a local user that allows replies. diff --git a/internal/api/client/status/statusdelete.go b/internal/api/client/status/statusdelete.go index 2b6a235f7..f9adccb57 100644 --- a/internal/api/client/status/statusdelete.go +++ b/internal/api/client/status/statusdelete.go @@ -19,11 +19,12 @@ package status import ( + "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -65,43 +66,32 @@ // description: forbidden // '404': // description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) StatusDELETEHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "StatusDELETEHandler", - "request_uri": c.Request.RequestURI, - "user_agent": c.Request.UserAgent(), - "origin_ip": c.ClientIP(), - }) - l.Debugf("entering function") - authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debug("not authed so can't delete status") - c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetStatusID := c.Param(IDKey) if targetStatusID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) + err := errors.New("no status id specified") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - apiStatus, err := m.processor.StatusDelete(c.Request.Context(), authed, targetStatusID) - if err != nil { - l.Debugf("error processing status delete: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) - return - } - - // the status was already gone/never existed - if apiStatus == nil { - c.JSON(http.StatusNotFound, gin.H{"error": "Record not found"}) + apiStatus, errWithCode := m.processor.StatusDelete(c.Request.Context(), authed, targetStatusID) + if errWithCode != nil { + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/status/statusfave.go b/internal/api/client/status/statusfave.go index 81f70da83..29f74316a 100644 --- a/internal/api/client/status/statusfave.go +++ b/internal/api/client/status/statusfave.go @@ -19,11 +19,12 @@ package status import ( + "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -62,37 +63,32 @@ // description: forbidden // '404': // description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) StatusFavePOSTHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "StatusFavePOSTHandler", - "request_uri": c.Request.RequestURI, - "user_agent": c.Request.UserAgent(), - "origin_ip": c.ClientIP(), - }) - l.Debugf("entering function") - authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debug("not authed so can't fave status") - c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetStatusID := c.Param(IDKey) if targetStatusID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) + err := errors.New("no status id specified") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - apiStatus, err := m.processor.StatusFave(c.Request.Context(), authed, targetStatusID) - if err != nil { - l.Debugf("error processing status fave: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) + apiStatus, errWithCode := m.processor.StatusFave(c.Request.Context(), authed, targetStatusID) + if errWithCode != nil { + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/status/statusfave_test.go b/internal/api/client/status/statusfave_test.go index 144296ebd..5b10ba9b3 100644 --- a/internal/api/client/status/statusfave_test.go +++ b/internal/api/client/status/statusfave_test.go @@ -118,13 +118,13 @@ func (suite *StatusFaveTestSuite) TestPostUnfaveable() { suite.statusModule.StatusFavePOSTHandler(ctx) // check response - suite.EqualValues(http.StatusBadRequest, recorder.Code) + suite.EqualValues(http.StatusForbidden, recorder.Code) result := recorder.Result() defer result.Body.Close() b, err := ioutil.ReadAll(result.Body) assert.NoError(suite.T(), err) - assert.Equal(suite.T(), `{"error":"bad request"}`, string(b)) + assert.Equal(suite.T(), `{"error":"Forbidden"}`, string(b)) } func TestStatusFaveTestSuite(t *testing.T) { diff --git a/internal/api/client/status/statusfavedby.go b/internal/api/client/status/statusfavedby.go index 3e3a18136..579df6432 100644 --- a/internal/api/client/status/statusfavedby.go +++ b/internal/api/client/status/statusfavedby.go @@ -19,11 +19,12 @@ package status import ( + "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -63,37 +64,32 @@ // description: forbidden // '404': // description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) StatusFavedByGETHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "statusGETHandler", - "request_uri": c.Request.RequestURI, - "user_agent": c.Request.UserAgent(), - "origin_ip": c.ClientIP(), - }) - l.Debugf("entering function") - - authed, err := oauth.Authed(c, true, true, true, true) // we don't really need an app here but we want everything else + authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Errorf("error authing status faved by request: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetStatusID := c.Param(IDKey) if targetStatusID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) + err := errors.New("no status id specified") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - apiAccounts, err := m.processor.StatusFavedBy(c.Request.Context(), authed, targetStatusID) - if err != nil { - l.Debugf("error processing status faved by request: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) + apiAccounts, errWithCode := m.processor.StatusFavedBy(c.Request.Context(), authed, targetStatusID) + if errWithCode != nil { + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/status/statusget.go b/internal/api/client/status/statusget.go index 0730bcdda..9a8b406dc 100644 --- a/internal/api/client/status/statusget.go +++ b/internal/api/client/status/statusget.go @@ -19,11 +19,12 @@ package status import ( + "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -54,45 +55,40 @@ // description: "The requested created status." // schema: // "$ref": "#/definitions/status" -// '401': -// description: unauthorized // '400': // description: bad request +// '401': +// description: unauthorized +// '403': +// description: forbidden // '404': // description: not found +// '406': +// description: not acceptable // '500': -// description: internal error +// description: internal server error func (m *Module) StatusGETHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "statusGETHandler", - "request_uri": c.Request.RequestURI, - "user_agent": c.Request.UserAgent(), - "origin_ip": c.ClientIP(), - }) - l.Debugf("entering function") - - authed, err := oauth.Authed(c, false, false, false, false) + authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Errorf("error authing status faved by request: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetStatusID := c.Param(IDKey) if targetStatusID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) + err := errors.New("no status id specified") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - apiStatus, err := m.processor.StatusGet(c.Request.Context(), authed, targetStatusID) - if err != nil { - l.Debugf("error processing status get: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) + apiStatus, errWithCode := m.processor.StatusGet(c.Request.Context(), authed, targetStatusID) + if errWithCode != nil { + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/status/statusunboost.go b/internal/api/client/status/statusunboost.go index 7d6052500..b3e921bb7 100644 --- a/internal/api/client/status/statusunboost.go +++ b/internal/api/client/status/statusunboost.go @@ -19,11 +19,12 @@ package status import ( + "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -63,37 +64,32 @@ // description: forbidden // '404': // description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) StatusUnboostPOSTHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "StatusUnboostPOSTHandler", - "request_uri": c.Request.RequestURI, - "user_agent": c.Request.UserAgent(), - "origin_ip": c.ClientIP(), - }) - l.Debugf("entering function") - authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debug("not authed so can't unboost status") - c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetStatusID := c.Param(IDKey) if targetStatusID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) + err := errors.New("no status id specified") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } apiStatus, errWithCode := m.processor.StatusUnboost(c.Request.Context(), authed, targetStatusID) if errWithCode != nil { - l.Debugf("error processing status unboost: %s", errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/status/statusunfave.go b/internal/api/client/status/statusunfave.go index 5c5c7e701..19f217160 100644 --- a/internal/api/client/status/statusunfave.go +++ b/internal/api/client/status/statusunfave.go @@ -19,11 +19,12 @@ package status import ( + "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -62,37 +63,32 @@ // description: forbidden // '404': // description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error func (m *Module) StatusUnfavePOSTHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "StatusUnfavePOSTHandler", - "request_uri": c.Request.RequestURI, - "user_agent": c.Request.UserAgent(), - "origin_ip": c.ClientIP(), - }) - l.Debugf("entering function") - authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debug("not authed so can't unfave status") - c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } targetStatusID := c.Param(IDKey) if targetStatusID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) + err := errors.New("no status id specified") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - apiStatus, err := m.processor.StatusUnfave(c.Request.Context(), authed, targetStatusID) - if err != nil { - l.Debugf("error processing status unfave: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) + apiStatus, errWithCode := m.processor.StatusUnfave(c.Request.Context(), authed, targetStatusID) + if errWithCode != nil { + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/streaming/stream.go b/internal/api/client/streaming/stream.go index c01bf2216..2c18e4e41 100644 --- a/internal/api/client/streaming/stream.go +++ b/internal/api/client/streaming/stream.go @@ -2,14 +2,24 @@ import ( "fmt" - "github.com/sirupsen/logrus" "net/http" "time" + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/gin-gonic/gin" "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 }, +} + // StreamGETHandler swagger:operation GET /api/v1/streaming streamGet // // Initiate a websocket connection for live streaming of statuses and notifications. @@ -108,79 +118,78 @@ // '400': // description: bad request func (m *Module) StreamGETHandler(c *gin.Context) { - l := logrus.WithField("func", "StreamGETHandler") - streamType := c.Query(StreamQueryKey) if streamType == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("no stream type provided under query key %s", StreamQueryKey)}) + err := fmt.Errorf("no stream type provided under query key %s", StreamQueryKey) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } accessToken := c.Query(AccessTokenQueryKey) if accessToken == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": fmt.Sprintf("no access token provided under query key %s", AccessTokenQueryKey)}) + err := fmt.Errorf("no access token provided under query key %s", AccessTokenQueryKey) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - // make sure a valid token has been provided and obtain the associated account - account, err := m.processor.AuthorizeStreamingRequest(c.Request.Context(), accessToken) - if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "could not authorize with given token"}) - return - } - - // prepare to upgrade the connection to a websocket connection - upgrader := websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, - CheckOrigin: func(r *http.Request) bool { - // we fully expect cors requests (via something like pinafore.social) so we should be lenient here - return true - }, - } - - // do the actual upgrade here - conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) - if err != nil { - l.Infof("error upgrading websocket connection: %s", err) - return - } - defer conn.Close() // whatever happens, when we leave this function we want to close the websocket connection - - // inform the processor that we have a new connection and want a s for it - s, errWithCode := m.processor.OpenStreamForAccount(c.Request.Context(), account, streamType) + account, errWithCode := m.processor.AuthorizeStreamingRequest(c.Request.Context(), accessToken) if errWithCode != nil { - c.JSON(errWithCode.Code(), errWithCode.Safe()) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } - defer close(s.Hangup) // closing stream.Hangup indicates that we've finished with the connection (the client has gone), so we want to do this on exiting this handler - // spawn a new ticker for pinging the connection periodically - t := time.NewTicker(30 * time.Second) + stream, errWithCode := m.processor.OpenStreamForAccount(c.Request.Context(), account, streamType) + if errWithCode != nil { + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + return + } - // we want to stay in the sendloop as long as possible while the client is connected -- the only thing that should break the loop is if the client leaves or something else goes wrong -sendLoop: + l := logrus.WithFields(logrus.Fields{ + "account": account.Username, + "path": BasePath, + "streamID": stream.ID, + "streamType": streamType, + }) + + wsConn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + // If the upgrade fails, then Upgrade replies to the client with an HTTP error response. + // Because websocket issues are a pretty common source of headaches, we should also log + // this at Error to make this plenty visible and help admins out a bit. + l.Errorf("error upgrading websocket connection: %s", err) + close(stream.Hangup) + return + } + + defer func() { + // cleanup + wsConn.Close() + close(stream.Hangup) + }() + + streamTicker := time.NewTicker(30 * time.Second) + + // 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. + // + // If the loop does break, we expect the client to reattempt connection, so it's cheap to leave + try again +wsLoop: for { select { - case m := <-s.Messages: - // we've got a streaming message!! + case m := <-stream.Messages: l.Trace("received message from stream") - if err := conn.WriteJSON(m); err != nil { - l.Debugf("error writing json to websocket connection: %s", err) - // if something is wrong we want to bail and drop the connection -- the client will create a new one - break sendLoop + if err := wsConn.WriteJSON(m); err != nil { + l.Debugf("error writing json to websocket connection; breaking off: %s", err) + break wsLoop } l.Trace("wrote message into websocket connection") - case <-t.C: + case <-streamTicker.C: l.Trace("received TICK from ticker") - if err := conn.WriteMessage(websocket.PingMessage, []byte(": ping")); err != nil { - l.Debugf("error writing ping to websocket connection: %s", err) - // if something is wrong we want to bail and drop the connection -- the client will create a new one - break sendLoop + if err := wsConn.WriteMessage(websocket.PingMessage, []byte(": ping")); err != nil { + l.Debugf("error writing ping to websocket connection; breaking off: %s", err) + break wsLoop } l.Trace("wrote ping message into websocket connection") } } - - l.Trace("leaving StreamGETHandler") } diff --git a/internal/api/client/timeline/home.go b/internal/api/client/timeline/home.go index 47bc2943e..db712e4d2 100644 --- a/internal/api/client/timeline/home.go +++ b/internal/api/client/timeline/home.go @@ -19,13 +19,13 @@ package timeline import ( + "fmt" "net/http" "strconv" - "github.com/sirupsen/logrus" - "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -105,17 +105,14 @@ // '400': // description: bad request func (m *Module) HomeTimelineGETHandler(c *gin.Context) { - l := logrus.WithField("func", "HomeTimelineGETHandler") - authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debugf("error authing: %s", err) - c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } @@ -142,8 +139,8 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) { if limitString != "" { i, err := strconv.ParseInt(limitString, 10, 64) if err != nil { - l.Debugf("error parsing limit string: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"}) + err := fmt.Errorf("error parsing %s: %s", LimitKey, err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } limit = int(i) @@ -154,8 +151,8 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) { if localString != "" { i, err := strconv.ParseBool(localString) if err != nil { - l.Debugf("error parsing local string: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse local query param"}) + err := fmt.Errorf("error parsing %s: %s", LocalKey, err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } local = i @@ -163,8 +160,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 { - l.Debugf("error from processor HomeTimelineGet: %s", errWithCode) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/timeline/public.go b/internal/api/client/timeline/public.go index 06da6e947..0cea856c9 100644 --- a/internal/api/client/timeline/public.go +++ b/internal/api/client/timeline/public.go @@ -19,13 +19,13 @@ package timeline import ( + "fmt" "net/http" "strconv" - "github.com/sirupsen/logrus" - "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -105,17 +105,14 @@ // '400': // description: bad request func (m *Module) PublicTimelineGETHandler(c *gin.Context) { - l := logrus.WithField("func", "PublicTimelineGETHandler") - authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debugf("error authing: %s", err) - c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } @@ -142,8 +139,8 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) { if limitString != "" { i, err := strconv.ParseInt(limitString, 10, 64) if err != nil { - l.Debugf("error parsing limit string: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"}) + err := fmt.Errorf("error parsing %s: %s", LimitKey, err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } limit = int(i) @@ -154,8 +151,8 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) { if localString != "" { i, err := strconv.ParseBool(localString) if err != nil { - l.Debugf("error parsing local string: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse local query param"}) + err := fmt.Errorf("error parsing %s: %s", LocalKey, err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } local = i @@ -163,8 +160,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 { - l.Debugf("error from processor PublicTimelineGet: %s", errWithCode) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/user/passwordchange.go b/internal/api/client/user/passwordchange.go index 15b082c1d..7676f5b85 100644 --- a/internal/api/client/user/passwordchange.go +++ b/internal/api/client/user/passwordchange.go @@ -19,12 +19,13 @@ package user import ( + "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -54,48 +55,48 @@ // responses: // '200': // description: Change successful +// '400': +// description: bad request // '401': // description: unauthorized // '403': // description: forbidden -// '400': -// description: bad request +// '406': +// description: not acceptable // '500': -// description: "internal error" +// description: internal error func (m *Module) PasswordChangePOSTHandler(c *gin.Context) { - l := logrus.WithField("func", "PasswordChangePOSTHandler") - authed, err := oauth.Authed(c, true, true, true, true) if err != nil { - l.Debugf("error authing: %s", err) - c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) - return - } - - // First check this user/account is active. - if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } form := &model.PasswordChangeRequest{} - if err := c.ShouldBind(form); err != nil || form == nil || form.NewPassword == "" || form.OldPassword == "" { - if err != nil { - l.Debugf("could not parse form from request: %s", err) - } - c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"}) + if err := c.ShouldBind(form); err != nil { + api.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) + 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) return } if errWithCode := m.processor.UserChangePassword(c.Request.Context(), authed, form); errWithCode != nil { - l.Debugf("error changing user password: %s", errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/client/user/passwordchange_test.go b/internal/api/client/user/passwordchange_test.go index 23a234116..11df161e2 100644 --- a/internal/api/client/user/passwordchange_test.go +++ b/internal/api/client/user/passwordchange_test.go @@ -49,7 +49,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordChangePOST() { 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", user.PasswordChangePath), nil) + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil) ctx.Request.Header.Set("accept", "application/json") ctx.Request.Form = url.Values{ "old_password": {"password"}, @@ -83,7 +83,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordMissingOldPassword() { 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", user.PasswordChangePath), nil) + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil) ctx.Request.Header.Set("accept", "application/json") ctx.Request.Form = url.Values{ "new_password": {"peepeepoopoopassword"}, @@ -97,7 +97,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordMissingOldPassword() { defer result.Body.Close() b, err := ioutil.ReadAll(result.Body) suite.NoError(err) - suite.Equal(`{"error":"missing one or more required form values"}`, string(b)) + suite.Equal(`{"error":"Bad Request: password change request missing field old_password"}`, string(b)) } func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() { @@ -110,7 +110,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() { 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", user.PasswordChangePath), nil) + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil) ctx.Request.Header.Set("accept", "application/json") ctx.Request.Form = url.Values{ "old_password": {"notright"}, @@ -125,7 +125,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() { defer result.Body.Close() b, err := ioutil.ReadAll(result.Body) suite.NoError(err) - suite.Equal(`{"error":"bad request: old password did not match"}`, string(b)) + suite.Equal(`{"error":"Bad Request: old password did not match"}`, string(b)) } func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() { @@ -138,7 +138,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() { 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", user.PasswordChangePath), nil) + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil) ctx.Request.Header.Set("accept", "application/json") ctx.Request.Form = url.Values{ "old_password": {"password"}, @@ -153,7 +153,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() { defer result.Body.Close() b, err := ioutil.ReadAll(result.Body) suite.NoError(err) - suite.Equal(`{"error":"bad request: password is 94% strength, try including more special characters, using uppercase letters, using numbers or using a longer password"}`, string(b)) + suite.Equal(`{"error":"Bad Request: password is 94% strength, try including more special characters, using uppercase letters, using numbers or using a longer password"}`, string(b)) } func TestPasswordChangeTestSuite(t *testing.T) { diff --git a/internal/api/client/user/user_test.go b/internal/api/client/user/user_test.go index 6e9c46525..b158ec161 100644 --- a/internal/api/client/user/user_test.go +++ b/internal/api/client/user/user_test.go @@ -56,8 +56,8 @@ type UserStandardTestSuite struct { } func (suite *UserStandardTestSuite) SetupTest() { - testrig.InitTestLog() testrig.InitTestConfig() + testrig.InitTestLog() fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1) suite.testTokens = testrig.NewTestTokens() diff --git a/internal/api/errorhandling.go b/internal/api/errorhandling.go new file mode 100644 index 000000000..57659f83c --- /dev/null +++ b/internal/api/errorhandling.go @@ -0,0 +1,127 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package api + +import ( + "context" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" +) + +// TODO: add more templated html pages here for different error types + +// NotFoundHandler serves a 404 html page through the provided gin context, +// if accept is 'text/html', or just returns a json error if 'accept' is empty +// or application/json. +// +// When serving html, NotFoundHandler calls the provided InstanceGet function +// to fetch the apimodel representation of the instance, for serving in the +// 404 header and footer. +// +// If an error is returned by InstanceGet, the function will panic. +func NotFoundHandler(c *gin.Context, instanceGet func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode), accept string) { + switch accept { + case string(TextHTML): + host := config.GetHost() + instance, err := instanceGet(c.Request.Context(), host) + if err != nil { + panic(err) + } + + c.HTML(http.StatusNotFound, "404.tmpl", gin.H{ + "instance": instance, + }) + default: + c.JSON(http.StatusNotFound, gin.H{"error": http.StatusText(http.StatusNotFound)}) + } +} + +// genericErrorHandler is a more general version of the NotFoundHandler, which can +// be used for serving either generic error pages with some rendered help text, +// or just some error json if the caller prefers (or has no preference). +func genericErrorHandler(c *gin.Context, instanceGet func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode), accept string, errWithCode gtserror.WithCode) { + switch accept { + case string(TextHTML): + host := config.GetHost() + instance, err := instanceGet(c.Request.Context(), host) + if err != nil { + panic(err) + } + + c.HTML(errWithCode.Code(), "error.tmpl", gin.H{ + "instance": instance, + "code": errWithCode.Code(), + "error": errWithCode.Safe(), + }) + default: + c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + } +} + +// ErrorHandler takes the provided gin context and errWithCode and tries to serve +// a helpful error to the caller. It will do content negotiation to figure out if +// the caller prefers to see an html page with the error rendered there. If not, or +// if something goes wrong during the function, it will recover and just try to serve +// an appropriate application/json content-type error. +func ErrorHandler(c *gin.Context, errWithCode gtserror.WithCode, instanceGet func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode)) { + path := c.Request.URL.Path + if raw := c.Request.URL.RawQuery; raw != "" { + path = path + "?" + raw + } + + l := logrus.WithFields(logrus.Fields{ + "path": path, + "error": errWithCode.Error(), + }) + + statusCode := errWithCode.Code() + + if statusCode == http.StatusInternalServerError { + l.Error("Internal Server Error") + } else { + l.Debug("handling error") + } + + // if we panic for any reason during error handling, + // we should still try to return a basic code + defer func() { + if p := recover(); p != nil { + l.Warnf("recovered from panic: %s", p) + c.JSON(statusCode, gin.H{"error": errWithCode.Safe()}) + } + }() + + // discover if we're allowed to serve a nice html error page, + // or if we should just use a json. Normally we would want to + // check for a returned error, but if an error occurs here we + // can just fall back to default behavior (serve json error). + accept, _ := NegotiateAccept(c, HTMLOrJSONAcceptHeaders...) + + if statusCode == http.StatusNotFound { + // use our special not found handler with useful status text + NotFoundHandler(c, instanceGet, accept) + } else { + genericErrorHandler(c, instanceGet, accept, errWithCode) + } +} diff --git a/internal/gtserror/unauthorized.go b/internal/api/mime.go similarity index 59% rename from internal/gtserror/unauthorized.go rename to internal/api/mime.go index fb10f748f..0c9595c50 100644 --- a/internal/gtserror/unauthorized.go +++ b/internal/api/mime.go @@ -16,4 +16,19 @@ along with this program. If not, see . */ -package gtserror +package api + +// MIME represents a mime-type. +type MIME string + +// MIME type +const ( + AppJSON MIME = `application/json` + AppXML MIME = `application/xml` + AppActivityJSON MIME = `application/activity+json` + AppActivityLDJSON MIME = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"` + AppForm MIME = `application/x-www-form-urlencoded` + MultipartForm MIME = `multipart/form-data` + TextXML MIME = `text/xml` + TextHTML MIME = `text/html` +) diff --git a/internal/api/negotiate.go b/internal/api/negotiate.go index 4fb763d9f..510694857 100644 --- a/internal/api/negotiate.go +++ b/internal/api/negotiate.go @@ -25,33 +25,40 @@ "github.com/gin-gonic/gin" ) -// Offer represents an offered mime-type. -type Offer string - -const ( - AppJSON Offer = `application/json` // AppJSON is the mime type for 'application/json'. - AppActivityJSON Offer = `application/activity+json` // AppActivityJSON is the mime type for 'application/activity+json'. - AppActivityLDJSON Offer = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"` // AppActivityLDJSON is the mime type for 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' - TextHTML Offer = `text/html` // TextHTML is the mime type for 'text/html'. -) - // ActivityPubAcceptHeaders represents the Accept headers mentioned here: -// https://www.w3.org/TR/activitypub/#retrieving-objects -var ActivityPubAcceptHeaders = []Offer{ +// +var ActivityPubAcceptHeaders = []MIME{ AppActivityJSON, AppActivityLDJSON, } // JSONAcceptHeaders is a slice of offers that just contains application/json types. -var JSONAcceptHeaders = []Offer{ +var JSONAcceptHeaders = []MIME{ + AppJSON, +} + +// HTMLOrJSONAcceptHeaders is a slice of offers that prefers TextHTML and will +// fall back to JSON if necessary. This is useful for error handling, since it can +// be used to serve a nice HTML page if the caller accepts that, or just JSON if not. +var HTMLOrJSONAcceptHeaders = []MIME{ + TextHTML, AppJSON, } // HTMLAcceptHeaders is a slice of offers that just contains text/html types. -var HTMLAcceptHeaders = []Offer{ +var HTMLAcceptHeaders = []MIME{ TextHTML, } +// HTMLOrActivityPubHeaders matches text/html first, then activitypub types. +// This is useful for user URLs that a user might go to in their browser. +// https://www.w3.org/TR/activitypub/#retrieving-objects +var HTMLOrActivityPubHeaders = []MIME{ + TextHTML, + AppActivityJSON, + AppActivityLDJSON, +} + // NegotiateAccept takes the *gin.Context from an incoming request, and a // slice of Offers, and performs content negotiation for the given request // with the given content-type offers. It will return a string representation @@ -73,7 +80,7 @@ // often-used Accept types. // // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation#server-driven_content_negotiation -func NegotiateAccept(c *gin.Context, offers ...Offer) (string, error) { +func NegotiateAccept(c *gin.Context, offers ...MIME) (string, error) { if len(offers) == 0 { return "", errors.New("no format offered") } diff --git a/internal/api/s2s/nodeinfo/nodeinfoget.go b/internal/api/s2s/nodeinfo/nodeinfoget.go index 8b3b1cfba..8f111bd98 100644 --- a/internal/api/s2s/nodeinfo/nodeinfoget.go +++ b/internal/api/s2s/nodeinfo/nodeinfoget.go @@ -23,8 +23,8 @@ "net/http" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) // NodeInfoGETHandler swagger:operation GET /nodeinfo/2.0 nodeInfoGet @@ -45,27 +45,22 @@ // schema: // "$ref": "#/definitions/nodeinfo" func (m *Module) NodeInfoGETHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "NodeInfoGETHandler", - "user-agent": c.Request.UserAgent(), - }) - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - ni, err := m.processor.GetNodeInfo(c.Request.Context(), c.Request) + ni, errWithCode := m.processor.GetNodeInfo(c.Request.Context(), c.Request) + if errWithCode != nil { + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + return + } + + b, err := json.Marshal(ni) if err != nil { - l.Debugf("error with get node info request: %s", err) - c.JSON(err.Code(), err.Safe()) + api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } - b, jsonErr := json.Marshal(ni) - if jsonErr != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": jsonErr.Error()}) - } - c.Data(http.StatusOK, `application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"`, b) } diff --git a/internal/api/s2s/nodeinfo/wellknownget.go b/internal/api/s2s/nodeinfo/wellknownget.go index 6fa3c4623..27e18987f 100644 --- a/internal/api/s2s/nodeinfo/wellknownget.go +++ b/internal/api/s2s/nodeinfo/wellknownget.go @@ -22,8 +22,8 @@ "net/http" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) // NodeInfoWellKnownGETHandler swagger:operation GET /.well-known/nodeinfo nodeInfoWellKnownGet @@ -45,19 +45,14 @@ // schema: // "$ref": "#/definitions/wellKnownResponse" func (m *Module) NodeInfoWellKnownGETHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "NodeInfoWellKnownGETHandler", - }) - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - niRel, err := m.processor.GetNodeInfoRel(c.Request.Context(), c.Request) - if err != nil { - l.Debugf("error with get node info rel request: %s", err) - c.JSON(err.Code(), err.Safe()) + niRel, errWithCode := m.processor.GetNodeInfoRel(c.Request.Context(), c.Request) + if errWithCode != nil { + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/s2s/user/followers.go b/internal/api/s2s/user/followers.go index ce5a03276..77c0e1307 100644 --- a/internal/api/s2s/user/followers.go +++ b/internal/api/s2s/user/followers.go @@ -20,48 +20,45 @@ import ( "encoding/json" - "fmt" + "errors" "net/http" + "strings" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) // FollowersGETHandler returns a collection of URIs for followers of the target user, formatted so that other AP servers can understand it. func (m *Module) FollowersGETHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "FollowersGETHandler", - "url": c.Request.RequestURI, - }) - - requestedUsername := c.Param(UsernameKey) + // usernames on our instance are always lowercase + requestedUsername := strings.ToLower(c.Param(UsernameKey)) if requestedUsername == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) + err := errors.New("no username specified in request") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...) + format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) if err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - l.Tracef("negotiated format: %s", format) - ctx := transferContext(c) + if format == string(api.TextHTML) { + // redirect to the user's profile + c.Redirect(http.StatusSeeOther, "/@"+requestedUsername) + } - followers, errWithCode := m.processor.GetFediFollowers(ctx, requestedUsername, c.Request.URL) + resp, errWithCode := m.processor.GetFediFollowers(transferContext(c), requestedUsername, c.Request.URL) if errWithCode != nil { - l.Info(errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } - b, mErr := json.Marshal(followers) - if mErr != nil { - err := fmt.Errorf("could not marshal json: %s", mErr) - l.Error(err) - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + b, err := json.Marshal(resp) + if err != nil { + api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } diff --git a/internal/api/s2s/user/following.go b/internal/api/s2s/user/following.go index ca3d4bb4c..14dc9343f 100644 --- a/internal/api/s2s/user/following.go +++ b/internal/api/s2s/user/following.go @@ -20,48 +20,45 @@ import ( "encoding/json" - "fmt" + "errors" "net/http" + "strings" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) // FollowingGETHandler returns a collection of URIs for accounts that the target user follows, formatted so that other AP servers can understand it. func (m *Module) FollowingGETHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "FollowingGETHandler", - "url": c.Request.RequestURI, - }) - - requestedUsername := c.Param(UsernameKey) + // usernames on our instance are always lowercase + requestedUsername := strings.ToLower(c.Param(UsernameKey)) if requestedUsername == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) + err := errors.New("no username specified in request") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...) + format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) if err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - l.Tracef("negotiated format: %s", format) - ctx := transferContext(c) + if format == string(api.TextHTML) { + // redirect to the user's profile + c.Redirect(http.StatusSeeOther, "/@"+requestedUsername) + } - following, errWithCode := m.processor.GetFediFollowing(ctx, requestedUsername, c.Request.URL) + resp, errWithCode := m.processor.GetFediFollowing(transferContext(c), requestedUsername, c.Request.URL) if errWithCode != nil { - l.Info(errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } - b, mErr := json.Marshal(following) - if mErr != nil { - err := fmt.Errorf("could not marshal json: %s", mErr) - l.Error(err) - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + b, err := json.Marshal(resp) + if err != nil { + api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } diff --git a/internal/api/s2s/user/inboxpost.go b/internal/api/s2s/user/inboxpost.go index 6e17850fd..fa6792cd2 100644 --- a/internal/api/s2s/user/inboxpost.go +++ b/internal/api/s2s/user/inboxpost.go @@ -19,43 +19,33 @@ package user import ( - "net/http" + "errors" + "strings" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/gtserror" //nolint:typecheck ) // InboxPOSTHandler deals with incoming POST requests to an actor's inbox. // Eg., POST to https://example.org/users/whatever/inbox. func (m *Module) InboxPOSTHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "InboxPOSTHandler", - "url": c.Request.RequestURI, - }) - - requestedUsername := c.Param(UsernameKey) + // usernames on our instance are always lowercase + requestedUsername := strings.ToLower(c.Param(UsernameKey)) if requestedUsername == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) + err := errors.New("no username specified in request") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - ctx := transferContext(c) - - posted, err := m.processor.InboxPost(ctx, c.Writer, c.Request) - if err != nil { + if posted, err := m.processor.InboxPost(transferContext(c), c.Writer, c.Request); err != nil { if withCode, ok := err.(gtserror.WithCode); ok { - l.Debugf("InboxPOSTHandler: %s", withCode.Error()) - c.JSON(withCode.Code(), withCode.Safe()) - return + api.ErrorHandler(c, withCode, m.processor.InstanceGet) + } else { + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) } - l.Debugf("InboxPOSTHandler: error processing request: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "unable to process request"}) - return - } - - if !posted { - l.Debugf("InboxPOSTHandler: request could not be handled as an AP request; headers were: %+v", c.Request.Header) - c.JSON(http.StatusBadRequest, gin.H{"error": "unable to process request"}) + } else if !posted { + err := errors.New("unable to process request") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) } } diff --git a/internal/api/s2s/user/outboxget.go b/internal/api/s2s/user/outboxget.go index b8e6126df..568e48017 100644 --- a/internal/api/s2s/user/outboxget.go +++ b/internal/api/s2s/user/outboxget.go @@ -20,13 +20,15 @@ import ( "encoding/json" + "errors" "fmt" "net/http" "strconv" + "strings" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) // OutboxGETHandler swagger:operation GET /users/{username}/outbox s2sOutboxGet @@ -80,23 +82,31 @@ // '404': // description: not found func (m *Module) OutboxGETHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "OutboxGETHandler", - "url": c.Request.RequestURI, - }) - - requestedUsername := c.Param(UsernameKey) + // usernames on our instance are always lowercase + requestedUsername := strings.ToLower(c.Param(UsernameKey)) if requestedUsername == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) + err := errors.New("no username specified in request") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } + format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) + if err != nil { + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + return + } + + if format == string(api.TextHTML) { + // redirect to the user's profile + c.Redirect(http.StatusSeeOther, "/@"+requestedUsername) + } + var page bool if pageString := c.Query(PageKey); pageString != "" { i, err := strconv.ParseBool(pageString) if err != nil { - l.Debugf("error parsing page string: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse page query param"}) + err := fmt.Errorf("error parsing %s: %s", PageKey, err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } page = i @@ -114,27 +124,15 @@ func (m *Module) OutboxGETHandler(c *gin.Context) { maxID = maxIDString } - format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...) - if err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) - return - } - l.Tracef("negotiated format: %s", format) - - ctx := transferContext(c) - - outbox, errWithCode := m.processor.GetFediOutbox(ctx, requestedUsername, page, maxID, minID, c.Request.URL) + resp, errWithCode := m.processor.GetFediOutbox(transferContext(c), requestedUsername, page, maxID, minID, c.Request.URL) if errWithCode != nil { - l.Info(errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } - b, mErr := json.Marshal(outbox) - if mErr != nil { - err := fmt.Errorf("could not marshal json: %s", mErr) - l.Error(err) - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + b, err := json.Marshal(resp) + if err != nil { + api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } diff --git a/internal/api/s2s/user/publickeyget.go b/internal/api/s2s/user/publickeyget.go index 6e963e191..e929c461d 100644 --- a/internal/api/s2s/user/publickeyget.go +++ b/internal/api/s2s/user/publickeyget.go @@ -20,12 +20,13 @@ import ( "encoding/json" - "fmt" + "errors" "net/http" + "strings" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) // PublicKeyGETHandler should be served at eg https://example.org/users/:username/main-key. @@ -34,38 +35,34 @@ // in the form of a vocab.ActivityStreamsPerson. The account will only contain the id, // public key, username, and type of the account. func (m *Module) PublicKeyGETHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "PublicKeyGETHandler", - "url": c.Request.RequestURI, - }) - - requestedUsername := c.Param(UsernameKey) + // usernames on our instance are always lowercase + requestedUsername := strings.ToLower(c.Param(UsernameKey)) if requestedUsername == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) + err := errors.New("no username specified in request") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...) + format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) if err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - l.Tracef("negotiated format: %s", format) - ctx := transferContext(c) + if format == string(api.TextHTML) { + // redirect to the user's profile + c.Redirect(http.StatusSeeOther, "/@"+requestedUsername) + } - user, errWithCode := m.processor.GetFediUser(ctx, requestedUsername, c.Request.URL) + resp, errWithCode := m.processor.GetFediUser(transferContext(c), requestedUsername, c.Request.URL) if errWithCode != nil { - l.Info(errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } - b, mErr := json.Marshal(user) - if mErr != nil { - err := fmt.Errorf("could not marshal json: %s", mErr) - l.Error(err) - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + b, err := json.Marshal(resp) + if err != nil { + api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } diff --git a/internal/api/s2s/user/repliesget.go b/internal/api/s2s/user/repliesget.go index 388f69885..6ef5a5f43 100644 --- a/internal/api/s2s/user/repliesget.go +++ b/internal/api/s2s/user/repliesget.go @@ -20,13 +20,15 @@ import ( "encoding/json" + "errors" "fmt" "net/http" "strconv" + "strings" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) // StatusRepliesGETHandler swagger:operation GET /users/{username}/statuses/{status}/replies s2sRepliesGet @@ -86,29 +88,39 @@ // '404': // description: not found func (m *Module) StatusRepliesGETHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "StatusRepliesGETHandler", - "url": c.Request.RequestURI, - }) - - requestedUsername := c.Param(UsernameKey) + // usernames on our instance are always lowercase + requestedUsername := strings.ToLower(c.Param(UsernameKey)) if requestedUsername == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) + err := errors.New("no username specified in request") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - requestedStatusID := c.Param(StatusIDKey) + // status IDs on our instance are always uppercase + requestedStatusID := strings.ToUpper(c.Param(StatusIDKey)) if requestedStatusID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no status id specified in request"}) + err := errors.New("no status id specified in request") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } + format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) + if err != nil { + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + return + } + + if format == string(api.TextHTML) { + // redirect to the status + c.Redirect(http.StatusSeeOther, "/@"+requestedUsername+"/statuses/"+requestedStatusID) + } + var page bool if pageString := c.Query(PageKey); pageString != "" { i, err := strconv.ParseBool(pageString) if err != nil { - l.Debugf("error parsing page string: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse page query param"}) + err := fmt.Errorf("error parsing %s: %s", PageKey, err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } page = i @@ -119,8 +131,8 @@ func (m *Module) StatusRepliesGETHandler(c *gin.Context) { if onlyOtherAccountsString != "" { i, err := strconv.ParseBool(onlyOtherAccountsString) if err != nil { - l.Debugf("error parsing only_other_accounts string: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse only_other_accounts query param"}) + err := fmt.Errorf("error parsing %s: %s", OnlyOtherAccountsKey, err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } onlyOtherAccounts = i @@ -132,27 +144,15 @@ func (m *Module) StatusRepliesGETHandler(c *gin.Context) { minID = minIDString } - format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...) - if err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) - return - } - l.Tracef("negotiated format: %s", format) - - ctx := transferContext(c) - - replies, errWithCode := m.processor.GetFediStatusReplies(ctx, requestedUsername, requestedStatusID, page, onlyOtherAccounts, minID, c.Request.URL) + resp, errWithCode := m.processor.GetFediStatusReplies(transferContext(c), requestedUsername, requestedStatusID, page, onlyOtherAccounts, minID, c.Request.URL) if errWithCode != nil { - l.Info(errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } - b, mErr := json.Marshal(replies) - if mErr != nil { - err := fmt.Errorf("could not marshal json: %s", mErr) - l.Error(err) - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + b, err := json.Marshal(resp) + if err != nil { + api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } diff --git a/internal/api/s2s/user/statusget.go b/internal/api/s2s/user/statusget.go index 3dee0c88a..b6690c4de 100644 --- a/internal/api/s2s/user/statusget.go +++ b/internal/api/s2s/user/statusget.go @@ -20,57 +20,53 @@ import ( "encoding/json" - "fmt" + "errors" "net/http" "strings" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) // StatusGETHandler serves the target status as an activitystreams NOTE so that other AP servers can parse it. func (m *Module) StatusGETHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "StatusGETHandler", - "url": c.Request.RequestURI, - }) - // usernames on our instance are always lowercase requestedUsername := strings.ToLower(c.Param(UsernameKey)) if requestedUsername == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) + err := errors.New("no username specified in request") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } // status IDs on our instance are always uppercase requestedStatusID := strings.ToUpper(c.Param(StatusIDKey)) if requestedStatusID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no status id specified in request"}) + err := errors.New("no status id specified in request") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...) + format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) if err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - l.Tracef("negotiated format: %s", format) - ctx := transferContext(c) + if format == string(api.TextHTML) { + // redirect to the status + c.Redirect(http.StatusSeeOther, "/@"+requestedUsername+"/statuses/"+requestedStatusID) + } - status, errWithCode := m.processor.GetFediStatus(ctx, requestedUsername, requestedStatusID, c.Request.URL) + resp, errWithCode := m.processor.GetFediStatus(transferContext(c), requestedUsername, requestedStatusID, c.Request.URL) if errWithCode != nil { - l.Info(errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } - b, mErr := json.Marshal(status) - if mErr != nil { - err := fmt.Errorf("could not marshal json: %s", mErr) - l.Error(err) - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + b, err := json.Marshal(resp) + if err != nil { + api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } diff --git a/internal/api/s2s/user/userget.go b/internal/api/s2s/user/userget.go index c034e6bfc..b5e8a84c9 100644 --- a/internal/api/s2s/user/userget.go +++ b/internal/api/s2s/user/userget.go @@ -20,12 +20,13 @@ import ( "encoding/json" - "fmt" + "errors" "net/http" + "strings" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) // UsersGETHandler should be served at https://example.org/users/:username. @@ -38,38 +39,34 @@ // And of course, the request should be refused if the account or server making the // request is blocked. func (m *Module) UsersGETHandler(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "UsersGETHandler", - "url": c.Request.RequestURI, - }) - - requestedUsername := c.Param(UsernameKey) + // usernames on our instance are always lowercase + requestedUsername := strings.ToLower(c.Param(UsernameKey)) if requestedUsername == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) + err := errors.New("no username specified in request") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } - format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...) + format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) if err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) return } - l.Tracef("negotiated format: %s", format) - ctx := transferContext(c) + if format == string(api.TextHTML) { + // redirect to the user's profile + c.Redirect(http.StatusSeeOther, "/@"+requestedUsername) + } - user, errWithCode := m.processor.GetFediUser(ctx, requestedUsername, c.Request.URL) // GetFediUser handles auth as well + resp, errWithCode := m.processor.GetFediUser(transferContext(c), requestedUsername, c.Request.URL) if errWithCode != nil { - l.Info(errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } - b, mErr := json.Marshal(user) - if mErr != nil { - err := fmt.Errorf("could not marshal json: %s", mErr) - l.Error(err) - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + b, err := json.Marshal(resp) + if err != nil { + api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } diff --git a/internal/api/s2s/webfinger/webfingerget.go b/internal/api/s2s/webfinger/webfingerget.go index a271c031b..7e7ca006b 100644 --- a/internal/api/s2s/webfinger/webfingerget.go +++ b/internal/api/s2s/webfinger/webfingerget.go @@ -27,6 +27,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/ap" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/config" ) @@ -105,10 +106,9 @@ func (m *Module) WebfingerGETRequest(c *gin.Context) { ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeyVerifier, verifier) } - resp, err := m.processor.GetWebfingerAccount(ctx, username) - if err != nil { - l.Debugf("aborting request with an error: %s", err.Error()) - c.JSON(err.Code(), gin.H{"error": err.Safe()}) + resp, errWithCode := m.processor.GetWebfingerAccount(ctx, username) + if errWithCode != nil { + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } diff --git a/internal/api/security/extraheaders.go b/internal/api/security/extraheaders.go index bcb41e0c0..f66ac43b4 100644 --- a/internal/api/security/extraheaders.go +++ b/internal/api/security/extraheaders.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package security import "github.com/gin-gonic/gin" diff --git a/internal/api/security/useragentblock.go b/internal/api/security/useragentblock.go index acbdeae58..b117e8608 100644 --- a/internal/api/security/useragentblock.go +++ b/internal/api/security/useragentblock.go @@ -19,21 +19,17 @@ package security import ( + "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" ) -// UserAgentBlock blocks requests with undesired, empty, or invalid user-agent strings. +// UserAgentBlock aborts requests with empty user agent strings. func (m *Module) UserAgentBlock(c *gin.Context) { - l := logrus.WithFields(logrus.Fields{ - "func": "UserAgentBlock", - }) - if ua := c.Request.UserAgent(); ua == "" { - l.Debug("aborting request because there's no user-agent set") - c.AbortWithStatus(http.StatusTeapot) - return + 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/federation/authenticate.go b/internal/federation/authenticate.go index 1d0a4a7ef..5ca2f9806 100644 --- a/internal/federation/authenticate.go +++ b/internal/federation/authenticate.go @@ -126,7 +126,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU vi := ctx.Value(ap.ContextRequestingPublicKeyVerifier) if vi == nil { err := errors.New("http request wasn't signed or http signature was invalid") - errWithCode := gtserror.NewErrorNotAuthorized(err, err.Error()) + errWithCode := gtserror.NewErrorUnauthorized(err, err.Error()) l.Debug(errWithCode) return nil, errWithCode } @@ -134,7 +134,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU verifier, ok := vi.(httpsig.Verifier) if !ok { err := errors.New("http request wasn't signed or http signature was invalid") - errWithCode := gtserror.NewErrorNotAuthorized(err, err.Error()) + errWithCode := gtserror.NewErrorUnauthorized(err, err.Error()) l.Debug(errWithCode) return nil, errWithCode } @@ -143,7 +143,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU si := ctx.Value(ap.ContextRequestingPublicKeySignature) if si == nil { err := errors.New("http request wasn't signed or http signature was invalid") - errWithCode := gtserror.NewErrorNotAuthorized(err, err.Error()) + errWithCode := gtserror.NewErrorUnauthorized(err, err.Error()) l.Debug(errWithCode) return nil, errWithCode } @@ -151,7 +151,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU signature, ok := si.(string) if !ok { err := errors.New("http request wasn't signed or http signature was invalid") - errWithCode := gtserror.NewErrorNotAuthorized(err, err.Error()) + errWithCode := gtserror.NewErrorUnauthorized(err, err.Error()) l.Debug(errWithCode) return nil, errWithCode } @@ -209,7 +209,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU // The actual http call to the remote server is made right here in the Dereference function. b, err := transport.Dereference(ctx, requestingPublicKeyID) if err != nil { - errWithCode := gtserror.NewErrorNotAuthorized(fmt.Errorf("error dereferencing public key %s: %s", requestingPublicKeyID, err)) + errWithCode := gtserror.NewErrorUnauthorized(fmt.Errorf("error dereferencing public key %s: %s", requestingPublicKeyID, err)) l.Debug(errWithCode) return nil, errWithCode } @@ -217,7 +217,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU // if the key isn't in the response, we can't authenticate the request requestingPublicKey, err := getPublicKeyFromResponse(ctx, b, requestingPublicKeyID) if err != nil { - errWithCode := gtserror.NewErrorNotAuthorized(fmt.Errorf("error parsing public key %s: %s", requestingPublicKeyID, err)) + errWithCode := gtserror.NewErrorUnauthorized(fmt.Errorf("error parsing public key %s: %s", requestingPublicKeyID, err)) l.Debug(errWithCode) return nil, errWithCode } @@ -225,7 +225,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU // we should be able to get the actual key embedded in the vocab.W3IDSecurityV1PublicKey pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem() if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() { - errWithCode := gtserror.NewErrorNotAuthorized(errors.New("publicKeyPem property is not provided or it is not embedded as a value")) + errWithCode := gtserror.NewErrorUnauthorized(errors.New("publicKeyPem property is not provided or it is not embedded as a value")) l.Debug(errWithCode) return nil, errWithCode } @@ -234,14 +234,14 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU pubKeyPem := pkPemProp.Get() block, _ := pem.Decode([]byte(pubKeyPem)) if block == nil || block.Type != "PUBLIC KEY" { - errWithCode := gtserror.NewErrorNotAuthorized(errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type")) + errWithCode := gtserror.NewErrorUnauthorized(errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type")) l.Debug(errWithCode) return nil, errWithCode } publicKey, err = x509.ParsePKIXPublicKey(block.Bytes) if err != nil { - errWithCode := gtserror.NewErrorNotAuthorized(fmt.Errorf("could not parse public key %s from block bytes: %s", requestingPublicKeyID, err)) + errWithCode := gtserror.NewErrorUnauthorized(fmt.Errorf("could not parse public key %s from block bytes: %s", requestingPublicKeyID, err)) l.Debug(errWithCode) return nil, errWithCode } @@ -249,7 +249,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU // all good! we just need the URI of the key owner to return pkOwnerProp := requestingPublicKey.GetW3IDSecurityV1Owner() if pkOwnerProp == nil || !pkOwnerProp.IsIRI() { - errWithCode := gtserror.NewErrorNotAuthorized(errors.New("publicKeyOwner property is not provided or it is not embedded as a value")) + errWithCode := gtserror.NewErrorUnauthorized(errors.New("publicKeyOwner property is not provided or it is not embedded as a value")) l.Debug(errWithCode) return nil, errWithCode } @@ -280,7 +280,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU l.Tracef("authentication for %s NOT PASSED with algorithm %s: %s", pkOwnerURI, algo, err) } - errWithCode := gtserror.NewErrorNotAuthorized(fmt.Errorf("authentication not passed for public key owner %s; signature value was '%s'", pkOwnerURI, signature)) + errWithCode := gtserror.NewErrorUnauthorized(fmt.Errorf("authentication not passed for public key owner %s; signature value was '%s'", pkOwnerURI, signature)) l.Debug(errWithCode) return nil, errWithCode } diff --git a/internal/gtserror/withcode.go b/internal/gtserror/withcode.go index 34889b961..6672000dc 100644 --- a/internal/gtserror/withcode.go +++ b/internal/gtserror/withcode.go @@ -60,7 +60,7 @@ func (e withCode) Code() int { // NewErrorBadRequest returns an ErrorWithCode 400 with the given original error and optional help text. func NewErrorBadRequest(original error, helpText ...string) WithCode { - safe := "bad request" + safe := http.StatusText(http.StatusBadRequest) if helpText != nil { safe = safe + ": " + strings.Join(helpText, ": ") } @@ -71,9 +71,9 @@ func NewErrorBadRequest(original error, helpText ...string) WithCode { } } -// NewErrorNotAuthorized returns an ErrorWithCode 401 with the given original error and optional help text. -func NewErrorNotAuthorized(original error, helpText ...string) WithCode { - safe := "not authorized" +// NewErrorUnauthorized returns an ErrorWithCode 401 with the given original error and optional help text. +func NewErrorUnauthorized(original error, helpText ...string) WithCode { + safe := http.StatusText(http.StatusUnauthorized) if helpText != nil { safe = safe + ": " + strings.Join(helpText, ": ") } @@ -86,7 +86,7 @@ func NewErrorNotAuthorized(original error, helpText ...string) WithCode { // NewErrorForbidden returns an ErrorWithCode 403 with the given original error and optional help text. func NewErrorForbidden(original error, helpText ...string) WithCode { - safe := "forbidden" + safe := http.StatusText(http.StatusForbidden) if helpText != nil { safe = safe + ": " + strings.Join(helpText, ": ") } @@ -99,7 +99,7 @@ func NewErrorForbidden(original error, helpText ...string) WithCode { // NewErrorNotFound returns an ErrorWithCode 404 with the given original error and optional help text. func NewErrorNotFound(original error, helpText ...string) WithCode { - safe := "404 not found" + safe := http.StatusText(http.StatusNotFound) if helpText != nil { safe = safe + ": " + strings.Join(helpText, ": ") } @@ -112,7 +112,7 @@ func NewErrorNotFound(original error, helpText ...string) WithCode { // NewErrorInternalError returns an ErrorWithCode 500 with the given original error and optional help text. func NewErrorInternalError(original error, helpText ...string) WithCode { - safe := "internal server error" + safe := http.StatusText(http.StatusInternalServerError) if helpText != nil { safe = safe + ": " + strings.Join(helpText, ": ") } @@ -125,7 +125,7 @@ func NewErrorInternalError(original error, helpText ...string) WithCode { // NewErrorConflict returns an ErrorWithCode 409 with the given original error and optional help text. func NewErrorConflict(original error, helpText ...string) WithCode { - safe := "conflict" + safe := http.StatusText(http.StatusConflict) if helpText != nil { safe = safe + ": " + strings.Join(helpText, ": ") } @@ -135,3 +135,29 @@ func NewErrorConflict(original error, helpText ...string) WithCode { code: http.StatusConflict, } } + +// NewErrorNotAcceptable returns an ErrorWithCode 406 with the given original error and optional help text. +func NewErrorNotAcceptable(original error, helpText ...string) WithCode { + safe := http.StatusText(http.StatusNotAcceptable) + if helpText != nil { + safe = safe + ": " + strings.Join(helpText, ": ") + } + return withCode{ + original: original, + safe: errors.New(safe), + code: http.StatusNotAcceptable, + } +} + +// NewErrorUnprocessableEntity returns an ErrorWithCode 422 with the given original error and optional help text. +func NewErrorUnprocessableEntity(original error, helpText ...string) WithCode { + safe := http.StatusText(http.StatusUnprocessableEntity) + if helpText != nil { + safe = safe + ": " + strings.Join(helpText, ": ") + } + return withCode{ + original: original, + safe: errors.New(safe), + code: http.StatusUnprocessableEntity, + } +} diff --git a/internal/oidc/handlecallback.go b/internal/oidc/handlecallback.go index c0c1ed453..588fb227b 100644 --- a/internal/oidc/handlecallback.go +++ b/internal/oidc/handlecallback.go @@ -24,24 +24,28 @@ "fmt" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) -func (i *idp) HandleCallback(ctx context.Context, code string) (*Claims, error) { +func (i *idp) HandleCallback(ctx context.Context, code string) (*Claims, gtserror.WithCode) { l := logrus.WithField("func", "HandleCallback") if code == "" { - return nil, errors.New("code was empty string") + err := errors.New("code was empty string") + return nil, gtserror.NewErrorBadRequest(err, err.Error()) } l.Debug("exchanging code for oauth2token") oauth2Token, err := i.oauth2Config.Exchange(ctx, code) if err != nil { - return nil, fmt.Errorf("error exchanging code for oauth2token: %s", err) + err := fmt.Errorf("error exchanging code for oauth2token: %s", err) + return nil, gtserror.NewErrorInternalError(err) } l.Debug("extracting id_token") rawIDToken, ok := oauth2Token.Extra("id_token").(string) if !ok { - return nil, errors.New("no id_token in oauth2token") + err := errors.New("no id_token in oauth2token") + return nil, gtserror.NewErrorBadRequest(err, err.Error()) } l.Debugf("raw id token: %s", rawIDToken) @@ -50,13 +54,15 @@ func (i *idp) HandleCallback(ctx context.Context, code string) (*Claims, error) idTokenVerifier := i.provider.Verifier(i.oidcConf) idToken, err := idTokenVerifier.Verify(ctx, rawIDToken) if err != nil { - return nil, fmt.Errorf("could not verify id token: %s", err) + err = fmt.Errorf("could not verify id token: %s", err) + return nil, gtserror.NewErrorUnauthorized(err, err.Error()) } l.Debug("extracting claims from id_token") claims := &Claims{} if err := idToken.Claims(claims); err != nil { - return nil, fmt.Errorf("could not parse claims from idToken: %s", err) + err := fmt.Errorf("could not parse claims from idToken: %s", err) + return nil, gtserror.NewErrorInternalError(err, err.Error()) } return claims, nil diff --git a/internal/oidc/idp.go b/internal/oidc/idp.go index 7ce535644..90aee81f4 100644 --- a/internal/oidc/idp.go +++ b/internal/oidc/idp.go @@ -24,6 +24,7 @@ "github.com/coreos/go-oidc/v3/oidc" "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "golang.org/x/oauth2" ) @@ -39,7 +40,7 @@ type IDP interface { // with a set of claims. // // Note that this function *does not* verify state. That should be handled by the caller *before* this function is called. - HandleCallback(ctx context.Context, code string) (*Claims, error) + HandleCallback(ctx context.Context, code string) (*Claims, gtserror.WithCode) // AuthCodeURL returns the proper redirect URL for this IDP, for redirecting requesters to the correct OIDC endpoint. AuthCodeURL(state string) string } diff --git a/internal/processing/account.go b/internal/processing/account.go index 43c4f65e6..2c21aee2e 100644 --- a/internal/processing/account.go +++ b/internal/processing/account.go @@ -26,7 +26,7 @@ "github.com/superseriousbusiness/gotosocial/internal/oauth" ) -func (p *processor) AccountCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, error) { +func (p *processor) AccountCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, gtserror.WithCode) { return p.accountProcessor.Create(ctx, authed.Token, authed.Application, form) } @@ -42,7 +42,7 @@ func (p *processor) AccountGetLocalByUsername(ctx context.Context, authed *oauth return p.accountProcessor.GetLocalByUsername(ctx, authed.Account, username) } -func (p *processor) AccountUpdate(ctx context.Context, authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) { +func (p *processor) AccountUpdate(ctx context.Context, authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode) { return p.accountProcessor.Update(ctx, authed.Account, form) } diff --git a/internal/processing/account/account.go b/internal/processing/account/account.go index 2a27fc743..56dfb90e9 100644 --- a/internal/processing/account/account.go +++ b/internal/processing/account/account.go @@ -40,7 +40,7 @@ // Processor wraps a bunch of functions for processing account actions. type Processor interface { // Create processes the given form for creating a new account, returning an oauth token for that account if successful. - Create(ctx context.Context, applicationToken oauth2.TokenInfo, application *gtsmodel.Application, form *apimodel.AccountCreateRequest) (*apimodel.Token, error) + Create(ctx context.Context, applicationToken oauth2.TokenInfo, application *gtsmodel.Application, form *apimodel.AccountCreateRequest) (*apimodel.Token, gtserror.WithCode) // Delete deletes an account, and all of that account's statuses, media, follows, notifications, etc etc etc. // The origin passed here should be either the ID of the account doing the delete (can be itself), or the ID of a domain block. Delete(ctx context.Context, account *gtsmodel.Account, origin string) gtserror.WithCode @@ -52,7 +52,7 @@ type Processor interface { // GetLocalByUsername processes the given request for account information targeting a local account by username. GetLocalByUsername(ctx context.Context, requestingAccount *gtsmodel.Account, username string) (*apimodel.Account, gtserror.WithCode) // Update processes the update of an account with the given form - Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) + Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode) // StatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for // the account given in authed. StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) (*apimodel.TimelineResponse, gtserror.WithCode) diff --git a/internal/processing/account/create.go b/internal/processing/account/create.go index 72626220b..44d6bbea5 100644 --- a/internal/processing/account/create.go +++ b/internal/processing/account/create.go @@ -27,29 +27,30 @@ "github.com/superseriousbusiness/gotosocial/internal/ap" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/messages" "github.com/superseriousbusiness/gotosocial/internal/text" "github.com/superseriousbusiness/oauth2/v4" ) -func (p *processor) Create(ctx context.Context, applicationToken oauth2.TokenInfo, application *gtsmodel.Application, form *apimodel.AccountCreateRequest) (*apimodel.Token, error) { +func (p *processor) Create(ctx context.Context, applicationToken oauth2.TokenInfo, application *gtsmodel.Application, form *apimodel.AccountCreateRequest) (*apimodel.Token, gtserror.WithCode) { l := logrus.WithField("func", "accountCreate") emailAvailable, err := p.db.IsEmailAvailable(ctx, form.Email) if err != nil { - return nil, err + return nil, gtserror.NewErrorBadRequest(err) } if !emailAvailable { - return nil, fmt.Errorf("email address %s in use", form.Email) + return nil, gtserror.NewErrorConflict(fmt.Errorf("email address %s is not available", form.Email)) } usernameAvailable, err := p.db.IsUsernameAvailable(ctx, form.Username) if err != nil { - return nil, err + return nil, gtserror.NewErrorBadRequest(err) } if !usernameAvailable { - return nil, fmt.Errorf("username %s in use", form.Username) + return nil, gtserror.NewErrorConflict(fmt.Errorf("username %s in use", form.Username)) } reasonRequired := config.GetAccountsReasonRequired() @@ -64,19 +65,19 @@ func (p *processor) Create(ctx context.Context, applicationToken oauth2.TokenInf l.Trace("creating new username and account") user, err := p.db.NewSignup(ctx, form.Username, text.SanitizePlaintext(reason), approvalRequired, form.Email, form.Password, form.IP, form.Locale, application.ID, false, false) if err != nil { - return nil, fmt.Errorf("error creating new signup in the database: %s", err) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error creating new signup in the database: %s", err)) } l.Tracef("generating a token for user %s with account %s and application %s", user.ID, user.AccountID, application.ID) accessToken, err := p.oauthServer.GenerateUserAccessToken(ctx, applicationToken, application.ClientSecret, user.ID) if err != nil { - return nil, fmt.Errorf("error creating new access token for user %s: %s", user.ID, err) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error creating new access token for user %s: %s", user.ID, err)) } if user.Account == nil { a, err := p.db.GetAccountByID(ctx, user.AccountID) if err != nil { - return nil, fmt.Errorf("error getting new account from the database: %s", err) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error getting new account from the database: %s", err)) } user.Account = a } diff --git a/internal/processing/account/get.go b/internal/processing/account/get.go index 97f2f0b4a..70c1cd9fe 100644 --- a/internal/processing/account/get.go +++ b/internal/processing/account/get.go @@ -94,5 +94,6 @@ func (p *processor) getAccountFor(ctx context.Context, requestingAccount *gtsmod if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting account: %s", err)) } + return apiAccount, nil } diff --git a/internal/processing/account/update.go b/internal/processing/account/update.go index 42da40ffe..d1085845b 100644 --- a/internal/processing/account/update.go +++ b/internal/processing/account/update.go @@ -29,6 +29,7 @@ "github.com/superseriousbusiness/gotosocial/internal/ap" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/messages" @@ -37,7 +38,7 @@ "github.com/superseriousbusiness/gotosocial/internal/validate" ) -func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) { +func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode) { l := logrus.WithField("func", "AccountUpdate") if form.Discoverable != nil { @@ -50,14 +51,14 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form if form.DisplayName != nil { if err := validate.DisplayName(*form.DisplayName); err != nil { - return nil, err + return nil, gtserror.NewErrorBadRequest(err) } account.DisplayName = text.SanitizePlaintext(*form.DisplayName) } if form.Note != nil { if err := validate.Note(*form.Note); err != nil { - return nil, err + return nil, gtserror.NewErrorBadRequest(err) } // Set the raw note before processing @@ -66,7 +67,7 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form // Process note to generate a valid HTML representation note, err := p.processNote(ctx, *form.Note, account.ID) if err != nil { - return nil, err + return nil, gtserror.NewErrorBadRequest(err) } // Set updated HTML-ified note @@ -76,7 +77,7 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form if form.Avatar != nil && form.Avatar.Size != 0 { avatarInfo, err := p.UpdateAvatar(ctx, form.Avatar, account.ID) if err != nil { - return nil, err + return nil, gtserror.NewErrorBadRequest(err) } account.AvatarMediaAttachmentID = avatarInfo.ID account.AvatarMediaAttachment = avatarInfo @@ -86,7 +87,7 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form if form.Header != nil && form.Header.Size != 0 { headerInfo, err := p.UpdateHeader(ctx, form.Header, account.ID) if err != nil { - return nil, err + return nil, gtserror.NewErrorBadRequest(err) } account.HeaderMediaAttachmentID = headerInfo.ID account.HeaderMediaAttachment = headerInfo @@ -100,7 +101,7 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form if form.Source != nil { if form.Source.Language != nil { if err := validate.Language(*form.Source.Language); err != nil { - return nil, err + return nil, gtserror.NewErrorBadRequest(err) } account.Language = *form.Source.Language } @@ -111,7 +112,7 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form if form.Source.Privacy != nil { if err := validate.Privacy(*form.Source.Privacy); err != nil { - return nil, err + return nil, gtserror.NewErrorBadRequest(err) } privacy := p.tc.APIVisToVis(apimodel.Visibility(*form.Source.Privacy)) account.Privacy = privacy @@ -120,7 +121,7 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form updatedAccount, err := p.db.UpdateAccount(ctx, account) if err != nil { - return nil, fmt.Errorf("could not update account %s: %s", account.ID, err) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("could not update account %s: %s", account.ID, err)) } p.clientWorker.Queue(messages.FromClientAPI{ @@ -132,7 +133,7 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form acctSensitive, err := p.tc.AccountToAPIAccountSensitive(ctx, updatedAccount) if err != nil { - return nil, fmt.Errorf("could not convert account into apisensitive account: %s", err) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("could not convert account into apisensitive account: %s", err)) } return acctSensitive, nil } diff --git a/internal/processing/account/update_test.go b/internal/processing/account/update_test.go index 9f9b6cb77..582dc82e9 100644 --- a/internal/processing/account/update_test.go +++ b/internal/processing/account/update_test.go @@ -45,8 +45,8 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateSimple() { } // should get no error from the update function, and an api model account returned - apiAccount, err := suite.accountProcessor.Update(context.Background(), testAccount, form) - suite.NoError(err) + apiAccount, errWithCode := suite.accountProcessor.Update(context.Background(), testAccount, form) + suite.NoError(errWithCode) suite.NotNil(apiAccount) // fields on the profile should be updated @@ -88,8 +88,8 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateWithMention() { } // should get no error from the update function, and an api model account returned - apiAccount, err := suite.accountProcessor.Update(context.Background(), testAccount, form) - suite.NoError(err) + apiAccount, errWithCode := suite.accountProcessor.Update(context.Background(), testAccount, form) + suite.NoError(errWithCode) suite.NotNil(apiAccount) // fields on the profile should be updated diff --git a/internal/processing/admin/emoji.go b/internal/processing/admin/emoji.go index 78920273c..f91f972a8 100644 --- a/internal/processing/admin/emoji.go +++ b/internal/processing/admin/emoji.go @@ -34,7 +34,7 @@ func (p *processor) EmojiCreate(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, gtserror.WithCode) { if !user.Admin { - return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("user %s not an admin", user.ID), "user is not an admin") + return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("user %s not an admin", user.ID), "user is not an admin") } data := func(innerCtx context.Context) (io.Reader, int, error) { diff --git a/internal/processing/app.go b/internal/processing/app.go index f0d8755c1..2e13c07b9 100644 --- a/internal/processing/app.go +++ b/internal/processing/app.go @@ -23,12 +23,13 @@ "github.com/google/uuid" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) -func (p *processor) AppCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error) { +func (p *processor) AppCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, gtserror.WithCode) { // set default 'read' for scopes if it's not set var scopes string if form.Scopes == "" { @@ -40,13 +41,13 @@ func (p *processor) AppCreate(ctx context.Context, authed *oauth.Auth, form *api // generate new IDs for this application and its associated client clientID, err := id.NewRandomULID() if err != nil { - return nil, err + return nil, gtserror.NewErrorInternalError(err) } clientSecret := uuid.NewString() appID, err := id.NewRandomULID() if err != nil { - return nil, err + return nil, gtserror.NewErrorInternalError(err) } // generate the application to put in the database @@ -62,7 +63,7 @@ func (p *processor) AppCreate(ctx context.Context, authed *oauth.Auth, form *api // chuck it in the db if err := p.db.Put(ctx, app); err != nil { - return nil, err + return nil, gtserror.NewErrorInternalError(err) } // now we need to model an oauth client from the application that the oauth library can use @@ -70,17 +71,18 @@ func (p *processor) AppCreate(ctx context.Context, authed *oauth.Auth, form *api ID: clientID, Secret: clientSecret, Domain: form.RedirectURIs, - UserID: "", // This client isn't yet associated with a specific user, it's just an app client right now + // This client isn't yet associated with a specific user, it's just an app client right now + UserID: "", } // chuck it in the db if err := p.db.Put(ctx, oc); err != nil { - return nil, err + return nil, gtserror.NewErrorInternalError(err) } apiApp, err := p.tc.AppToAPIAppSensitive(ctx, app) if err != nil { - return nil, err + return nil, gtserror.NewErrorInternalError(err) } return apiApp, nil diff --git a/internal/processing/federation/getfollowers.go b/internal/processing/federation/getfollowers.go index a49037397..6c54dbd50 100644 --- a/internal/processing/federation/getfollowers.go +++ b/internal/processing/federation/getfollowers.go @@ -42,7 +42,7 @@ func (p *processor) GetFollowers(ctx context.Context, requestedUsername string, requestingAccount, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false, false) if err != nil { - return nil, gtserror.NewErrorNotAuthorized(err) + return nil, gtserror.NewErrorUnauthorized(err) } blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) @@ -51,7 +51,7 @@ func (p *processor) GetFollowers(ctx context.Context, requestedUsername string, } if blocked { - return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) + return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) } requestedAccountURI, err := url.Parse(requestedAccount.URI) diff --git a/internal/processing/federation/getfollowing.go b/internal/processing/federation/getfollowing.go index a38c049fd..6b1afae92 100644 --- a/internal/processing/federation/getfollowing.go +++ b/internal/processing/federation/getfollowing.go @@ -42,7 +42,7 @@ func (p *processor) GetFollowing(ctx context.Context, requestedUsername string, requestingAccount, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false, false) if err != nil { - return nil, gtserror.NewErrorNotAuthorized(err) + return nil, gtserror.NewErrorUnauthorized(err) } blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) @@ -51,7 +51,7 @@ func (p *processor) GetFollowing(ctx context.Context, requestedUsername string, } if blocked { - return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) + return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) } requestedAccountURI, err := url.Parse(requestedAccount.URI) diff --git a/internal/processing/federation/getoutbox.go b/internal/processing/federation/getoutbox.go index 455f427f3..4e428c1ae 100644 --- a/internal/processing/federation/getoutbox.go +++ b/internal/processing/federation/getoutbox.go @@ -43,7 +43,7 @@ func (p *processor) GetOutbox(ctx context.Context, requestedUsername string, pag requestingAccount, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false, false) if err != nil { - return nil, gtserror.NewErrorNotAuthorized(err) + return nil, gtserror.NewErrorUnauthorized(err) } // authorize the request: @@ -53,7 +53,7 @@ func (p *processor) GetOutbox(ctx context.Context, requestedUsername string, pag return nil, gtserror.NewErrorInternalError(err) } if blocked { - return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) + return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) } var data map[string]interface{} diff --git a/internal/processing/federation/getstatus.go b/internal/processing/federation/getstatus.go index 2cc37071e..ef77d6c4f 100644 --- a/internal/processing/federation/getstatus.go +++ b/internal/processing/federation/getstatus.go @@ -42,7 +42,7 @@ func (p *processor) GetStatus(ctx context.Context, requestedUsername string, req requestingAccount, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false, false) if err != nil { - return nil, gtserror.NewErrorNotAuthorized(err) + return nil, gtserror.NewErrorUnauthorized(err) } // authorize the request: @@ -53,7 +53,7 @@ func (p *processor) GetStatus(ctx context.Context, requestedUsername string, req } if blocked { - return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) + return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) } // get the status out of the database here diff --git a/internal/processing/federation/getstatusreplies.go b/internal/processing/federation/getstatusreplies.go index 984f3a407..3a2c9d944 100644 --- a/internal/processing/federation/getstatusreplies.go +++ b/internal/processing/federation/getstatusreplies.go @@ -44,7 +44,7 @@ func (p *processor) GetStatusReplies(ctx context.Context, requestedUsername stri requestingAccount, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false, false) if err != nil { - return nil, gtserror.NewErrorNotAuthorized(err) + return nil, gtserror.NewErrorUnauthorized(err) } // authorize the request: @@ -55,7 +55,7 @@ func (p *processor) GetStatusReplies(ctx context.Context, requestedUsername stri } if blocked { - return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) + return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) } // get the status out of the database here diff --git a/internal/processing/federation/getuser.go b/internal/processing/federation/getuser.go index f870baa12..63c83f3c5 100644 --- a/internal/processing/federation/getuser.go +++ b/internal/processing/federation/getuser.go @@ -54,7 +54,7 @@ func (p *processor) GetUser(ctx context.Context, requestedUsername string, reque if !p.federator.Handshaking(ctx, requestedUsername, requestingAccountURI) { requestingAccount, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false, false) if err != nil { - return nil, gtserror.NewErrorNotAuthorized(err) + return nil, gtserror.NewErrorUnauthorized(err) } blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) @@ -63,7 +63,7 @@ func (p *processor) GetUser(ctx context.Context, requestedUsername string, reque } if blocked { - return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) + return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) } } diff --git a/internal/processing/media.go b/internal/processing/media.go index 907ce1fd2..2522c4bfb 100644 --- a/internal/processing/media.go +++ b/internal/processing/media.go @@ -26,7 +26,7 @@ "github.com/superseriousbusiness/gotosocial/internal/oauth" ) -func (p *processor) MediaCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) { +func (p *processor) MediaCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, gtserror.WithCode) { return p.mediaProcessor.Create(ctx, authed.Account, form) } diff --git a/internal/processing/media/create.go b/internal/processing/media/create.go index 1eb9fa512..1f40ac48f 100644 --- a/internal/processing/media/create.go +++ b/internal/processing/media/create.go @@ -24,11 +24,12 @@ "io" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/media" ) -func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) { +func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, gtserror.WithCode) { data := func(innerCtx context.Context) (io.Reader, int, error) { f, err := form.File.Open() return f, int(form.File.Size), err @@ -36,7 +37,8 @@ func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, form focusX, focusY, err := parseFocus(form.Focus) if err != nil { - return nil, fmt.Errorf("could not parse focus value %s: %s", form.Focus, err) + err := fmt.Errorf("could not parse focus value %s: %s", form.Focus, err) + return nil, gtserror.NewErrorBadRequest(err, err.Error()) } // process the media attachment and load it immediately @@ -46,19 +48,18 @@ func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, form FocusY: &focusY, }) if err != nil { - return nil, err + return nil, gtserror.NewErrorUnprocessableEntity(err) } attachment, err := media.LoadAttachment(ctx) if err != nil { - return nil, err + return nil, gtserror.NewErrorUnprocessableEntity(err) } - // prepare the frontend representation now -- if there are any errors here at least we can bail without - // having already put something in the database and then having to clean it up again (eugh) apiAttachment, err := p.tc.AttachmentToAPIAttachment(ctx, attachment) if err != nil { - return nil, fmt.Errorf("error parsing media attachment to frontend type: %s", err) + err := fmt.Errorf("error parsing media attachment to frontend type: %s", err) + return nil, gtserror.NewErrorInternalError(err) } return &apiAttachment, nil diff --git a/internal/processing/media/media.go b/internal/processing/media/media.go index e895e0837..05bea615f 100644 --- a/internal/processing/media/media.go +++ b/internal/processing/media/media.go @@ -34,7 +34,7 @@ // Processor wraps a bunch of functions for processing media actions. type Processor interface { // Create creates a new media attachment belonging to the given account, using the request form. - Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) + Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, gtserror.WithCode) // Delete deletes the media attachment with the given ID, including all files pertaining to that attachment. Delete(ctx context.Context, mediaAttachmentID string) gtserror.WithCode // GetFile retrieves a file from storage and streams it back to the caller via an io.reader embedded in *apimodel.Content. diff --git a/internal/processing/processor.go b/internal/processing/processor.go index a10e0d6b3..a7a1c22e0 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -72,7 +72,7 @@ type Processor interface { */ // AccountCreate processes the given form for creating a new account, returning an oauth token for that account if successful. - AccountCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, error) + AccountCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, gtserror.WithCode) // AccountDeleteLocal processes the delete of a LOCAL account using the given form. AccountDeleteLocal(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountDeleteRequest) gtserror.WithCode // AccountGet processes the given request for account information. @@ -80,7 +80,7 @@ type Processor interface { // AccountGet processes the given request for account information. AccountGetLocalByUsername(ctx context.Context, authed *oauth.Auth, username string) (*apimodel.Account, gtserror.WithCode) // AccountUpdate processes the update of an account with the given form - AccountUpdate(ctx context.Context, authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) + AccountUpdate(ctx context.Context, authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode) // AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for // the account given in authed. AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) (*apimodel.TimelineResponse, gtserror.WithCode) @@ -117,7 +117,7 @@ type Processor interface { AdminMediaPrune(ctx context.Context, mediaRemoteCacheDays int) gtserror.WithCode // AppCreate processes the creation of a new API application - AppCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error) + AppCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, gtserror.WithCode) // BlocksGet returns a list of accounts blocked by the requesting account. BlocksGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, limit int) (*apimodel.BlocksResponse, gtserror.WithCode) @@ -143,7 +143,7 @@ type Processor interface { InstancePatch(ctx context.Context, form *apimodel.InstanceSettingsUpdateRequest) (*apimodel.Instance, gtserror.WithCode) // MediaCreate handles the creation of a media attachment, using the given form. - MediaCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) + MediaCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, gtserror.WithCode) // MediaGet handles the GET of a media attachment with the given ID MediaGet(ctx context.Context, authed *oauth.Auth, attachmentID string) (*apimodel.Attachment, gtserror.WithCode) // MediaUpdate handles the PUT of a media attachment with the given ID and form @@ -156,11 +156,11 @@ type Processor interface { SearchGet(ctx context.Context, authed *oauth.Auth, searchQuery *apimodel.SearchQuery) (*apimodel.SearchResult, gtserror.WithCode) // StatusCreate processes the given form to create a new status, returning the api model representation of that status if it's OK. - StatusCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, error) + StatusCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode) // StatusDelete processes the delete of a given status, returning the deleted status if the delete goes through. - StatusDelete(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) + StatusDelete(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) // StatusFave processes the faving of a given status, returning the updated status if the fave goes through. - StatusFave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) + StatusFave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) // StatusBoost processes the boost/reblog of a given status, returning the newly-created boost if all is well. StatusBoost(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) // StatusUnboost processes the unboost/unreblog of a given status, returning the status if all is well. @@ -168,11 +168,11 @@ type Processor interface { // StatusBoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings. StatusBoostedBy(ctx context.Context, authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) // StatusFavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings. - StatusFavedBy(ctx context.Context, authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, error) + StatusFavedBy(ctx context.Context, authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) // StatusGet gets the given status, taking account of privacy settings and blocks etc. - StatusGet(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) + StatusGet(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) // StatusUnfave processes the unfaving of a given status, returning the updated status if the fave goes through. - StatusUnfave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) + StatusUnfave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) // StatusGetContext returns the context (previous and following posts) from the given status ID StatusGetContext(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Context, gtserror.WithCode) @@ -184,7 +184,7 @@ type Processor interface { FavedTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.TimelineResponse, gtserror.WithCode) // AuthorizeStreamingRequest returns a gotosocial account in exchange for an access token, or an error if the given token is not valid. - AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, error) + AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, gtserror.WithCode) // OpenStreamForAccount opens a new stream for the given account, with the given stream type. OpenStreamForAccount(ctx context.Context, account *gtsmodel.Account, streamType string) (*stream.Stream, gtserror.WithCode) diff --git a/internal/processing/status.go b/internal/processing/status.go index 5f287488a..b2f222971 100644 --- a/internal/processing/status.go +++ b/internal/processing/status.go @@ -26,15 +26,15 @@ "github.com/superseriousbusiness/gotosocial/internal/oauth" ) -func (p *processor) StatusCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, error) { +func (p *processor) StatusCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode) { return p.statusProcessor.Create(ctx, authed.Account, authed.Application, form) } -func (p *processor) StatusDelete(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) { +func (p *processor) StatusDelete(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { return p.statusProcessor.Delete(ctx, authed.Account, targetStatusID) } -func (p *processor) StatusFave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) { +func (p *processor) StatusFave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { return p.statusProcessor.Fave(ctx, authed.Account, targetStatusID) } @@ -50,15 +50,15 @@ func (p *processor) StatusBoostedBy(ctx context.Context, authed *oauth.Auth, tar return p.statusProcessor.BoostedBy(ctx, authed.Account, targetStatusID) } -func (p *processor) StatusFavedBy(ctx context.Context, authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, error) { +func (p *processor) StatusFavedBy(ctx context.Context, authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) { return p.statusProcessor.FavedBy(ctx, authed.Account, targetStatusID) } -func (p *processor) StatusGet(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) { +func (p *processor) StatusGet(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { return p.statusProcessor.Get(ctx, authed.Account, targetStatusID) } -func (p *processor) StatusUnfave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) { +func (p *processor) StatusUnfave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { return p.statusProcessor.Unfave(ctx, authed.Account, targetStatusID) } diff --git a/internal/processing/status/create.go b/internal/processing/status/create.go index e5f6e9647..2c509809a 100644 --- a/internal/processing/status/create.go +++ b/internal/processing/status/create.go @@ -57,8 +57,8 @@ func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, appli Text: form.Status, } - if err := p.ProcessReplyToID(ctx, form, account.ID, newStatus); err != nil { - return nil, gtserror.NewErrorInternalError(err) + if errWithCode := p.ProcessReplyToID(ctx, form, account.ID, newStatus); errWithCode != nil { + return nil, errWithCode } if err := p.ProcessMediaIDs(ctx, form, account.ID, newStatus); err != nil { diff --git a/internal/processing/status/status.go b/internal/processing/status/status.go index e8b4a8268..acd588461 100644 --- a/internal/processing/status/status.go +++ b/internal/processing/status/status.go @@ -60,7 +60,7 @@ type Processor interface { */ ProcessVisibility(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error - ProcessReplyToID(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error + ProcessReplyToID(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode ProcessMediaIDs(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error ProcessLanguage(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error ProcessMentions(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error diff --git a/internal/processing/status/util.go b/internal/processing/status/util.go index df645189e..79c416f98 100644 --- a/internal/processing/status/util.go +++ b/internal/processing/status/util.go @@ -26,6 +26,7 @@ "github.com/sirupsen/logrus" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -103,7 +104,7 @@ func (p *processor) ProcessVisibility(ctx context.Context, form *apimodel.Advanc return nil } -func (p *processor) ProcessReplyToID(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error { +func (p *processor) ProcessReplyToID(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode { if form.InReplyToID == "" { return nil } @@ -117,32 +118,37 @@ func (p *processor) ProcessReplyToID(ctx context.Context, form *apimodel.Advance // If this is all OK, then we fetch the repliedStatus and the repliedAccount for later processing. repliedStatus := >smodel.Status{} repliedAccount := >smodel.Account{} - // check replied status exists + is replyable + if err := p.db.GetByID(ctx, form.InReplyToID, repliedStatus); err != nil { if err == db.ErrNoEntries { - return fmt.Errorf("status with id %s not replyable because it doesn't exist", form.InReplyToID) + err := fmt.Errorf("status with id %s not replyable because it doesn't exist", form.InReplyToID) + return gtserror.NewErrorBadRequest(err, err.Error()) } - return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err) + err := fmt.Errorf("db error fetching status with id %s: %s", form.InReplyToID, err) + return gtserror.NewErrorInternalError(err) } if !repliedStatus.Replyable { - return fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID) + err := fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID) + return gtserror.NewErrorForbidden(err, err.Error()) } - // check replied account is known to us if err := p.db.GetByID(ctx, repliedStatus.AccountID, repliedAccount); err != nil { if err == db.ErrNoEntries { - return fmt.Errorf("status with id %s not replyable because account id %s is not known", form.InReplyToID, repliedStatus.AccountID) + err := fmt.Errorf("status with id %s not replyable because account id %s is not known", form.InReplyToID, repliedStatus.AccountID) + return gtserror.NewErrorBadRequest(err, err.Error()) } - return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err) + err := fmt.Errorf("db error fetching account with id %s: %s", repliedStatus.AccountID, err) + return gtserror.NewErrorInternalError(err) } - // check if a block exists + if blocked, err := p.db.IsBlocked(ctx, thisAccountID, repliedAccount.ID, true); err != nil { - if err != db.ErrNoEntries { - return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err) - } + err := fmt.Errorf("db error checking block: %s", err) + return gtserror.NewErrorInternalError(err) } else if blocked { - return fmt.Errorf("status with id %s not replyable", form.InReplyToID) + err := fmt.Errorf("status with id %s not replyable", form.InReplyToID) + return gtserror.NewErrorNotFound(err) } + status.InReplyToID = repliedStatus.ID status.InReplyToAccountID = repliedAccount.ID diff --git a/internal/processing/streaming.go b/internal/processing/streaming.go index 0d51629fa..c5ab52d6e 100644 --- a/internal/processing/streaming.go +++ b/internal/processing/streaming.go @@ -26,7 +26,7 @@ "github.com/superseriousbusiness/gotosocial/internal/stream" ) -func (p *processor) AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, error) { +func (p *processor) AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, gtserror.WithCode) { return p.streamingProcessor.AuthorizeStreamingRequest(ctx, accessToken) } diff --git a/internal/processing/streaming/authorize.go b/internal/processing/streaming/authorize.go index 9f014e723..70e4741e1 100644 --- a/internal/processing/streaming/authorize.go +++ b/internal/processing/streaming/authorize.go @@ -22,29 +22,40 @@ "context" "fmt" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) -func (p *processor) AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, error) { +func (p *processor) AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, gtserror.WithCode) { ti, err := p.oauthServer.LoadAccessToken(ctx, accessToken) if err != nil { - return nil, fmt.Errorf("AuthorizeStreamingRequest: error loading access token: %s", err) + err := fmt.Errorf("could not load access token: %s", err) + return nil, gtserror.NewErrorUnauthorized(err) } uid := ti.GetUserID() if uid == "" { - return nil, fmt.Errorf("AuthorizeStreamingRequest: no userid in token") + err := fmt.Errorf("no userid in token") + return nil, gtserror.NewErrorUnauthorized(err) } - // fetch user's and account for this user id user := >smodel.User{} - if err := p.db.GetByID(ctx, uid, user); err != nil || user == nil { - return nil, fmt.Errorf("AuthorizeStreamingRequest: no user found for validated uid %s", uid) + if err := p.db.GetByID(ctx, uid, user); err != nil { + if err == db.ErrNoEntries { + err := fmt.Errorf("no user found for validated uid %s", uid) + return nil, gtserror.NewErrorUnauthorized(err) + } + return nil, gtserror.NewErrorInternalError(err) } acct, err := p.db.GetAccountByID(ctx, user.AccountID) - if err != nil || acct == nil { - return nil, fmt.Errorf("AuthorizeStreamingRequest: no account retrieved for user with id %s", uid) + if err != nil { + if err == db.ErrNoEntries { + err := fmt.Errorf("no account found for validated uid %s", uid) + return nil, gtserror.NewErrorUnauthorized(err) + } + return nil, gtserror.NewErrorInternalError(err) } return acct, nil diff --git a/internal/processing/streaming/authorize_test.go b/internal/processing/streaming/authorize_test.go index b6b1db729..51cec38be 100644 --- a/internal/processing/streaming/authorize_test.go +++ b/internal/processing/streaming/authorize_test.go @@ -39,7 +39,7 @@ func (suite *AuthorizeTestSuite) TestAuthorize() { suite.Equal(suite.testAccounts["local_account_2"].ID, account2.ID) noAccount, err := suite.streamingProcessor.AuthorizeStreamingRequest(context.Background(), "aaaaaaaaaaaaaaaaaaaaa!!") - suite.EqualError(err, "AuthorizeStreamingRequest: error loading access token: no entries") + suite.EqualError(err, "could not load access token: no entries") suite.Nil(noAccount) } diff --git a/internal/processing/streaming/streaming.go b/internal/processing/streaming/streaming.go index defe52a9c..88c8dde05 100644 --- a/internal/processing/streaming/streaming.go +++ b/internal/processing/streaming/streaming.go @@ -33,7 +33,7 @@ // Processor wraps a bunch of functions for processing streaming. type Processor interface { // AuthorizeStreamingRequest returns an oauth2 token info in response to an access token query from the streaming API - AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, error) + AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, gtserror.WithCode) // OpenStreamForAccount returns a new Stream for the given account, which will contain a channel for passing messages back to the caller. OpenStreamForAccount(ctx context.Context, account *gtsmodel.Account, timeline string) (*stream.Stream, gtserror.WithCode) // StreamUpdateToAccount streams the given update to any open, appropriate streams belonging to the given account. diff --git a/internal/processing/user/changepassword_test.go b/internal/processing/user/changepassword_test.go index b88b11b3d..e769f4cc0 100644 --- a/internal/processing/user/changepassword_test.go +++ b/internal/processing/user/changepassword_test.go @@ -57,7 +57,7 @@ func (suite *ChangePasswordTestSuite) TestChangePasswordIncorrectOld() { errWithCode := suite.user.ChangePassword(context.Background(), user, "ooooopsydoooopsy", "verygoodnewpassword") suite.EqualError(errWithCode, "crypto/bcrypt: hashedPassword is not the hash of the given password") suite.Equal(http.StatusBadRequest, errWithCode.Code()) - suite.Equal("bad request: old password did not match", errWithCode.Safe()) + suite.Equal("Bad Request: old password did not match", errWithCode.Safe()) } func (suite *ChangePasswordTestSuite) TestChangePasswordWeakNew() { @@ -66,7 +66,7 @@ func (suite *ChangePasswordTestSuite) TestChangePasswordWeakNew() { errWithCode := suite.user.ChangePassword(context.Background(), user, "password", "1234") suite.EqualError(errWithCode, "password is 11% strength, try including more special characters, using lowercase letters, using uppercase letters or using a longer password") suite.Equal(http.StatusBadRequest, errWithCode.Code()) - suite.Equal("bad request: password is 11% strength, try including more special characters, using lowercase letters, using uppercase letters or using a longer password", errWithCode.Safe()) + suite.Equal("Bad Request: password is 11% strength, try including more special characters, using lowercase letters, using uppercase letters or using a longer password", errWithCode.Safe()) } func TestChangePasswordTestSuite(t *testing.T) { diff --git a/internal/transport/deliver.go b/internal/transport/deliver.go index 0405116ad..16ebbd4b6 100644 --- a/internal/transport/deliver.go +++ b/internal/transport/deliver.go @@ -27,6 +27,7 @@ "strings" "sync" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/config" ) @@ -79,7 +80,7 @@ func (t *transport) Deliver(ctx context.Context, b []byte, to *url.URL) error { return err } - req.Header.Add("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") + req.Header.Add("Content-Type", string(api.AppActivityLDJSON)) req.Header.Add("Accept-Charset", "utf-8") req.Header.Add("User-Agent", t.controller.userAgent) req.Header.Set("Host", to.Host) diff --git a/internal/transport/dereference.go b/internal/transport/dereference.go index ba48488d7..cb1220d13 100644 --- a/internal/transport/dereference.go +++ b/internal/transport/dereference.go @@ -25,6 +25,7 @@ "net/http" "net/url" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/uris" ) @@ -52,7 +53,8 @@ func (t *transport) Dereference(ctx context.Context, iri *url.URL) ([]byte, erro if err != nil { return nil, err } - req.Header.Add("Accept", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\",application/activity+json") + + req.Header.Add("Accept", string(api.AppActivityLDJSON)+","+string(api.AppActivityJSON)) req.Header.Add("Accept-Charset", "utf-8") req.Header.Add("User-Agent", t.controller.userAgent) req.Header.Set("Host", iri.Host) diff --git a/internal/transport/derefinstance.go b/internal/transport/derefinstance.go index 1acbcc364..b99d28a23 100644 --- a/internal/transport/derefinstance.go +++ b/internal/transport/derefinstance.go @@ -30,6 +30,7 @@ "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" @@ -94,7 +95,7 @@ func dereferenceByAPIV1Instance(ctx context.Context, t *transport, iri *url.URL) return nil, err } - req.Header.Add("Accept", "application/json") + req.Header.Add("Accept", string(api.AppJSON)) req.Header.Add("User-Agent", t.controller.userAgent) req.Header.Set("Host", cleanIRI.Host) @@ -245,7 +246,7 @@ func callNodeInfoWellKnown(ctx context.Context, t *transport, iri *url.URL) (*ur if err != nil { return nil, err } - req.Header.Add("Accept", "application/json") + req.Header.Add("Accept", string(api.AppJSON)) req.Header.Add("User-Agent", t.controller.userAgent) req.Header.Set("Host", cleanIRI.Host) @@ -297,7 +298,7 @@ func callNodeInfo(ctx context.Context, t *transport, iri *url.URL) (*apimodel.No if err != nil { return nil, err } - req.Header.Add("Accept", "application/json") + req.Header.Add("Accept", string(api.AppJSON)) req.Header.Add("User-Agent", t.controller.userAgent) req.Header.Set("Host", iri.Host) diff --git a/internal/transport/finger.go b/internal/transport/finger.go index 7554a242f..2ea34ad81 100644 --- a/internal/transport/finger.go +++ b/internal/transport/finger.go @@ -23,6 +23,8 @@ "fmt" "io/ioutil" "net/http" + + "github.com/superseriousbusiness/gotosocial/internal/api" ) func (t *transport) Finger(ctx context.Context, targetUsername string, targetDomain string) ([]byte, error) { @@ -37,7 +39,7 @@ func (t *transport) Finger(ctx context.Context, targetUsername string, targetDom if err != nil { return nil, err } - req.Header.Add("Accept", "application/json") + req.Header.Add("Accept", string(api.AppJSON)) req.Header.Add("Accept", "application/jrd+json") req.Header.Add("User-Agent", t.controller.userAgent) req.Header.Set("Host", req.URL.Host) diff --git a/internal/web/base.go b/internal/web/base.go index d203522ae..a8d99619c 100644 --- a/internal/web/base.go +++ b/internal/web/base.go @@ -19,6 +19,7 @@ package web import ( + "errors" "fmt" "io/ioutil" "net/http" @@ -29,6 +30,7 @@ "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/processing" "github.com/superseriousbusiness/gotosocial/internal/router" "github.com/superseriousbusiness/gotosocial/internal/uris" @@ -118,24 +120,6 @@ func (m *Module) baseHandler(c *gin.Context) { }) } -// NotFoundHandler serves a 404 html page instead of a blank 404 error. -func (m *Module) NotFoundHandler(c *gin.Context) { - l := logrus.WithField("func", "404") - l.Trace("serving 404 html") - - host := config.GetHost() - instance, err := m.processor.InstanceGet(c.Request.Context(), host) - if err != nil { - l.Debugf("error getting instance from processor: %s", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) - return - } - - c.HTML(404, "404.tmpl", gin.H{ - "instance": instance, - }) -} - // Route satisfies the RESTAPIModule interface func (m *Module) Route(s router.Router) error { // serve static files from assets dir at /assets @@ -152,16 +136,18 @@ func (m *Module) Route(s router.Router) error { s.AttachHandler(http.MethodGet, "/", m.baseHandler) // serve profile pages at /@username - s.AttachHandler(http.MethodGet, profilePath, m.profileTemplateHandler) + s.AttachHandler(http.MethodGet, profilePath, m.profileGETHandler) // serve statuses - s.AttachHandler(http.MethodGet, statusPath, m.threadTemplateHandler) + s.AttachHandler(http.MethodGet, statusPath, m.threadGETHandler) // serve email confirmation page at /confirm_email?token=whatever s.AttachHandler(http.MethodGet, confirmEmailPath, m.confirmEmailGETHandler) // 404 handler - s.AttachNoRouteHandler(m.NotFoundHandler) + s.AttachNoRouteHandler(func(c *gin.Context) { + api.ErrorHandler(c, gtserror.NewErrorNotFound(errors.New(http.StatusText(http.StatusNotFound))), m.processor.InstanceGet) + }) return nil } diff --git a/internal/web/confirmemail.go b/internal/web/confirmemail.go index 7c8aa464f..58f932bde 100644 --- a/internal/web/confirmemail.go +++ b/internal/web/confirmemail.go @@ -19,35 +19,35 @@ package web import ( + "errors" "net/http" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) func (m *Module) confirmEmailGETHandler(c *gin.Context) { + ctx := c.Request.Context() + // if there's no token in the query, just serve the 404 web handler token := c.Query(tokenParam) if token == "" { - m.NotFoundHandler(c) + api.ErrorHandler(c, gtserror.NewErrorNotFound(errors.New(http.StatusText(http.StatusNotFound))), m.processor.InstanceGet) return } - ctx := c.Request.Context() - user, errWithCode := m.processor.UserConfirmEmail(ctx, token) if errWithCode != nil { - logrus.Debugf("error confirming email: %s", errWithCode.Error()) - // if something goes wrong, just log it and direct to the 404 handler to not give anything away - m.NotFoundHandler(c) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } host := config.GetHost() instance, err := m.processor.InstanceGet(ctx, host) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } diff --git a/internal/web/profile.go b/internal/web/profile.go index 3155c022d..051d55d28 100644 --- a/internal/web/profile.go +++ b/internal/web/profile.go @@ -21,59 +21,60 @@ import ( "context" "encoding/json" + "errors" "fmt" "math/rand" "net/http" + "strings" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/api" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) -func (m *Module) profileTemplateHandler(c *gin.Context) { - l := logrus.WithField("func", "profileTemplateHandler") - l.Trace("rendering profile template") +func (m *Module) profileGETHandler(c *gin.Context) { ctx := c.Request.Context() - username := c.Param(usernameKey) - if username == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no account username specified"}) - return - } - authed, err := oauth.Authed(c, false, false, false, false) if err != nil { - l.Errorf("error authing profile GET request: %s", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) return } - instance, errWithCode := m.processor.InstanceGet(ctx, config.GetHost()) - if errWithCode != nil { - l.Debugf("error getting instance from processor: %s", errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + // usernames on our instance will always be lowercase + 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) return } + host := config.GetHost() + instance, err := m.processor.InstanceGet(ctx, host) + if err != nil { + api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) + return + } + + instanceGet := func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode) { + return instance, nil + } + account, errWithCode := m.processor.AccountGetLocalByUsername(ctx, authed, username) if errWithCode != nil { - l.Debugf("error getting account from processor: %s", errWithCode.Error()) - if errWithCode.Code() == http.StatusNotFound { - m.NotFoundHandler(c) - return - } - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, instanceGet) return } - // if we're getting an AP request on this endpoint we should render the account's AP representation instead + // 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) { - m.returnAPRepresentation(ctx, c, username, accept) + m.returnAPProfile(ctx, c, username, accept) return } @@ -82,8 +83,7 @@ func (m *Module) profileTemplateHandler(c *gin.Context) { // with or without media statusResp, errWithCode := m.processor.AccountStatusesGet(ctx, authed, account.ID, 10, true, true, "", "", false, false, true) if errWithCode != nil { - l.Debugf("error getting statuses from processor: %s", errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, instanceGet) return } @@ -114,7 +114,7 @@ func (m *Module) profileTemplateHandler(c *gin.Context) { }) } -func (m *Module) returnAPRepresentation(ctx context.Context, c *gin.Context, username string, accept string) { +func (m *Module) returnAPProfile(ctx context.Context, c *gin.Context, username string, accept string) { verifier, signed := c.Get(string(ap.ContextRequestingPublicKeyVerifier)) if signed { ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeyVerifier, verifier) @@ -125,17 +125,16 @@ func (m *Module) returnAPRepresentation(ctx context.Context, c *gin.Context, use ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeySignature, signature) } - user, errWithCode := m.processor.GetFediUser(ctx, username, c.Request.URL) // GetFediUser handles auth as well + user, errWithCode := m.processor.GetFediUser(ctx, username, c.Request.URL) if errWithCode != nil { - logrus.Infof(errWithCode.Error()) - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } b, mErr := json.Marshal(user) if mErr != nil { err := fmt.Errorf("could not marshal json: %s", mErr) - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } diff --git a/internal/web/thread.go b/internal/web/thread.go index 3a9839281..5e953ee06 100644 --- a/internal/web/thread.go +++ b/internal/web/thread.go @@ -19,65 +19,88 @@ package web import ( + "context" + "encoding/json" + "errors" + "fmt" "net/http" "strings" - "github.com/sirupsen/logrus" - "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" "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) -func (m *Module) threadTemplateHandler(c *gin.Context) { - l := logrus.WithField("func", "threadTemplateGET") - l.Trace("rendering thread template") - +func (m *Module) threadGETHandler(c *gin.Context) { ctx := c.Request.Context() + authed, err := oauth.Authed(c, false, false, false, false) + if err != nil { + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + return + } + // usernames on our instance will always be lowercase username := strings.ToLower(c.Param(usernameKey)) if username == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no account username specified"}) + err := errors.New("no account username specified") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } // status ids will always be uppercase statusID := strings.ToUpper(c.Param(statusIDKey)) if statusID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "no status id specified"}) - return - } - - authed, err := oauth.Authed(c, false, false, false, false) - if err != nil { - l.Errorf("error authing status GET request: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "status not found"}) + err := errors.New("no status id specified") + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) return } host := config.GetHost() instance, err := m.processor.InstanceGet(ctx, host) if err != nil { - l.Debugf("error getting instance from processor: %s", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } - status, err := m.processor.StatusGet(ctx, authed, statusID) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "status not found"}) + instanceGet := func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode) { + return instance, nil + } + + // 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) + return + } + + status, errWithCode := m.processor.StatusGet(ctx, authed, statusID) + if errWithCode != nil { + api.ErrorHandler(c, errWithCode, instanceGet) return } if !strings.EqualFold(username, status.Account.Username) { - c.JSON(http.StatusBadRequest, gin.H{"error": "status not found"}) + err := gtserror.NewErrorNotFound(errors.New("path username not equal to status author username")) + api.ErrorHandler(c, gtserror.NewErrorNotFound(err), instanceGet) return } - context, err := m.processor.StatusGetContext(ctx, authed, statusID) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "status not found"}) + // 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) { + m.returnAPStatus(ctx, c, username, statusID, accept) + return + } + + context, errWithCode := m.processor.StatusGetContext(ctx, authed, statusID) + if errWithCode != nil { + api.ErrorHandler(c, errWithCode, instanceGet) return } @@ -88,3 +111,30 @@ func (m *Module) threadTemplateHandler(c *gin.Context) { "stylesheets": []string{"/assets/Fork-Awesome/css/fork-awesome.min.css", "/assets/status.css"}, }) } + +func (m *Module) returnAPStatus(ctx context.Context, c *gin.Context, username string, statusID string, accept string) { + 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) + } + + status, errWithCode := m.processor.GetFediStatus(ctx, username, statusID, c.Request.URL) + if errWithCode != nil { + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + 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) + return + } + + c.Data(http.StatusOK, accept, b) +} diff --git a/testrig/router.go b/testrig/router.go index e06e21bd4..5ab228611 100644 --- a/testrig/router.go +++ b/testrig/router.go @@ -20,10 +20,8 @@ import ( "context" - "fmt" "os" "path/filepath" - "runtime" "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/config" @@ -54,25 +52,7 @@ func NewTestRouter(db db.DB) router.Router { } // ConfigureTemplatesWithGin will panic on any errors related to template loading during tests -func ConfigureTemplatesWithGin(engine *gin.Engine) { +func ConfigureTemplatesWithGin(engine *gin.Engine, templatePath string) { router.LoadTemplateFunctions(engine) - - templateBaseDir := config.GetWebTemplateBaseDir() - - if !filepath.IsAbs(templateBaseDir) { - // https://stackoverflow.com/questions/31873396/is-it-possible-to-get-the-current-root-of-package-structure-as-a-string-in-golan - _, runtimeCallerLocation, _, _ := runtime.Caller(0) - projectRoot, err := filepath.Abs(filepath.Join(filepath.Dir(runtimeCallerLocation), "../")) - if err != nil { - panic(err) - } - - templateBaseDir = filepath.Join(projectRoot, templateBaseDir) - } - - if _, err := os.Stat(filepath.Join(templateBaseDir, "index.tmpl")); err != nil { - panic(fmt.Errorf("%s doesn't seem to contain the templates; index.tmpl is missing: %w", templateBaseDir, err)) - } - - engine.LoadHTMLGlob(filepath.Join(templateBaseDir, "*")) + engine.LoadHTMLGlob(filepath.Join(templatePath, "*")) } diff --git a/web/template/404.tmpl b/web/template/404.tmpl index 096f7f364..7153bfd40 100644 --- a/web/template/404.tmpl +++ b/web/template/404.tmpl @@ -2,7 +2,17 @@

404: Page Not Found

- If you believe this was an error, you can contact an admin +

+ GoToSocial only serves Public statuses via the web. + If you reached this page by clicking on a status link, + it's possible that the status is not Public, has been + deleted by the author, you don't have permission to see + it, or it just doesn't exist at all. +

+

+ If you believe this 404 was an error, you can contact + the instance admin. +

diff --git a/web/template/error.tmpl b/web/template/error.tmpl index a8a72b062..c7df695dd 100644 --- a/web/template/error.tmpl +++ b/web/template/error.tmpl @@ -3,6 +3,5 @@
{{.error}}
- {{ template "footer.tmpl" .}} \ No newline at end of file diff --git a/web/template/footer.tmpl b/web/template/footer.tmpl index 5aba0e1a9..f95d8b71b 100644 --- a/web/template/footer.tmpl +++ b/web/template/footer.tmpl @@ -5,10 +5,14 @@ Source Code
+ {{ if .instance.ContactAccount }} Contact: {{.instance.ContactAccount.Username}}
+ {{ end }}
+ {{ if .instance.Email }} Email: {{.instance.Email}}
+ {{ end }}
From f30a5812681efcd4df6d592e04f3547ec4162b93 Mon Sep 17 00:00:00 2001 From: f0x52 Date: Wed, 8 Jun 2022 21:31:12 +0200 Subject: [PATCH 09/13] [chore] gitignore shell.nix (#639) --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b1f97a828..3af4a6508 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,7 @@ web/assets/swagger.yaml example/docker-compose/docker-volume # excludes debug build -cmd/gotosocial/__debug_bin \ No newline at end of file +cmd/gotosocial/__debug_bin + +# ignore f0x' nix-shell +shell.nix \ No newline at end of file From b43f9ceca9f7e02248f1d88245ede5267e8b72c8 Mon Sep 17 00:00:00 2001 From: f0x52 Date: Thu, 9 Jun 2022 12:51:19 +0200 Subject: [PATCH 10/13] [frontend] Restructure Frontend Sources (#634) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐸restructure frontend stuff, include admin and future user panel in main repo, properly deduplicate bundles for css+js across uses * rename bundled to dist, caught by gitignore * re-include status.css for profile template * default to localhost * serve frontend panels * add todo message for abstraction * refactor oauth registration flow * oauth restructure * update footer template * change panel routes * remove superfluous css imports * write bundle to disk from test server, use forked budo-express * wrap all page content in container for robustness with addons etc injection other elements in body * update documentation, goreleaser, Dockerfile * update template meta tags * add AGPL-3.0+ license header everywhere * only attach update listener on EventEmitter * cleaner config for various frontend bundles * fix bundler script paths * Merge commit 'd191931932b9293ce1be44ed08a1e69b9fcc1e25' * fix up dockerfile, goreleaser * go mod tidy * add uglifyify * move status hide/show js to frontend bundle * fix stylesheet color( func regressions * update contributing docs for new build path * update goreleaser + docker building * resolve dependency paths properly * update package name * use api errorhandler Co-authored-by: tsmethurst --- .dockerignore | 5 +- .goreleaser.yml | 33 +- CONTRIBUTING.md | 37 +- Dockerfile | 35 +- docs/admin/admin_panel.md | 12 +- go.sum | 2 - internal/web/base.go | 67 +- internal/web/profile.go | 7 +- internal/web/thread.go | 14 +- scripts/bundle.sh | 8 + web/assets/base.css | 230 - web/assets/colors.css | 1 - web/assets/profile.css | 110 - web/assets/status.css | 230 - web/gotosocial-styling/.eslintrc.js | 5 - web/gotosocial-styling/.gitignore | 3 - web/gotosocial-styling/LICENSE | 661 -- web/gotosocial-styling/README.md | 18 - web/gotosocial-styling/colors.css | 9 - web/gotosocial-styling/index.js | 85 - web/gotosocial-styling/package.json | 21 - web/gotosocial-styling/yarn.lock | 409 -- web/source/.eslintrc.js | 3 + web/source/.gitignore | 2 + web/source/css/_colors.css | 35 + .../templates => source/css}/base.css | 37 +- .../templates => source/css}/profile.css | 24 +- .../templates => source/css}/status.css | 20 +- web/source/dev-server.js | 73 + web/source/frontend/index.js | 41 + web/source/index.js | 108 + web/source/lib/oauth.js | 220 + web/source/lib/split-css.js | 76 + web/source/package.json | 47 + web/source/panels/admin/README.md | 21 + web/source/panels/admin/auth.js | 96 + web/source/panels/admin/blocks.js | 318 + web/source/panels/admin/index.js | 95 + web/source/panels/admin/settings.js | 175 + web/source/panels/admin/style.css | 141 + web/source/panels/user/index.js | 31 + web/source/yarn.lock | 6307 +++++++++++++++++ web/template/footer.tmpl | 40 +- web/template/frontend.tmpl | 5 + web/template/header.tmpl | 32 +- web/template/profile.tmpl | 15 - web/template/thread.tmpl | 15 - 47 files changed, 8061 insertions(+), 1918 deletions(-) create mode 100755 scripts/bundle.sh delete mode 100644 web/assets/base.css delete mode 100644 web/assets/colors.css delete mode 100644 web/assets/profile.css delete mode 100644 web/assets/status.css delete mode 100644 web/gotosocial-styling/.eslintrc.js delete mode 100644 web/gotosocial-styling/.gitignore delete mode 100644 web/gotosocial-styling/LICENSE delete mode 100644 web/gotosocial-styling/README.md delete mode 100644 web/gotosocial-styling/colors.css delete mode 100644 web/gotosocial-styling/index.js delete mode 100644 web/gotosocial-styling/package.json delete mode 100644 web/gotosocial-styling/yarn.lock create mode 100644 web/source/.eslintrc.js create mode 100644 web/source/.gitignore create mode 100644 web/source/css/_colors.css rename web/{gotosocial-styling/templates => source/css}/base.css (75%) rename web/{gotosocial-styling/templates => source/css}/profile.css (66%) rename web/{gotosocial-styling/templates => source/css}/status.css (80%) create mode 100644 web/source/dev-server.js create mode 100644 web/source/frontend/index.js create mode 100644 web/source/index.js create mode 100644 web/source/lib/oauth.js create mode 100644 web/source/lib/split-css.js create mode 100644 web/source/package.json create mode 100644 web/source/panels/admin/README.md create mode 100644 web/source/panels/admin/auth.js create mode 100644 web/source/panels/admin/blocks.js create mode 100644 web/source/panels/admin/index.js create mode 100644 web/source/panels/admin/settings.js create mode 100644 web/source/panels/admin/style.css create mode 100644 web/source/panels/user/index.js create mode 100644 web/source/yarn.lock create mode 100644 web/template/frontend.tmpl diff --git a/.dockerignore b/.dockerignore index 0a1dd90cb..228ff6a5f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,9 @@ .github -cmd +.vscode +archive +dist docs example -internal scripts test testrig diff --git a/.goreleaser.yml b/.goreleaser.yml index fbf3ec687..8a6cc802c 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -3,16 +3,12 @@ project_name: gotosocial before: # https://goreleaser.com/customization/hooks/ hooks: - # tidy up and lint - - go mod tidy - - go fmt ./... # generate the swagger.yaml file using go-swagger and bundle it into the assets directory - - swagger generate spec -o docs/api/swagger.yaml --scan-models - - sed -i "s/REPLACE_ME/{{ incpatch .Version }}/" docs/api/swagger.yaml - - cp docs/api/swagger.yaml web/assets/swagger.yaml - # install and bundle the web assets and styling - - yarn install --cwd web/gotosocial-styling - - node web/gotosocial-styling/index.js --build-dir="web/assets" + - swagger generate spec -o web/assets/swagger.yaml --scan-models + - sed -i "s/REPLACE_ME/{{ incpatch .Version }}/" web/assets/swagger.yaml + # bundle web assets + - yarn install --cwd web/source + - scripts/bundle.sh builds: # https://goreleaser.com/customization/build/ - @@ -67,6 +63,10 @@ dockers: - "--label=org.opencontainers.image.version={{.Version}}" extra_files: - web + - go.mod + - go.sum + - cmd + - internal - use: buildx goos: linux @@ -82,6 +82,10 @@ dockers: - "--label=org.opencontainers.image.version={{.Version}}" extra_files: - web + - go.mod + - go.sum + - cmd + - internal - use: buildx goos: linux @@ -98,6 +102,10 @@ dockers: - "--label=org.opencontainers.image.version={{.Version}}" extra_files: - web + - go.mod + - go.sum + - cmd + - internal - use: buildx goos: linux @@ -114,6 +122,10 @@ dockers: - "--label=org.opencontainers.image.version={{.Version}}" extra_files: - web + - go.mod + - go.sum + - cmd + - internal docker_manifests: - name_template: superseriousbusiness/{{ .ProjectName }}:{{ .Version }} image_templates: @@ -136,7 +148,8 @@ archives: - README.md - CHANGELOG* # web assets - - web + - web/assets + - web/template # example config files - example/config.yaml - example/gotosocial.service diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4cedd6698..636acc90e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,7 +28,8 @@ Check the [issues](https://github.com/superseriousbusiness/gotosocial/issues) to - [Updating Swagger docs](#updating-swagger-docs) - [CI/CD configuration](#cicd-configuration) - [Release Checklist](#release-checklist) -- [Building releases and Docker containers](#building-releases-and-docker-containers) + - [What if something goes wrong?](#what-if-something-goes-wrong) +- [Building Docker containers](#building-docker-containers) - [With GoReleaser](#with-goreleaser) - [Manually](#manually) - [Financial Compensation](#financial-compensation) @@ -70,19 +71,21 @@ To work with the stylesheet for templates, you need [Node.js](https://nodejs.org To install Yarn dependencies: ```bash -yarn install --cwd web/gotosocial-styling +yarn install --cwd web/source ``` To recompile bundles: ```bash -node web/gotosocial-styling/index.js --build-dir="web/assets" +BUDO_BUILD=1 node web/source ``` -You can do automatic live-reloads of bundles with: +Or you can run livereloading in development. It will start a webserver (ip/port printed in console, default localhost:8081), while also keeping the bundles +up-to-date on disk. You can access the user/admin panels at localhost:8080/user, localhost:8080/admin, or run in tandem with the GoToSocial testrig, which will also +serve the updated bundles from disk. ``` bash -NODE_ENV=development node web/gotosocial-styling/index.js --build-dir="web/assets" +NODE_ENV=development node web/source ``` ### Golang forking quirks @@ -185,7 +188,7 @@ Finally, to run tests against both database types one after the other, use: ### CLI Tests -In [./test/cliparsing.sh](./test/cliparsing.sh) there are a bunch of tests for making sure that CLI flags, config, and environment variables get parsed as expected. +In [./test/cliparsing.sh](./test/cliparsing.sh) and [./test/envparsing.sh](./test/envparsing.sh) there are a bunch of tests for making sure that CLI flags, config, and environment variables get parsed as expected. Although these tests *are* part of the CI/CD testing process, you probably won't need to worry too much about running them yourself. That is, unless you're messing about with code inside the `main` package in `cmd/gotosocial`, or inside the `config` package in `internal/config`. @@ -299,7 +302,9 @@ That is: Delete the tag. Either way, once we've fixed the issue, we just start from the top of this list again. Version numbers are cheap. It's cheap to burn them. -## Building releases and Docker containers +## Building Docker containers + +For both of the below methods, you need to have [Docker buildx](https://docs.docker.com/buildx/working-with-buildx/) installed. ### With GoReleaser @@ -311,22 +316,30 @@ Normally, these processes are handled by Drone (see CI/CD above). However, you c To do this, first [install GoReleaser](https://goreleaser.com/install/). -Then, to create snapshot builds, do: +Then install GoSwagger as described in [the Swagger section](#updating-swagger-docs). + +Then install Node and Yarn as described in [Stylesheet / Web dev](#stylesheet--web-dev). + +Finally, to create a snapshot build, do: ```bash -goreleaser release --rm-dist --snapshot +goreleaser --rm-dist --snapshot ``` -If all goes according to plan, you should now have a bunch of multiple-architecture binaries and tars inside the `./dist` folder, and a snapshot Docker image should be built (check your terminal output for version). +If all goes according to plan, you should now have a bunch of multiple-architecture binaries and tars inside the `./dist` folder, and snapshot Docker images should be built (check your terminal output for version). ### Manually -If you prefer a simple approach with fewer dependencies, you can also just build a Docker container manually in the following way: +If you prefer a simple approach to building a Docker container, with fewer dependencies, you can also just build in the following way: ```bash -./scripts/build.sh && docker build -t superseriousbusiness/gotosocial:latest . +./scripts/build.sh && docker buildx build -t superseriousbusiness/gotosocial:latest . ``` +The above command first builds the `gotosocial` binary, then invokes Docker buildx to build the container image. + +You don't need to install go-swagger, Node, or Yarn to build Docker images this way. + ## Financial Compensation Right now there's no structure in place for financial compensation for pull requests and code. This is simply because there's no money being made on the project apart from the very small weekly Liberapay donations. diff --git a/Dockerfile b/Dockerfile index a4beea7ec..5da7c246f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,26 +1,31 @@ # syntax=docker/dockerfile:1.3 +# stage 1: generate up-to-date swagger.yaml to put in the final container +FROM --platform=${BUILDPLATFORM} quay.io/goswagger/swagger:v0.29.0 AS swagger -# bundle the admin webapp -FROM --platform=${BUILDPLATFORM} node:17.6.0-alpine3.15 AS admin_builder -RUN apk update && apk upgrade --no-cache -RUN apk add git +COPY go.mod /go/src/github.com/superseriousbusiness/gotosocial/go.mod +COPY go.sum /go/src/github.com/superseriousbusiness/gotosocial/go.sum +COPY cmd /go/src/github.com/superseriousbusiness/gotosocial/cmd +COPY internal /go/src/github.com/superseriousbusiness/gotosocial/internal +WORKDIR /go/src/github.com/superseriousbusiness/gotosocial +RUN swagger generate spec -o /go/src/github.com/superseriousbusiness/gotosocial/swagger.yaml --scan-models -RUN git clone https://github.com/superseriousbusiness/gotosocial-admin -WORKDIR /gotosocial-admin +# stage 2: generate the web/assets/dist bundles +FROM --platform=${BUILDPLATFORM} node:16.15.1-alpine3.15 AS bundler -RUN npm install -RUN node index.js +COPY web web +RUN yarn install --cwd web/source && \ + BUDO_BUILD=1 node web/source && \ + rm -r web/source -FROM --platform=${TARGETPLATFORM} alpine:3.15.0 AS executor +# stage 3: build the executor container +FROM --platform=${TARGETPLATFORM} alpine:3.15.4 as executor -# copy over the binary from the first stage +# copy the dist binary created by goreleaser or build.sh COPY --chown=1000:1000 gotosocial /gotosocial/gotosocial -# copy over the web directory with templates etc -COPY --chown=1000:1000 web /gotosocial/web - -# copy over the admin directory -COPY --chown=1000:1000 --from=admin_builder /gotosocial-admin/public /gotosocial/web/assets/admin +# copy over the web directories with templates, assets etc +COPY --chown=1000:1000 --from=bundler web /gotosocial/web +COPY --chown=1000:1000 --from=swagger /go/src/github.com/superseriousbusiness/gotosocial/swagger.yaml web/assets/swagger.yaml WORKDIR "/gotosocial" ENTRYPOINT [ "/gotosocial/gotosocial", "server", "start" ] diff --git a/docs/admin/admin_panel.md b/docs/admin/admin_panel.md index da801f0d9..7443b5253 100644 --- a/docs/admin/admin_panel.md +++ b/docs/admin/admin_panel.md @@ -1,6 +1,6 @@ # Admin Control Panel -[gotosocial-admin](https://github.com/superseriousbusiness/gotosocial-admin) is a simple webclient that uses the [admin api routes](https://docs.gotosocial.org/en/latest/api/swagger/#operations-tag-admin) to manage your instance. It uses the same OAUTH mechanism as normal clients (with scope: admin), and as such can be hosted anywhere, separately from your instance, or run locally. A public installation is available here: [https://gts.superseriousbusiness.org/admin](https://gts.superseriousbusiness.org/admin). +The GoToSocial admin panel is a simple webclient that uses the [admin api routes](https://docs.gotosocial.org/en/latest/api/swagger/#operations-tag-admin) to manage your instance. It uses the same OAUTH mechanism as normal clients (with scope: admin), and as such can be hosted anywhere, separately from your instance, or run locally. A public installation is available here: [https://gts.superseriousbusiness.org/admin](https://gts.superseriousbusiness.org/admin). ## Using the panel To use the Admin API your account has to be promoted as such: @@ -16,11 +16,11 @@ any other client. You can change the instance's settings like the title and descriptions, and add/remove/change domain blocks including a bulk import/export. -## Installing the panel +## Building the panel Build requirements: some version of [Node.js](https://nodejs.org) and yarn. ``` -git clone https://github.com/superseriousbusiness/gotosocial-admin.git && cd gotosocial-admin -yarn install -node index.js +yarn install --cwd web/source +BUDO_BUILD=1 node web/source ``` -This will compile a static bundle in `public/`, which can be copied to any webhost, or put into your GoToSocial installation in the `web/admin` directory. \ No newline at end of file + +See also: [Contributing.md Stylesheet / Web dev](https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#stylesheet--web-dev) \ No newline at end of file diff --git a/go.sum b/go.sum index 4e1d80880..1539eda55 100644 --- a/go.sum +++ b/go.sum @@ -663,8 +663,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220524220425-1d687d428aca h1:xTaFYiPROfpPhqrfTIDXj0ri1SpfueYT951s4bAuDO8= golang.org/x/net v0.0.0-20220524220425-1d687d428aca/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= diff --git a/internal/web/base.go b/internal/web/base.go index a8d99619c..8e7e539f6 100644 --- a/internal/web/base.go +++ b/internal/web/base.go @@ -27,7 +27,6 @@ "strings" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" @@ -104,14 +103,10 @@ func New(processor processing.Processor) (api.ClientModule, error) { } func (m *Module) baseHandler(c *gin.Context) { - l := logrus.WithField("func", "BaseGETHandler") - l.Trace("serving index html") - host := config.GetHost() instance, err := m.processor.InstanceGet(c.Request.Context(), host) if err != nil { - l.Debugf("error getting instance from processor: %s", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) return } @@ -120,16 +115,66 @@ func (m *Module) baseHandler(c *gin.Context) { }) } +// TODO: abstract the {admin, user}panel handlers in some way +func (m *Module) AdminPanelHandler(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) + return + } + + c.HTML(http.StatusOK, "frontend.tmpl", gin.H{ + "instance": instance, + "stylesheets": []string{ + "/assets/Fork-Awesome/css/fork-awesome.min.css", + "/assets/dist/panels-admin-style.css", + }, + "javascript": []string{ + "/assets/dist/bundle.js", + "/assets/dist/admin-panel.js", + }, + }) +} + +func (m *Module) UserPanelHandler(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) + return + } + + c.HTML(http.StatusOK, "frontend.tmpl", gin.H{ + "instance": instance, + "stylesheets": []string{ + "/assets/Fork-Awesome/css/fork-awesome.min.css", + "/assets/dist/_colors.css", + "/assets/dist/base.css", + "/assets/dist/panels-user-style.css", + }, + "javascript": []string{ + "/assets/dist/bundle.js", + "/assets/dist/user-panel.js", + }, + }) +} + // Route satisfies the RESTAPIModule interface func (m *Module) Route(s router.Router) error { // serve static files from assets dir at /assets s.AttachStaticFS("/assets", fileSystem{http.Dir(m.assetsPath)}) - // serve admin panel from within assets dir at /admin/ - // and redirect /admin to /admin/ - s.AttachStaticFS("/admin/", fileSystem{http.Dir(m.adminPath)}) - s.AttachHandler(http.MethodGet, "/admin", func(c *gin.Context) { - c.Redirect(http.StatusMovedPermanently, "/admin/") + s.AttachHandler(http.MethodGet, "/admin", m.AdminPanelHandler) + // redirect /admin/ to /admin + s.AttachHandler(http.MethodGet, "/admin/", func(c *gin.Context) { + c.Redirect(http.StatusMovedPermanently, "/admin") + }) + + s.AttachHandler(http.MethodGet, "/user", m.UserPanelHandler) + // redirect /settings/ to /settings + s.AttachHandler(http.MethodGet, "/user/", func(c *gin.Context) { + c.Redirect(http.StatusMovedPermanently, "/user") }) // serve front-page diff --git a/internal/web/profile.go b/internal/web/profile.go index 051d55d28..ce3fe645b 100644 --- a/internal/web/profile.go +++ b/internal/web/profile.go @@ -108,8 +108,11 @@ func (m *Module) profileGETHandler(c *gin.Context) { "statuses": statusResp.Items, "stylesheets": []string{ "/assets/Fork-Awesome/css/fork-awesome.min.css", - "/assets/status.css", - "/assets/profile.css", + "/assets/dist/status.css", + "/assets/dist/profile.css", + }, + "javascript": []string{ + "/assets/dist/frontend.js", }, }) } diff --git a/internal/web/thread.go b/internal/web/thread.go index 5e953ee06..3db4952c7 100644 --- a/internal/web/thread.go +++ b/internal/web/thread.go @@ -105,10 +105,16 @@ func (m *Module) threadGETHandler(c *gin.Context) { } c.HTML(http.StatusOK, "thread.tmpl", gin.H{ - "instance": instance, - "status": status, - "context": context, - "stylesheets": []string{"/assets/Fork-Awesome/css/fork-awesome.min.css", "/assets/status.css"}, + "instance": instance, + "status": status, + "context": context, + "stylesheets": []string{ + "/assets/Fork-Awesome/css/fork-awesome.min.css", + "/assets/dist/status.css", + }, + "javascript": []string{ + "/assets/dist/frontend.js", + }, }) } diff --git a/scripts/bundle.sh b/scripts/bundle.sh new file mode 100755 index 000000000..de218949f --- /dev/null +++ b/scripts/bundle.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +# this script is really just here because GoReleaser doesn't let +# you set env vars in your 'before' commands in the free version + +set -eu + +BUDO_BUILD=1 node web/source diff --git a/web/assets/base.css b/web/assets/base.css deleted file mode 100644 index ba593b474..000000000 --- a/web/assets/base.css +++ /dev/null @@ -1,230 +0,0 @@ -html, body { - padding: 0; - margin: 0; - background: rgb(70, 79, 88); - color: #fafaff; - font-family: sans-serif; -} - -body { - display: grid; - - grid-template-rows: auto 1fr auto; - min-height: 100vh; - line-height: 1.5em; -} - -main { - background: #525c66; - display: grid; - padding-top: 2rem; - padding-bottom: 2rem; - grid-template-columns: 1fr 50% 1fr; - grid-template-columns: auto min(92%, 90ch) auto; -} - -main .left { - grid-column: 1; - } - -main .right { - grid-column: 3; - } - -main.lightgray { - background: #525c66; - } - -main > * { - align-self: start; - grid-column: 2; - } - -header { - background: rgb(70, 79, 88); - padding: 2rem 0; - padding-bottom: 0; - display: flex; - flex-wrap: wrap; -} - -header img { - height: 4rem; - padding-left: 2rem; - padding-bottom: 2rem; - } - -header div { - height: 100%; - margin: 0 2rem; - margin-top: -2rem; - flex-grow: 1; - align-self: center; - display: flex; - } - -header div h1 { - align-self: center; - color: #fafaff; - } - -h1 { - /* color: $acc1; */ - margin: 0; - line-height: 2.4rem; -} - -a { - color: #de8957; -} - -.button, button { - border-radius: 0.2rem; - background: #de8957; - color: #fafaff; - text-decoration: none; - font-size: 1.2rem; - font-weight: bold; - padding: 0.5rem; - border: none; - cursor: pointer; -} - -.button:hover, button:hover { - background: #c76d33; - } - -.count { - background: rgb(70, 79, 88); - border-radius: 0.3rem; - padding: 0.2rem; -} - -.nounderline { - text-decoration: none; -} - -.accent { - color: #de8957; -} - -.logo { - justify-self: center; -} - -.logo img { - height: 30vh; - } - -section.apps { - align-self: start; -} - -section.apps .applist { - display: grid; - grid-template-columns: 1fr 1fr; - grid-gap: 0.5rem; - align-content: start; - } - -section.apps .applist .entry { - display: grid; - grid-template-columns: 30% 1fr; - gap: 0.5rem; - padding: 0.5rem; - background: rgb(70, 79, 88); - border-radius: 0.5rem; - } - -section.apps .applist .entry .logo { - align-self: center; - width: 100%; - object-fit: contain; - flex: 1 1 auto; - } - -section.apps .applist .entry .logo.redraw { - fill: #fafaff; - stroke: #fafaff; - } - -section.apps .applist .entry div { - padding: 1rem 0; - } - -section.apps .applist .entry div h3 { - margin-top: 0; - } - -section.login form { - display: inline-grid; - grid-template-columns: auto 100%; - grid-gap: 0.7rem; - } - -section.login form button { - place-self: center; - grid-column: 2; - } - -section.error { - display: flex; - flex-direction: row; - align-items: center; -} - -section.error span { - font-size: 2em; - } - -section.error pre { - border: 1px solid #ff000080; - margin-left: 1em; - padding: 0 0.7em; - border-radius: 0.5em; - background-color: #ff000010; - font-size: 1.3em; - white-space: pre-wrap; - } - -input, select, textarea { - border: 1px solid #fafaff; - color: #fafaff; - background: #525c66; - width: 100%; -} - -footer { - align-self: end; - - padding: 2rem; - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr; -} - -footer a { - font-weight: bold; - } - -@media screen and (orientation: portrait) { - main { - grid-template-columns: 1fr 92% 1fr; - } - - header { - text-align: center; - } - - footer { - margin-top: 2rem; - grid-template-columns: 1fr; - } - - footer div { - margin: 0.3rem 0; - } - - section.apps .applist { - grid-template-columns: 1fr; - } -} diff --git a/web/assets/colors.css b/web/assets/colors.css deleted file mode 100644 index 8b1378917..000000000 --- a/web/assets/colors.css +++ /dev/null @@ -1 +0,0 @@ - diff --git a/web/assets/profile.css b/web/assets/profile.css deleted file mode 100644 index 978468f72..000000000 --- a/web/assets/profile.css +++ /dev/null @@ -1,110 +0,0 @@ -main { - background: transparent; -} - -.headerimage img { - width: 100%; - height: 15em; - object-fit: cover; - border-radius: 10px; - } - -.profile { - position: relative; - background: rgb(75, 84, 93); - padding: 2rem; - display: flex; - flex-wrap: wrap; - justify-content: space-around; - gap: 0.5rem; - margin-bottom: 0.2rem; -} - -.profile .basic { - display: flex; - flex-direction: column; - flex: 1 1 25em; - gap: 0.5rem; - } - -.profile .basic a { - position: relative; - z-index: 1; - color: inherit; - text-decoration: none; - } - -.profile .basic .avatar-container { - position: relative; - width: 100%; - max-width: 25em; - } - -.profile .basic .avatar-container .avatar { - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - } - -.profile .basic .avatar-container .avatar img { - object-fit: cover; - border-radius: 10px; - width: 100%; - height: 100%; - } - -.profile .basic .avatar-container:before { - content: ""; - float: left; - padding-top: 100%; - } - -.profile .basic .displayname { - font-weight: bold; - font-size: 1.6rem; - align-self: start; - } - -.profile .detailed { - display: flex; - flex-direction: column; - flex: 1 1 25em; - } - -.profile .detailed h2 { - margin-top: 0; - } - -.profile .detailed .bio { - margin: 0; - } - -.profile .detailed .bio a { - color: #de8957; - text-decoration: underline; - } - -.accountstats { - position: relative; - background: rgb(75, 84, 93); - padding: 0.5rem; - display: flex; - flex-wrap: wrap; - justify-content: space-evenly; - gap: 0.5rem; - margin-bottom: 0.2rem; -} - -.accountstats .entry { - background: rgb(89, 99, 110); - padding: 0.5rem; - flex-grow: 1; - text-align: center; - } - -footer + div { - /* something weird from the devstack.. */ - display: none; -} diff --git a/web/assets/status.css b/web/assets/status.css deleted file mode 100644 index 9dd788a9e..000000000 --- a/web/assets/status.css +++ /dev/null @@ -1,230 +0,0 @@ -main { - background: transparent; - grid-auto-rows: auto; -} - -.thread { - display: flex; - flex-direction: column; -} - -.toot { - position: relative; - background: rgb(75, 84, 93); - padding: 2rem; - /* padding-bottom: 0; */ - display: grid; - grid-template-columns: 3.2rem auto 1fr; - column-gap: 0.5rem; - margin-bottom: 0.2rem; -} - -.toot a { - position: relative; - z-index: 1; - color: inherit; - text-decoration: none; - } - -.toot .avatar { - grid-row: span 2; - } - -.toot .avatar img { - height: 3.2rem; - width: 3.2rem; - object-fit: cover; - } - -.toot .displayname { - font-weight: bold; - font-size: 1.2rem; - align-self: start; - } - -.toot .username { - color: #b0b0b5; - justify-self: start; - } - -.toot input.spoiler:checked ~ .content { - display: none; - } - -.toot .spoiler label { - background: #de8957; - border-radius: 0.3rem; - padding: 0.3rem; - margin-left: 0.4rem; - position: relative; - z-index: 2; - cursor: pointer; - } - -.toot .text { - margin: 0; - grid-column: span 2; - } - -.toot .text a { - color: #de8957; - text-decoration: underline; - } - -.toot .media { - margin-top: 0.6rem; - border-radius: 0.2rem; - grid-column: span 3; - display: grid; - grid-template-columns: 50% 50%; - grid-auto-rows: 10rem; - overflow: hidden; - gap: 0.3rem; - } - -.toot .media a { - position: relative; - } - -.toot .media .no-image-desc { - display: flex; - position: absolute; - bottom: 0.1rem; - right: 0.4rem; - color: white; - background: #5897df; - padding: 0.2rem 0.4rem; - border-radius: 100%; - z-index: 3; - } - -.toot .media .no-image-desc i.fa { - display: block; - line-height: 1.3rem; - } - -.toot .media .no-image-desc span { - margin-left: 0.3rem; - display: none; - } - -.toot .media .no-image-desc:hover span { - display: block; - } - -.toot .media .no-image-desc:hover { - border-radius: 0.2rem; -} - -.toot .media img { - width: 100%; - height: 100%; - object-fit: cover; - } - -.toot .media.single a { - grid-column: span 2; - } - -.toot .media.odd a:first-child, .toot .media.double a { - grid-row: span 2; - } - -.toot .info { - display: none; - } - -.toot .info div { - position: relative; - padding-right: 1.3rem; - } - -.toot .info .stats { - display: flex; - } - -.toot .info { - - color: #b0b0b5; - grid-column: span 3; - margin-top: 0.5rem; - flex-wrap: wrap; -} - -.toot .info div.stats::after { - display: none; - } - -.toot .info div::after { - display: block; - background: #b0b0b5; - height: 0.25rem; - width: 0.25rem; - content: ""; - position: absolute; - top: calc((1.5rem - 0.25rem) / 2); - right: 0.55rem; - border-radius: 1rem; - } - -.toot .info div:last-child::after { - display: none; - } - -.toot .info div:last-child { - margin-right: 0; -} - -.toot .toot-link { - top: 0; - right: 0; - bottom: 0; - left: 0; - overflow: hidden; - text-indent: 100%; - white-space: nowrap; - - position: absolute; - z-index: 0; - } - -.toot {} - -.toot:first-child { - /* top left, top right */ - border-radius: 0.3rem 0.3rem 0 0; - } - -.toot:last-child { - /* bottom left, bottom right */ - border-radius: 0 0 0.3rem 0.3rem; - padding-bottom: 1.5rem; - } - -.toot.expanded { - background: #525c66; - padding-bottom: 1.5rem; - } - -.toot.expanded .displayname { - grid-column: span 2; - } - -.toot.expanded .text { - grid-column: span 3; - grid-row: span 1; - margin-top: 0.3rem; - } - -.toot.expanded .info { - display: flex; - } - -.toot.expanded .media { - grid-auto-rows: 1fr; - max-height: 120rem; - } - -footer + div { /* something weird from the devstack.. */ - display: none; -} \ No newline at end of file diff --git a/web/gotosocial-styling/.eslintrc.js b/web/gotosocial-styling/.eslintrc.js deleted file mode 100644 index 376c92936..000000000 --- a/web/gotosocial-styling/.eslintrc.js +++ /dev/null @@ -1,5 +0,0 @@ -"use strict"; - -module.exports = { - "extends": "@f0x52/eslint-config" -}; diff --git a/web/gotosocial-styling/.gitignore b/web/gotosocial-styling/.gitignore deleted file mode 100644 index ee48c081f..000000000 --- a/web/gotosocial-styling/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -yarn-error.log -build/* diff --git a/web/gotosocial-styling/LICENSE b/web/gotosocial-styling/LICENSE deleted file mode 100644 index be3f7b28e..000000000 --- a/web/gotosocial-styling/LICENSE +++ /dev/null @@ -1,661 +0,0 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - 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 . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. diff --git a/web/gotosocial-styling/README.md b/web/gotosocial-styling/README.md deleted file mode 100644 index 71314e838..000000000 --- a/web/gotosocial-styling/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# GoToSocial Styling - -Common package for the PostCSS styling of GoToSocial (related) pages. - -## Bundle -Source in `src/style.css` is bundled by running `node index.js`. Output appears in `build/bundle.css`, and can be required from other packages with `require("gotosocial-styling/build/bundle.css")`. - -## Development -You can run `NODE_ENV=development node index.js` to start a livereloading setup that automatically re-bundles on file changes in `src/`. - -## License, donations -[AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.html). If you want to support my work, you can: -Donate using Liberapay - - -## Changelog -### v0.0.1 (August 29th, 2021) -initial release diff --git a/web/gotosocial-styling/colors.css b/web/gotosocial-styling/colors.css deleted file mode 100644 index 8dfcd4a09..000000000 --- a/web/gotosocial-styling/colors.css +++ /dev/null @@ -1,9 +0,0 @@ -$bg: #525c66; -$fg: #fafaff; -$fg_dark: #b0b0b5; - -$bg_accent: color($bg lightness(-5%)); - -$acc1: #de8957; // sloth light orange -$acc2: #c76d33; // sloth dark orange -$blue: #5897df; diff --git a/web/gotosocial-styling/index.js b/web/gotosocial-styling/index.js deleted file mode 100644 index 43384a108..000000000 --- a/web/gotosocial-styling/index.js +++ /dev/null @@ -1,85 +0,0 @@ -"use strict"; - -const Promise = require("bluebird"); -const fs = require("fs").promises; -const postcss = require('postcss'); -const {parse} = require("postcss-scss"); -const argv = require('minimist')(process.argv.slice(2)); - -/* - Bundle all postCSS files under the `templates/` directory separately, each prepended with the (variable) contents of ./colors.css - Outputs in plain CSS are in `build/`, split by template -*/ - -const postcssPlugins = ["postcss-strip-inline-comments", "postcss-nested", "postcss-simple-vars", "postcss-color-function"].map((plugin) => require(plugin)()); - -function getTemplates() { - return fs.readdir(`${__dirname}/templates`).then((templates) => { - return templates.map((a) => { - return [a, `${__dirname}/templates/${a}`]; - }); - }); -} - -getTemplates(); - -function bundle([template, path]) { - return Promise.try(() => { - return Promise.all([ - fs.readFile(`${__dirname}/colors.css`, "utf-8"), - fs.readFile(path, "utf-8") - ]); - }).then(([colors, style]) => { - return parse(colors + "\n" + style); - }).then((ast) => { - return postcss(postcssPlugins).process(ast, { - from: template, - to: template - }); - }).then((bundle) => { - return fs.writeFile(`${buildDir}/${template}`, bundle.css); - }).then(() => { - console.log(`Finished writing CSS to ${buildDir}/${template}`); - }); -} - -let buildDir - -// try reading from arguments first -if (argv["build-dir"] != undefined) { - buildDir = argv["build-dir"] -} - -// then try reading from environment variable -if (buildDir == undefined) { - buildDir = process.env.BUILD_DIR; -} - -// then take default -if (buildDir == undefined) { - buildDir = `${__dirname}/build`; -} - -console.log("bundling to", buildDir); - -function bundleAll() { - return getTemplates().then((templates) => { - return Promise.map(templates, bundle); - }); -} - -if (process.env.NODE_ENV != "development") { - bundleAll(); -} else { - const chokidar = require("chokidar"); - console.log("Watching for changes"); - chokidar.watch(`${__dirname}/templates`).on("all", (_, path) => { - if (path.endsWith(".css")) { - bundle([path.split("/").slice(-1)[0], path]); - } - }); - chokidar.watch(`${__dirname}/colors.css`).on("all", () => { - console.log("colors.css updated, rebuilding all templates"); - bundleAll(); - }); -} \ No newline at end of file diff --git a/web/gotosocial-styling/package.json b/web/gotosocial-styling/package.json deleted file mode 100644 index 57594b091..000000000 --- a/web/gotosocial-styling/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "gotosocial-styling", - "version": "0.0.1", - "description": "PostCSS source for the https://github.com/superseriousbusiness/gotosocial common styling", - "repository": "https://github.com/superseriousbusiness/gotosocial-styling", - "author": "f0x", - "license": "AGPL-3.0", - "dependencies": { - "bluebird": "^3.7.2", - "minimist": "^1.2.5", - "postcss": "^8.3.5", - "postcss-color-function": "^4.1.0", - "postcss-nested": "^4.2.1", - "postcss-scss": "^4.0.0", - "postcss-simple-vars": "^5.0.2", - "postcss-strip-inline-comments": "^0.1.5" - }, - "devDependencies": { - "chokidar": "^3.5.2" - } -} diff --git a/web/gotosocial-styling/yarn.lock b/web/gotosocial-styling/yarn.lock deleted file mode 100644 index 6b7fe6e64..000000000 --- a/web/gotosocial-styling/yarn.lock +++ /dev/null @@ -1,409 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -anymatch@~3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" - integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -balanced-match@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.1.0.tgz#b504bd05869b39259dd0c5efc35d843176dccc4a" - integrity sha1-tQS9BYabOSWd0MXvw12EMXbczEo= - -binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== - -bluebird@^3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" - integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== - -braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chalk@^2.4.1, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chokidar@^3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" - integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -clone@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" - integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= - -color-convert@^1.3.0, color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -color-name@^1.0.0: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -color-string@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991" - integrity sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE= - dependencies: - color-name "^1.0.0" - -color@^0.11.0: - version "0.11.4" - resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764" - integrity sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q= - dependencies: - clone "^1.0.2" - color-convert "^1.3.0" - color-string "^0.3.0" - -colorette@^1.2.2: - version "1.4.0" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" - integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== - -css-color-function@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/css-color-function/-/css-color-function-1.3.3.tgz#8ed24c2c0205073339fafa004bc8c141fccb282e" - integrity sha1-jtJMLAIFBzM5+voAS8jBQfzLKC4= - dependencies: - balanced-match "0.1.0" - color "^0.11.0" - debug "^3.1.0" - rgb "~0.1.0" - -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - -debug@^3.1.0: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - -glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - -has-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" - integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo= - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== - dependencies: - is-extglob "^2.1.1" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -js-base64@^2.1.9: - version "2.6.4" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4" - integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ== - -minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - -ms@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -nanoid@^3.1.23: - version "3.2.0" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" - integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -picomatch@^2.0.4, picomatch@^2.2.1: - version "2.3.0" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" - integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== - -postcss-color-function@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/postcss-color-function/-/postcss-color-function-4.1.0.tgz#b6f9355e07b12fcc5c34dab957834769b03d8f57" - integrity sha512-2/fuv6mP5Lt03XbRpVfMdGC8lRP1sykme+H1bR4ARyOmSMB8LPSjcL6EAI1iX6dqUF+jNEvKIVVXhan1w/oFDQ== - dependencies: - css-color-function "~1.3.3" - postcss "^6.0.23" - postcss-message-helpers "^2.0.0" - postcss-value-parser "^3.3.1" - -postcss-message-helpers@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz#a4f2f4fab6e4fe002f0aed000478cdf52f9ba60e" - integrity sha1-pPL0+rbk/gAvCu0ABHjN9S+bpg4= - -postcss-nested@^4.2.1: - version "4.2.3" - resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-4.2.3.tgz#c6f255b0a720549776d220d00c4b70cd244136f6" - integrity sha512-rOv0W1HquRCamWy2kFl3QazJMMe1ku6rCFoAAH+9AcxdbpDeBr6k968MLWuLjvjMcGEip01ak09hKOEgpK9hvw== - dependencies: - postcss "^7.0.32" - postcss-selector-parser "^6.0.2" - -postcss-scss@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-4.0.0.tgz#6cfe09040f01e3345c9c226894c5913ac5fa9381" - integrity sha512-xakgIr5ukOEyXFcsnADKjQtrk8nQyqn5VIEAA+PmPP4kBOpknmjpJMxBNqCR1/x20AS0aSfZkWsSdbMx2Ozm5A== - -postcss-selector-parser@^6.0.2: - version "6.0.6" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz#2c5bba8174ac2f6981ab631a42ab0ee54af332ea" - integrity sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-simple-vars@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-simple-vars/-/postcss-simple-vars-5.0.2.tgz#e2f81b3d0847ddd4169816b6d141b91d51e6e22e" - integrity sha512-xWIufxBoINJv6JiLb7jl5oElgp+6puJwvT5zZHliUSydoLz4DADRB3NDDsYgfKVwojn4TDLiseoC65MuS8oGGg== - dependencies: - postcss "^7.0.14" - -postcss-strip-inline-comments@^0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/postcss-strip-inline-comments/-/postcss-strip-inline-comments-0.1.5.tgz#7ff6bcdc14e633ed4cdfa020bae3eddad4f84b90" - integrity sha1-f/a83BTmM+1M36AguuPt2tT4S5A= - dependencies: - postcss "^5.0.18" - -postcss-value-parser@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" - integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== - -postcss@^5.0.18: - version "5.2.18" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5" - integrity sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg== - dependencies: - chalk "^1.1.3" - js-base64 "^2.1.9" - source-map "^0.5.6" - supports-color "^3.2.3" - -postcss@^6.0.23: - version "6.0.23" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" - integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag== - dependencies: - chalk "^2.4.1" - source-map "^0.6.1" - supports-color "^5.4.0" - -postcss@^7.0.14, postcss@^7.0.32: - version "7.0.36" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.36.tgz#056f8cffa939662a8f5905950c07d5285644dfcb" - integrity sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw== - dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.1.0" - -postcss@^8.3.5: - version "8.3.6" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.6.tgz#2730dd76a97969f37f53b9a6096197be311cc4ea" - integrity sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A== - dependencies: - colorette "^1.2.2" - nanoid "^3.1.23" - source-map-js "^0.6.2" - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -rgb@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/rgb/-/rgb-0.1.0.tgz#be27b291e8feffeac1bd99729721bfa40fc037b5" - integrity sha1-vieykej+/+rBvZlylyG/pA/AN7U= - -source-map-js@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e" - integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug== - -source-map@^0.5.6: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -source-map@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -strip-ansi@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - -supports-color@^3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" - integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY= - dependencies: - has-flag "^1.0.0" - -supports-color@^5.3.0, supports-color@^5.4.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== - dependencies: - has-flag "^3.0.0" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -util-deprecate@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= diff --git a/web/source/.eslintrc.js b/web/source/.eslintrc.js new file mode 100644 index 000000000..36fbbacd6 --- /dev/null +++ b/web/source/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + "extends": ["@f0x52/eslint-config-react"] +}; diff --git a/web/source/.gitignore b/web/source/.gitignore new file mode 100644 index 000000000..88f2cb7be --- /dev/null +++ b/web/source/.gitignore @@ -0,0 +1,2 @@ +node_modules +public/bundle.* diff --git a/web/source/css/_colors.css b/web/source/css/_colors.css new file mode 100644 index 000000000..ca18e22ed --- /dev/null +++ b/web/source/css/_colors.css @@ -0,0 +1,35 @@ +/* + 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 . +*/ + +/* + This stylesheets defines (color) variables to be used by other stylesheets on the page + postcss-custom-prop-vars will transpile these to css --variables +*/ + +$bg: #525c66; +$fg: #fafaff; +$fg_dark: #b0b0b5; + +$bg_darker3: color-mod($bg lightness(-3%)); +$bg_darker5: color-mod($bg lightness(-5%)); + +$bg_lighter3: color-mod($bg lightness(+3%)); + +$acc1: #de8957; // sloth light orange +$acc2: #c76d33; // sloth dark orange +$blue: #5897df; diff --git a/web/gotosocial-styling/templates/base.css b/web/source/css/base.css similarity index 75% rename from web/gotosocial-styling/templates/base.css rename to web/source/css/base.css index 0e1ea23eb..e8b8818df 100644 --- a/web/gotosocial-styling/templates/base.css +++ b/web/source/css/base.css @@ -1,17 +1,42 @@ +/* + 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 . +*/ + html, body { padding: 0; margin: 0; - background: $bg_accent; + background: $bg_darker5; color: $fg; font-family: sans-serif; } body { + line-height: 1.5em; + min-height: 100vh; + position: relative; +} + +.page { + position: absolute; display: grid; + min-height: 100%; + min-width: 100%; grid-template-rows: auto 1fr auto; - min-height: 100vh; - line-height: 1.5em; } main { @@ -41,7 +66,7 @@ main { } header { - background: $bg_accent; + background: $bg_darker5; padding: 2rem 0; padding-bottom: 0; display: flex; @@ -95,7 +120,7 @@ a { } .count { - background: $bg_accent; + background: $bg_darker5; border-radius: 0.3rem; padding: 0.2rem; } @@ -129,7 +154,7 @@ section.apps { grid-template-columns: 30% 1fr; gap: 0.5rem; padding: 0.5rem; - background: $bg_accent; + background: $bg_darker5; border-radius: 0.5rem; .logo { diff --git a/web/gotosocial-styling/templates/profile.css b/web/source/css/profile.css similarity index 66% rename from web/gotosocial-styling/templates/profile.css rename to web/source/css/profile.css index 472c6b730..10b4db12a 100644 --- a/web/gotosocial-styling/templates/profile.css +++ b/web/source/css/profile.css @@ -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 . +*/ + main { background: transparent; } @@ -13,7 +31,7 @@ main { .profile { position: relative; - background: color($bg lightness(-3%)); + background: $bg_darker3; padding: 2rem; display: flex; flex-wrap: wrap; @@ -91,7 +109,7 @@ main { .accountstats { position: relative; - background: color($bg lightness(-3%)); + background: $bg_darker3; padding: 0.5rem; display: flex; flex-wrap: wrap; @@ -100,7 +118,7 @@ main { margin-bottom: 0.2rem; .entry { - background: color($bg lightness(+3%)); + background: $bg_lighter3; padding: 0.5rem; flex-grow: 1; text-align: center; diff --git a/web/gotosocial-styling/templates/status.css b/web/source/css/status.css similarity index 80% rename from web/gotosocial-styling/templates/status.css rename to web/source/css/status.css index b71a7c9f7..bfedb2c4e 100644 --- a/web/gotosocial-styling/templates/status.css +++ b/web/source/css/status.css @@ -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 . +*/ + main { background: transparent; grid-auto-rows: auto; @@ -10,7 +28,7 @@ main { .toot { position: relative; - background: color($bg lightness(-3%)); + background: $bg_darker3; padding: 2rem; /* padding-bottom: 0; */ display: grid; diff --git a/web/source/dev-server.js b/web/source/dev-server.js new file mode 100644 index 000000000..3802eb88c --- /dev/null +++ b/web/source/dev-server.js @@ -0,0 +1,73 @@ +/* + 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 . +*/ + +"use strict"; + +const express = require("express"); + +const app = express(); + +function html(title, css, js) { + return ` + + + + + + + ${["_colors.css", "base.css", ...css].map((file) => { + return ``; + }).join("\n")} + GoToSocial ${title} Panel + + +
+ Instance Logo +
+

+ GoToSocial ${title} Panel +

+
+
+
+
+
+ ${["bundle.js", ...js].map((file) => { + return ``; + }).join("\n")} + + +`; +} + +app.get("/admin", (req, res) => { + res.send(html("Admin", ["panels-admin-style.css"], ["admin-panel.js"])); +}); + +app.get("/user", (req, res) => { + res.send(html("Settings", ["panels-user-style.css"], ["user-panel.js"])); +}); + +app.use("/assets", express.static("../assets/")); + +if (process.env.NODE_ENV != "development") { + console.log("adding static asset route"); + app.use(express.static("../assets/dist")); +} + +module.exports = app; \ No newline at end of file diff --git a/web/source/frontend/index.js b/web/source/frontend/index.js new file mode 100644 index 000000000..e761202bb --- /dev/null +++ b/web/source/frontend/index.js @@ -0,0 +1,41 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +"use strict"; + + +// WARNING: currently dependencies get deduplicated with factor-bundle, but +// our frontend templates don't load the common bundle.js since it contains React etc +// so we can't use any dependencies that would deduplicate with the other files + +Array.from(document.getElementsByClassName("spoiler-label")).forEach((label) => { + let checkbox = document.getElementById(label.htmlFor); + console.log(label, checkbox); + if (checkbox != undefined) { + function update() { + if(checkbox.checked) { + label.innerHTML = "Show more"; + } else { + label.innerHTML = "Show less"; + } + } + update(); + + label.addEventListener("click", () => {setTimeout(update, 1);}); + } +}); diff --git a/web/source/index.js b/web/source/index.js new file mode 100644 index 000000000..0e5df9f9b --- /dev/null +++ b/web/source/index.js @@ -0,0 +1,108 @@ +/* + 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 . +*/ + +"use strict"; + +/* + Bundle the frontend panels for admin and user settings +*/ + +const path = require('path'); +const budoExpress = require('@f0x52/budo-express'); +const babelify = require('babelify'); +const fs = require("fs"); +const EventEmitter = require('events'); + +function out(name = "") { + return path.join(__dirname, "../assets/dist/", name); +} + +module.exports = {out}; + +const splitCSS = require("./lib/split-css.js"); + +const bundles = { + "./frontend/index.js": "frontend.js", + "./panels/admin/index.js": "admin-panel.js", + "./panels/user/index.js": "user-panel.js", +}; + +const postcssPlugins = [ + "postcss-import", + "postcss-strip-inline-comments", + "postcss-nested", + "autoprefixer", + "postcss-custom-prop-vars", + "postcss-color-mod-function" +].map((plugin) => require(plugin)()); + +const browserifyConfig = { + transform: [ + babelify.configure({ + presets: [ + require.resolve("@babel/preset-env"), + require.resolve("@babel/preset-react") + ] + }), + [require("uglifyify"), { + global: true, + exts: ".js" + }] + ], + plugin: [ + [require("icssify"), { + parser: require('postcss-scss'), + before: postcssPlugins, + mode: 'global' + }], + [require("css-extract"), { out: splitCSS }], + [require("factor-bundle"), { + outputs: Object.values(bundles).map((file) => { + return out(file); + }) + }] + ] +}; + +const entryFiles = Object.keys(bundles); + +fs.readdirSync(path.join(__dirname, "./css")).forEach((file) => { + entryFiles.push(path.join(__dirname, "./css", file)); +}); + +if (!fs.existsSync(out())){ + fs.mkdirSync(out(), { recursive: true }); +} + +const server = budoExpress({ + port: 8081, + host: "localhost", + entryFiles: entryFiles, + basePath: __dirname, + bundlePath: "bundle.js", + staticPath: out(), + expressApp: require("./dev-server.js"), + browserify: browserifyConfig, + livereloadPattern: "**/*.{html,js,svg}" +}); + +if (server instanceof EventEmitter) { + server.on("update", (contents) => { + fs.writeFileSync(out("bundle.js"), contents); + }); +} \ No newline at end of file diff --git a/web/source/lib/oauth.js b/web/source/lib/oauth.js new file mode 100644 index 000000000..673d4ce83 --- /dev/null +++ b/web/source/lib/oauth.js @@ -0,0 +1,220 @@ +/* + 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 . +*/ + +"use strict"; + +const Promise = require("bluebird"); + +function getCurrentUrl() { + return window.location.origin + window.location.pathname; // strips ?query=string and #hash +} + +module.exports = function oauthClient(config, initState) { + /* config: + instance: instance domain (https://testingtesting123.xyz) + client_name: "GoToSocial Admin Panel" + scope: [] + website: + */ + + let state = initState; + if (initState == undefined) { + state = localStorage.getItem("oauth"); + if (state == undefined) { + state = { + config + }; + storeState(); + } else { + state = JSON.parse(state); + } + } + + function storeState() { + localStorage.setItem("oauth", JSON.stringify(state)); + } + + /* register app + /api/v1/apps + */ + function register() { + if (state.client_id != undefined) { + return true; // we already have a registration + } + let url = new URL(config.instance); + url.pathname = "/api/v1/apps"; + + return fetch(url.href, { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + client_name: config.client_name, + redirect_uris: getCurrentUrl(), + scopes: config.scope.join(" "), + website: getCurrentUrl() + }) + }).then((res) => { + if (res.status != 200) { + throw res; + } + return res.json(); + }).then((json) => { + state.client_id = json.client_id; + state.client_secret = json.client_secret; + storeState(); + }); + } + + /* authorize: + /oauth/authorize + ?client_id=CLIENT_ID + &redirect_uri=window.location.href + &response_type=code + &scope=admin + */ + function authorize() { + let url = new URL(config.instance); + url.pathname = "/oauth/authorize"; + url.searchParams.set("client_id", state.client_id); + url.searchParams.set("redirect_uri", getCurrentUrl()); + url.searchParams.set("response_type", "code"); + url.searchParams.set("scope", config.scope.join(" ")); + + window.location.assign(url.href); + } + + function callback() { + if (state.access_token != undefined) { + return; // we're already done :) + } + let params = (new URL(window.location)).searchParams; + + let token = params.get("code"); + if (token != null) { + console.log("got token callback:", token); + } + + return authorizeToken(token) + .catch((e) => { + console.log("Error processing oauth callback:", e); + logout(); // just to be sure + }); + } + + function authorizeToken(token) { + let url = new URL(config.instance); + url.pathname = "/oauth/token"; + return fetch(url.href, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + client_id: state.client_id, + client_secret: state.client_secret, + redirect_uri: getCurrentUrl(), + grant_type: "authorization_code", + code: token + }) + }).then((res) => { + if (res.status != 200) { + throw res; + } + return res.json(); + }).then((json) => { + state.access_token = json.access_token; + storeState(); + window.location = getCurrentUrl(); // clear ?token= + }); + } + + function isAuthorized() { + return (state.access_token != undefined); + } + + function apiRequest(path, method, data, type="json") { + if (!isAuthorized()) { + throw new Error("Not Authenticated"); + } + let url = new URL(config.instance); + let [p, s] = path.split("?"); + url.pathname = p; + if (s != undefined) { + url.search = s; + } + let headers = { + "Authorization": `Bearer ${state.access_token}` + }; + let body = data; + if (type == "json" && body != undefined) { + headers["Content-Type"] = "application/json"; + body = JSON.stringify(data); + } + return fetch(url.href, { + method, + headers, + body + }).then((res) => { + return Promise.all([res.json(), res]); + }).then(([json, res]) => { + if (res.status != 200) { + if (json.error) { + throw new Error(json.error); + } else { + throw new Error(`${res.status}: ${res.statusText}`); + } + } else { + return json; + } + }); + } + + function logout() { + let url = new URL(config.instance); + url.pathname = "/oauth/revoke"; + return fetch(url.href, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + client_id: state.client_id, + client_secret: state.client_secret, + token: state.access_token, + }) + }).then((res) => { + if (res.status != 200) { + // GoToSocial doesn't actually implement this route yet, + // so error is to be expected + return; + } + return res.json(); + }).catch(() => { + // see above + }).then(() => { + localStorage.removeItem("oauth"); + window.location = getCurrentUrl(); + }); + } + + return { + register, authorize, callback, isAuthorized, apiRequest, logout + }; +}; diff --git a/web/source/lib/split-css.js b/web/source/lib/split-css.js new file mode 100644 index 000000000..fb8694095 --- /dev/null +++ b/web/source/lib/split-css.js @@ -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 . +*/ + +"use strict"; + +const fs = require("fs"); +const path = require("path"); + +const {Writable} = require("stream"); +const {out} = require("../index.js"); + +const fromRegex = /\/\* from (.+?) \*\//; +module.exports = function splitCSS() { + let chunks = []; + return new Writable({ + write: function(chunk, encoding, next) { + chunks.push(chunk); + next(); + }, + final: function() { + let stream = chunks.join(""); + let input; + let content = []; + + function write() { + if (content.length != 0) { + if (input == undefined) { + throw new Error("Got CSS content without filename, can't output: ", content); + } else { + console.log("writing to", out(input)); + fs.writeFileSync(out(input), content.join("\n")); + } + content = []; + } + } + + const cssDir = path.join(__dirname, "../css"); + + stream.split("\n").forEach((line) => { + if (line.startsWith("/* from")) { + let found = fromRegex.exec(line); + if (found != null) { + write(); + + let parts = path.parse(found[1]); + if (path.relative(cssDir, path.join(process.cwd(), parts.dir)) == "") { + input = parts.base; + } else { + // prefix filename with path + let relative = path.relative(path.join(__dirname, "../"), path.join(process.cwd(), found[1])); + input = relative.replace(/\//g, "-"); + } + } + } else { + content.push(line); + } + }); + write(); + } + }); +}; diff --git a/web/source/package.json b/web/source/package.json new file mode 100644 index 000000000..9c40dc39b --- /dev/null +++ b/web/source/package.json @@ -0,0 +1,47 @@ +{ + "name": "gotosocial-frontend", + "version": "0.3.4", + "description": "GoToSocial frontend sources", + "main": "index.js", + "author": "f0x", + "license": "AGPL-3.0", + "dependencies": { + "@babel/core": "^7.12.13", + "@babel/preset-env": "^7.12.13", + "@babel/preset-react": "^7.12.13", + "@f0x52/budo-express": "^1.1.0", + "autoprefixer": "^9.8.0", + "babelify": "^10.0.0", + "bluebird": "^3.7.2", + "browserify": "^17.0.0", + "browserlist": "^1.0.1", + "css-extract": "^2.0.0", + "eslint-plugin-react": "^7.24.0", + "express": "^4.18.1", + "factor-bundle": "^2.5.0", + "from2-string": "^1.1.0", + "icssify": "^1.2.1", + "js-file-download": "^0.4.12", + "postcss": "^8.3.5", + "postcss-color-function": "^4.1.0", + "postcss-color-mod-function": "3.0.0", + "postcss-comment": "^2.0.0", + "postcss-custom-prop-vars": "0.0.3", + "postcss-import": "12", + "postcss-mixins": "^6.2.3", + "postcss-nested": "^4.2.1", + "postcss-scss": "^4.0.0", + "postcss-simple-vars": "^5.0.2", + "postcss-strip-inline-comments": "^0.1.5", + "pretty-bytes": "^5.6.0", + "react": "^17.0.1", + "react-dom": "^17.0.1", + "reactify": "^1.1.1", + "uglifyify": "^5.0.2" + }, + "devDependencies": { + "@f0x52/eslint-config-react": "^1.1.0", + "eslint": "^7.30.0", + "eslint-plugin-react-hooks": "^4.2.0" + } +} diff --git a/web/source/panels/admin/README.md b/web/source/panels/admin/README.md new file mode 100644 index 000000000..9a4572270 --- /dev/null +++ b/web/source/panels/admin/README.md @@ -0,0 +1,21 @@ +# GoToSocial Admin Panel + +Standalone web admin panel for [GoToSocial](https://github.com/superseriousbusiness/gotosocial). + +A public hosted instance is also available at https://gts.superseriousbusiness.org/admin/, so you can fill your own instance URL in there. + +## Installation +Build requirements: some version of Node.js with npm, +``` +git clone https://github.com/superseriousbusiness/gotosocial-admin.git && cd gotosocial-admin +npm install +node index.js +``` +All processed build output will now be in `public/`, which you can copy over to a folder in your GoToSocial installation like `web/assets/admin`, or serve elsewhere. +No further configuration is required, authentication happens through normal OAUTH flow. + +## Development +Follow the installation steps, but run `NODE_ENV=development node index.js` to start the livereloading dev server instead. + +## License, donations +[AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.html). If you want to support my work, you can: Donate using Liberapay \ No newline at end of file diff --git a/web/source/panels/admin/auth.js b/web/source/panels/admin/auth.js new file mode 100644 index 000000000..e26ff06b9 --- /dev/null +++ b/web/source/panels/admin/auth.js @@ -0,0 +1,96 @@ +/* + 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 . +*/ + +"use strict"; + +const Promise = require("bluebird"); +const React = require("react"); +const oauthLib = require("../../lib/oauth"); + +module.exports = function Auth({setOauth}) { + const [ instance, setInstance ] = React.useState(""); + + React.useEffect(() => { + let isStillMounted = true; + // check if current domain runs an instance + let thisUrl = new URL(window.location.origin); + thisUrl.pathname = "/api/v1/instance"; + Promise.try(() => { + return fetch(thisUrl.href); + }).then((res) => { + if (res.status == 200) { + return res.json(); + } + }).then((json) => { + if (json && json.uri && isStillMounted) { + setInstance(json.uri); + } + }).catch((e) => { + console.log("error checking instance response:", e); + }); + + return () => { + // cleanup function + isStillMounted = false; + }; + }, []); + + function doAuth() { + return Promise.try(() => { + return new URL(instance); + }).catch(TypeError, () => { + return new URL(`https://${instance}`); + }).then((parsedURL) => { + let url = parsedURL.toString(); + let oauth = oauthLib({ + instance: url, + client_name: "GoToSocial Admin Panel", + scope: ["admin"], + website: window.location.href + }); + setOauth(oauth); + setInstance(url); + return oauth.register().then(() => { + return oauth; + }); + }).then((oauth) => { + return oauth.authorize(); + }).catch((e) => { + console.log("error authenticating:", e); + }); + } + + function updateInstance(e) { + if (e.key == "Enter") { + doAuth(); + } else { + setInstance(e.target.value); + } + } + + return ( +
+

OAUTH Login:

+
e.preventDefault()}> + + + +
+
+ ); +}; \ No newline at end of file diff --git a/web/source/panels/admin/blocks.js b/web/source/panels/admin/blocks.js new file mode 100644 index 000000000..b12eb50a9 --- /dev/null +++ b/web/source/panels/admin/blocks.js @@ -0,0 +1,318 @@ +/* + 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 . +*/ + +"use strict"; + +const Promise = require("bluebird"); +const React = require("react"); +const fileDownload = require("js-file-download"); + +function sortBlocks(blocks) { + return blocks.sort((a, b) => { // alphabetical sort + return a.domain.localeCompare(b.domain); + }); +} + +function deduplicateBlocks(blocks) { + let a = new Map(); + blocks.forEach((block) => { + a.set(block.id, block); + }); + return Array.from(a.values()); +} + +module.exports = function Blocks({oauth}) { + const [blocks, setBlocks] = React.useState([]); + const [info, setInfo] = React.useState("Fetching blocks"); + const [errorMsg, setError] = React.useState(""); + const [checked, setChecked] = React.useState(new Set()); + + React.useEffect(() => { + Promise.try(() => { + return oauth.apiRequest("/api/v1/admin/domain_blocks", undefined, undefined, "GET"); + }).then((json) => { + setInfo(""); + setError(""); + setBlocks(sortBlocks(json)); + }).catch((e) => { + setError(e.message); + setInfo(""); + }); + }, []); + + let blockList = blocks.map((block) => { + function update(e) { + let newChecked = new Set(checked.values()); + if (e.target.checked) { + newChecked.add(block.id); + } else { + newChecked.delete(block.id); + } + setChecked(newChecked); + } + + return ( + +
+
{block.domain}
+
{(new Date(block.created_at)).toLocaleString()}
+
+ ); + }); + + function clearChecked() { + setChecked(new Set()); + } + + function undoChecked() { + let amount = checked.size; + if(confirm(`Are you sure you want to remove ${amount} block(s)?`)) { + setInfo(""); + Promise.map(Array.from(checked.values()), (block) => { + console.log("deleting", block); + return oauth.apiRequest(`/api/v1/admin/domain_blocks/${block}`, "DELETE"); + }).then((res) => { + console.log(res); + setInfo(`Deleted ${amount} blocks: ${res.map((a) => a.domain).join(", ")}`); + }).catch((e) => { + setError(e); + }); + + let newBlocks = blocks.filter((block) => { + if (checked.size > 0 && checked.has(block.id)) { + checked.delete(block.id); + return false; + } else { + return true; + } + }); + setBlocks(newBlocks); + clearChecked(); + } + } + + return ( +
+

Blocks

+
{errorMsg}
+
{info}
+ +

Blocks:

+
+ uncheck all + +
+
+ {blockList} +
+ +
+ ); +}; + +function BulkBlocking({oauth, blocks, setBlocks}) { + const [bulk, setBulk] = React.useState(""); + const [blockMap, setBlockMap] = React.useState(new Map()); + const [output, setOutput] = React.useState(); + + React.useEffect(() => { + let newBlockMap = new Map(); + blocks.forEach((block) => { + newBlockMap.set(block.domain, block); + }); + setBlockMap(newBlockMap); + }, [blocks]); + + const fileRef = React.useRef(); + + function error(e) { + setOutput(
{e}
); + throw e; + } + + function fileUpload() { + let reader = new FileReader(); + reader.addEventListener("load", (e) => { + try { + // TODO: use validatem? + let json = JSON.parse(e.target.result); + json.forEach((block) => { + console.log("block:", block); + }); + } catch(e) { + error(e.message); + } + }); + reader.readAsText(fileRef.current.files[0]); + } + + React.useEffect(() => { + if (fileRef && fileRef.current) { + fileRef.current.addEventListener("change", fileUpload); + } + return function cleanup() { + fileRef.current.removeEventListener("change", fileUpload); + }; + }); + + function textImport() { + Promise.try(() => { + if (bulk[0] == "[") { + // assume it's json + return JSON.parse(bulk); + } else { + return bulk.split("\n").map((val) => { + return { + domain: val.trim() + }; + }); + } + }).then((domains) => { + console.log(domains); + let before = domains.length; + setOutput(`Importing ${before} domain(s)`); + domains = domains.filter(({domain}) => { + return (domain != "" && !blockMap.has(domain)); + }); + setOutput({output}
{`Deduplicated ${before - domains.length}/${before} with existing blocks, adding ${domains.length} block(s)`}
); + if (domains.length > 0) { + let data = new FormData(); + data.append("domains", new Blob([JSON.stringify(domains)], {type: "application/json"}), "import.json"); + return oauth.apiRequest("/api/v1/admin/domain_blocks?import=true", "POST", data, "form"); + } + }).then((json) => { + console.log("bulk import result:", json); + setBlocks(sortBlocks(deduplicateBlocks([...json, ...blocks]))); + }).catch((e) => { + error(e.message); + }); + } + + function textExport() { + setBulk(blocks.reduce((str, val) => { + if (typeof str == "object") { + return str.domain; + } else { + return str + "\n" + val.domain; + } + })); + } + + function jsonExport() { + Promise.try(() => { + return oauth.apiRequest("/api/v1/admin/domain_blocks?export=true", "GET"); + }).then((json) => { + fileDownload(JSON.stringify(json), "block-export.json"); + }).catch((e) => { + error(e); + }); + } + + function textAreaUpdate(e) { + setBulk(e.target.value); + } + + return ( + +

Bulk import/export

+ + +
+ + + + +
+ {output} + +
+ ); +} + +function AddBlock({oauth, blocks, setBlocks}) { + const [domain, setDomain] = React.useState(""); + const [type, setType] = React.useState("suspend"); + const [obfuscated, setObfuscated] = React.useState(false); + const [privateDescription, setPrivateDescription] = React.useState(""); + const [publicDescription, setPublicDescription] = React.useState(""); + + function addBlock() { + console.log(`${type}ing`, domain); + Promise.try(() => { + return oauth.apiRequest("/api/v1/admin/domain_blocks", "POST", { + domain: domain, + obfuscate: obfuscated, + private_comment: privateDescription, + public_comment: publicDescription + }, "json"); + }).then((json) => { + setDomain(""); + setPrivateDescription(""); + setPublicDescription(""); + setBlocks([json, ...blocks]); + }); + } + + function onDomainChange(e) { + setDomain(e.target.value); + } + + function onTypeChange(e) { + setType(e.target.value); + } + + function onKeyDown(e) { + if (e.key == "Enter") { + addBlock(); + } + } + + return ( + +

Add Block:

+
+ + + +
+
+ +
+
+
+ +
+
+ + setObfuscated(e.target.checked)}/> +
+
+
+ ); +} + +// function Blocklist() { +// return ( +//
+//

Blocklists

+//
+// ); +// } \ No newline at end of file diff --git a/web/source/panels/admin/index.js b/web/source/panels/admin/index.js new file mode 100644 index 000000000..05ab8e583 --- /dev/null +++ b/web/source/panels/admin/index.js @@ -0,0 +1,95 @@ +/* + 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 . +*/ + +"use strict"; + +const Promise = require("bluebird"); +const React = require("react"); +const ReactDom = require("react-dom"); + +const oauthLib = require("../../lib/oauth.js"); +const Auth = require("./auth"); +const Settings = require("./settings"); +const Blocks = require("./blocks"); + +require("./style.css"); + +function App() { + const [oauth, setOauth] = React.useState(); + const [hasAuth, setAuth] = React.useState(false); + const [oauthState, setOauthState] = React.useState(localStorage.getItem("oauth")); + + React.useEffect(() => { + let state = localStorage.getItem("oauth"); + if (state != undefined) { + state = JSON.parse(state); + let restoredOauth = oauthLib(state.config, state); + Promise.try(() => { + return restoredOauth.callback(); + }).then(() => { + setAuth(true); + }); + setOauth(restoredOauth); + } + }, []); + + if (!hasAuth && oauth && oauth.isAuthorized()) { + setAuth(true); + } + + if (oauth && oauth.isAuthorized()) { + return ; + } else if (oauthState != undefined) { + return "processing oauth..."; + } else { + return ; + } +} + +function AdminPanel({oauth}) { + /* + Features: (issue #78) + - [ ] Instance information updating + GET /api/v1/instance PATCH /api/v1/instance + - [ ] Domain block creation, viewing, and deletion + GET /api/v1/admin/domain_blocks + POST /api/v1/admin/domain_blocks + GET /api/v1/admin/domain_blocks/DOMAIN_BLOCK_ID, DELETE /api/v1/admin/domain_blocks/DOMAIN_BLOCK_ID + - [ ] Blocklist import/export + GET /api/v1/admin/domain_blocks?export=true + POST json file as form field domains to /api/v1/admin/domain_blocks + */ + + return ( + + + + + + ); +} + +function Logout({oauth}) { + return ( +
+ +
+ ); +} + +ReactDom.render(, document.getElementById("root")); \ No newline at end of file diff --git a/web/source/panels/admin/settings.js b/web/source/panels/admin/settings.js new file mode 100644 index 000000000..2a10951a7 --- /dev/null +++ b/web/source/panels/admin/settings.js @@ -0,0 +1,175 @@ +/* + 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 . +*/ + +"use strict"; + +const Promise = require("bluebird"); +const React = require("react"); + +module.exports = function Settings({oauth}) { + const [info, setInfo] = React.useState({}); + const [errorMsg, setError] = React.useState(""); + const [statusMsg, setStatus] = React.useState("Fetching instance info"); + + React.useEffect(() => { + Promise.try(() => { + return oauth.apiRequest("/api/v1/instance", "GET"); + }).then((json) => { + setInfo(json); + }).catch((e) => { + setError(e.message); + setStatus(""); + }); + }, []); + + function submit() { + setStatus("PATCHing"); + setError(""); + return Promise.try(() => { + let formDataInfo = new FormData(); + Object.entries(info).forEach(([key, val]) => { + if (key == "contact_account") { + key = "contact_username"; + val = val.username; + } + if (key == "email") { + key = "contact_email"; + } + if (typeof val != "object") { + formDataInfo.append(key, val); + } + }); + return oauth.apiRequest("/api/v1/instance", "PATCH", formDataInfo, "form"); + }).then((json) => { + setStatus("Config saved"); + console.log(json); + }).catch((e) => { + setError(e.message); + setStatus(""); + }); + } + + return ( +
+

Instance Information

+
+ {errorMsg} +
+
+ {statusMsg} +
+
e.preventDefault()}> + {editableObject(info)} +
+
+ ); +}; + +function editableObject(obj, path=[]) { + const readOnlyKeys = ["uri", "version", "urls_streaming_api", "stats"]; + const hiddenKeys = ["contact_account_", "urls"]; + const explicitShownKeys = ["contact_account_username"]; + const implementedKeys = "title, contact_account_username, email, short_description, description, terms, avatar, header".split(", "); + + let listing = Object.entries(obj).map(([key, val]) => { + let fullkey = [...path, key].join("_"); + + if ( + hiddenKeys.includes(fullkey) || + hiddenKeys.includes(path.join("_")+"_") // also match just parent path + ) { + if (!explicitShownKeys.includes(fullkey)) { + return null; + } + } + + if (Array.isArray(val)) { + // FIXME: handle this + } else if (typeof val == "object") { + return ( + {editableObject(val, [...path, key])} + ); + } + + let isImplemented = ""; + if (!implementedKeys.includes(fullkey)) { + isImplemented = " notImplemented"; + } + + let isReadOnly = ( + readOnlyKeys.includes(fullkey) || + readOnlyKeys.includes(path.join("_")) || + isImplemented != "" + ); + + let label = key.replace(/_/g, " "); + if (path.length > 0) { + label = `\u00A0`.repeat(4 * path.length) + label; + } + + let inputProps; + let changeFunc; + if (val === true || val === false) { + inputProps = { + type: "checkbox", + defaultChecked: val, + disabled: isReadOnly + }; + changeFunc = (e) => e.target.checked; + } else if (val.length != 0 && !isNaN(val)) { + inputProps = { + type: "number", + defaultValue: val, + readOnly: isReadOnly + }; + changeFunc = (e) => e.target.value; + } else { + inputProps = { + type: "text", + defaultValue: val, + readOnly: isReadOnly + }; + changeFunc = (e) => e.target.value; + } + + function setRef(element) { + if (element != null) { + element.addEventListener("change", (e) => { + obj[key] = changeFunc(e); + }); + } + } + + return ( + + +
+ +
+
+ ); + }); + return ( + + {path != "" && + <>{path}: + } + {listing} + + ); +} \ No newline at end of file diff --git a/web/source/panels/admin/style.css b/web/source/panels/admin/style.css new file mode 100644 index 000000000..c9d2f09b4 --- /dev/null +++ b/web/source/panels/admin/style.css @@ -0,0 +1,141 @@ +/* + 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 . +*/ + +body { + grid-template-rows: auto 1fr; +} + +.capitalize { + text-transform: capitalize; +} + +section { + margin-bottom: 1rem; +} + +input, select, textarea { + box-sizing: border-box; +} + +section.info { + form { + grid-template-columns: auto 1fr; + width: calc(100% - 0.35rem); + + input { + width: 100%; + line-height: 1.5rem; + } + + label, input { + padding: 0.2rem 0.5rem; + } + + input[type=checkbox] { + justify-self: start; + width: initial; + } + + input:read-only { + border: none; + } + + input:invalid { + border-color: red; + } + } + + textarea { + width: 100%; + height: 8rem; + } + + h1 { + display: flex; + justify-content: space-between; + margin-bottom: 0.5rem; + } +} + +section.blocks { + .overflow { + max-height: 80vh; + overflow-y: auto; + } + + .blocklist { + display: grid; + grid-template-columns: auto 1fr auto; + grid-gap: 0.35rem 0; + + div { + background: rgb(70, 79, 88); + padding: 0.2rem 0.4rem; + } + } + + .addblock { + display: grid; + grid-template-columns: 1fr auto auto; + grid-gap: 0.35rem; + + input, select { + font-size: 1.2rem; + } + + input, select, textarea { + padding: 0.5rem; + } + + div { + grid-column: 1/4; + } + + div.single input { + width: initial; + } + } + + h3 { + margin-bottom: 0; + } + + .controls { + display: flex; + gap: 0.5rem; + } +} + +.error { + font-weight: bold; +} + +.hidden { + display: none; +} + +.notImplemented { + border: 2px solid rgb(70, 79, 88); + background: repeating-linear-gradient( + -45deg, + #525c66, + #525c66 10px, + rgb(70, 79, 88) 10px, + rgb(70, 79, 88) 20px + ) !important; +} \ No newline at end of file diff --git a/web/source/panels/user/index.js b/web/source/panels/user/index.js new file mode 100644 index 000000000..7adc320d8 --- /dev/null +++ b/web/source/panels/user/index.js @@ -0,0 +1,31 @@ +/* + 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 . +*/ + +"use strict"; + +const Promise = require("bluebird"); +const React = require("react"); +const ReactDom = require("react-dom"); + +// require("./style.css"); + +function App() { + return "hello world - user panel"; +} + +ReactDom.render(, document.getElementById("root")); \ No newline at end of file diff --git a/web/source/yarn.lock b/web/source/yarn.lock new file mode 100644 index 000000000..6d9059e11 --- /dev/null +++ b/web/source/yarn.lock @@ -0,0 +1,6307 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" + integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== + dependencies: + "@babel/highlight" "^7.10.4" + +"@babel/code-frame@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb" + integrity sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw== + dependencies: + "@babel/highlight" "^7.14.5" + +"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.14.7", "@babel/compat-data@^7.15.0": + version "7.15.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.15.0.tgz#2dbaf8b85334796cafbb0f5793a90a2fc010b176" + integrity sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA== + +"@babel/core@^7.12.13": + version "7.15.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.15.0.tgz#749e57c68778b73ad8082775561f67f5196aafa8" + integrity sha512-tXtmTminrze5HEUPn/a0JtOzzfp0nk+UEXQ/tqIJo3WDGypl/2OFQEMll/zSFU8f/lfmfLXvTaORHF3cfXIQMw== + dependencies: + "@babel/code-frame" "^7.14.5" + "@babel/generator" "^7.15.0" + "@babel/helper-compilation-targets" "^7.15.0" + "@babel/helper-module-transforms" "^7.15.0" + "@babel/helpers" "^7.14.8" + "@babel/parser" "^7.15.0" + "@babel/template" "^7.14.5" + "@babel/traverse" "^7.15.0" + "@babel/types" "^7.15.0" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.1.2" + semver "^6.3.0" + source-map "^0.5.0" + +"@babel/generator@^7.15.0": + version "7.15.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.15.0.tgz#a7d0c172e0d814974bad5aa77ace543b97917f15" + integrity sha512-eKl4XdMrbpYvuB505KTta4AV9g+wWzmVBW69tX0H2NwKVKd2YJbKgyK6M8j/rgLbmHOYJn6rUklV677nOyJrEQ== + dependencies: + "@babel/types" "^7.15.0" + jsesc "^2.5.1" + source-map "^0.5.0" + +"@babel/helper-annotate-as-pure@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz#7bf478ec3b71726d56a8ca5775b046fc29879e61" + integrity sha512-EivH9EgBIb+G8ij1B2jAwSH36WnGvkQSEC6CkX/6v6ZFlw5fVOHvsgGF4uiEHO2GzMvunZb6tDLQEQSdrdocrA== + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-builder-binary-assignment-operator-visitor@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.14.5.tgz#b939b43f8c37765443a19ae74ad8b15978e0a191" + integrity sha512-YTA/Twn0vBXDVGJuAX6PwW7x5zQei1luDDo2Pl6q1qZ7hVNl0RZrhHCQG/ArGpR29Vl7ETiB8eJyrvpuRp300w== + dependencies: + "@babel/helper-explode-assignable-expression" "^7.14.5" + "@babel/types" "^7.14.5" + +"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.14.5", "@babel/helper-compilation-targets@^7.15.0": + version "7.15.0" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.0.tgz#973df8cbd025515f3ff25db0c05efc704fa79818" + integrity sha512-h+/9t0ncd4jfZ8wsdAsoIxSa61qhBYlycXiHWqJaQBCXAhDCMbPRSMTGnZIkkmt1u4ag+UQmuqcILwqKzZ4N2A== + dependencies: + "@babel/compat-data" "^7.15.0" + "@babel/helper-validator-option" "^7.14.5" + browserslist "^4.16.6" + semver "^6.3.0" + +"@babel/helper-create-class-features-plugin@^7.14.5": + version "7.15.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.15.0.tgz#c9a137a4d137b2d0e2c649acf536d7ba1a76c0f7" + integrity sha512-MdmDXgvTIi4heDVX/e9EFfeGpugqm9fobBVg/iioE8kueXrOHdRDe36FAY7SnE9xXLVeYCoJR/gdrBEIHRC83Q== + dependencies: + "@babel/helper-annotate-as-pure" "^7.14.5" + "@babel/helper-function-name" "^7.14.5" + "@babel/helper-member-expression-to-functions" "^7.15.0" + "@babel/helper-optimise-call-expression" "^7.14.5" + "@babel/helper-replace-supers" "^7.15.0" + "@babel/helper-split-export-declaration" "^7.14.5" + +"@babel/helper-create-regexp-features-plugin@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.14.5.tgz#c7d5ac5e9cf621c26057722fb7a8a4c5889358c4" + integrity sha512-TLawwqpOErY2HhWbGJ2nZT5wSkR192QpN+nBg1THfBfftrlvOh+WbhrxXCH4q4xJ9Gl16BGPR/48JA+Ryiho/A== + dependencies: + "@babel/helper-annotate-as-pure" "^7.14.5" + regexpu-core "^4.7.1" + +"@babel/helper-define-polyfill-provider@^0.2.2": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.2.3.tgz#0525edec5094653a282688d34d846e4c75e9c0b6" + integrity sha512-RH3QDAfRMzj7+0Nqu5oqgO5q9mFtQEVvCRsi8qCEfzLR9p2BHfn5FzhSB2oj1fF7I2+DcTORkYaQ6aTR9Cofew== + dependencies: + "@babel/helper-compilation-targets" "^7.13.0" + "@babel/helper-module-imports" "^7.12.13" + "@babel/helper-plugin-utils" "^7.13.0" + "@babel/traverse" "^7.13.0" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + semver "^6.1.2" + +"@babel/helper-explode-assignable-expression@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.14.5.tgz#8aa72e708205c7bb643e45c73b4386cdf2a1f645" + integrity sha512-Htb24gnGJdIGT4vnRKMdoXiOIlqOLmdiUYpAQ0mYfgVT/GDm8GOYhgi4GL+hMKrkiPRohO4ts34ELFsGAPQLDQ== + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-function-name@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz#89e2c474972f15d8e233b52ee8c480e2cfcd50c4" + integrity sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ== + dependencies: + "@babel/helper-get-function-arity" "^7.14.5" + "@babel/template" "^7.14.5" + "@babel/types" "^7.14.5" + +"@babel/helper-get-function-arity@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz#25fbfa579b0937eee1f3b805ece4ce398c431815" + integrity sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg== + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-hoist-variables@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz#e0dd27c33a78e577d7c8884916a3e7ef1f7c7f8d" + integrity sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ== + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-member-expression-to-functions@^7.15.0": + version "7.15.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.0.tgz#0ddaf5299c8179f27f37327936553e9bba60990b" + integrity sha512-Jq8H8U2kYiafuj2xMTPQwkTBnEEdGKpT35lJEQsRRjnG0LW3neucsaMWLgKcwu3OHKNeYugfw+Z20BXBSEs2Lg== + dependencies: + "@babel/types" "^7.15.0" + +"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz#6d1a44df6a38c957aa7c312da076429f11b422f3" + integrity sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ== + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-module-transforms@^7.14.5", "@babel/helper-module-transforms@^7.15.0": + version "7.15.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.15.0.tgz#679275581ea056373eddbe360e1419ef23783b08" + integrity sha512-RkGiW5Rer7fpXv9m1B3iHIFDZdItnO2/BLfWVW/9q7+KqQSDY5kUfQEbzdXM1MVhJGcugKV7kRrNVzNxmk7NBg== + dependencies: + "@babel/helper-module-imports" "^7.14.5" + "@babel/helper-replace-supers" "^7.15.0" + "@babel/helper-simple-access" "^7.14.8" + "@babel/helper-split-export-declaration" "^7.14.5" + "@babel/helper-validator-identifier" "^7.14.9" + "@babel/template" "^7.14.5" + "@babel/traverse" "^7.15.0" + "@babel/types" "^7.15.0" + +"@babel/helper-optimise-call-expression@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz#f27395a8619e0665b3f0364cddb41c25d71b499c" + integrity sha512-IqiLIrODUOdnPU9/F8ib1Fx2ohlgDhxnIDU7OEVi+kAbEZcyiF7BLU8W6PfvPi9LzztjS7kcbzbmL7oG8kD6VA== + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9" + integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ== + +"@babel/helper-remap-async-to-generator@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.14.5.tgz#51439c913612958f54a987a4ffc9ee587a2045d6" + integrity sha512-rLQKdQU+HYlxBwQIj8dk4/0ENOUEhA/Z0l4hN8BexpvmSMN9oA9EagjnhnDpNsRdWCfjwa4mn/HyBXO9yhQP6A== + dependencies: + "@babel/helper-annotate-as-pure" "^7.14.5" + "@babel/helper-wrap-function" "^7.14.5" + "@babel/types" "^7.14.5" + +"@babel/helper-replace-supers@^7.14.5", "@babel/helper-replace-supers@^7.15.0": + version "7.15.0" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.15.0.tgz#ace07708f5bf746bf2e6ba99572cce79b5d4e7f4" + integrity sha512-6O+eWrhx+HEra/uJnifCwhwMd6Bp5+ZfZeJwbqUTuqkhIT6YcRhiZCOOFChRypOIe0cV46kFrRBlm+t5vHCEaA== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.15.0" + "@babel/helper-optimise-call-expression" "^7.14.5" + "@babel/traverse" "^7.15.0" + "@babel/types" "^7.15.0" + +"@babel/helper-simple-access@^7.14.8": + version "7.14.8" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.14.8.tgz#82e1fec0644a7e775c74d305f212c39f8fe73924" + integrity sha512-TrFN4RHh9gnWEU+s7JloIho2T76GPwRHhdzOWLqTrMnlas8T9O7ec+oEDNsRXndOmru9ymH9DFrEOxpzPoSbdg== + dependencies: + "@babel/types" "^7.14.8" + +"@babel/helper-skip-transparent-expression-wrappers@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.14.5.tgz#96f486ac050ca9f44b009fbe5b7d394cab3a0ee4" + integrity sha512-dmqZB7mrb94PZSAOYtr+ZN5qt5owZIAgqtoTuqiFbHFtxgEcmQlRJVI+bO++fciBunXtB6MK7HrzrfcAzIz2NQ== + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-split-export-declaration@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz#22b23a54ef51c2b7605d851930c1976dd0bc693a" + integrity sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA== + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-validator-identifier@^7.14.5", "@babel/helper-validator-identifier@^7.14.9": + version "7.14.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz#6654d171b2024f6d8ee151bf2509699919131d48" + integrity sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g== + +"@babel/helper-validator-option@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3" + integrity sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow== + +"@babel/helper-wrap-function@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.14.5.tgz#5919d115bf0fe328b8a5d63bcb610f51601f2bff" + integrity sha512-YEdjTCq+LNuNS1WfxsDCNpgXkJaIyqco6DAelTUjT4f2KIWC1nBcaCaSdHTBqQVLnTBexBcVcFhLSU1KnYuePQ== + dependencies: + "@babel/helper-function-name" "^7.14.5" + "@babel/template" "^7.14.5" + "@babel/traverse" "^7.14.5" + "@babel/types" "^7.14.5" + +"@babel/helpers@^7.14.8": + version "7.15.3" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.15.3.tgz#c96838b752b95dcd525b4e741ed40bb1dc2a1357" + integrity sha512-HwJiz52XaS96lX+28Tnbu31VeFSQJGOeKHJeaEPQlTl7PnlhFElWPj8tUXtqFIzeN86XxXoBr+WFAyK2PPVz6g== + dependencies: + "@babel/template" "^7.14.5" + "@babel/traverse" "^7.15.0" + "@babel/types" "^7.15.0" + +"@babel/highlight@^7.10.4", "@babel/highlight@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9" + integrity sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg== + dependencies: + "@babel/helper-validator-identifier" "^7.14.5" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.14.5", "@babel/parser@^7.15.0": + version "7.15.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.3.tgz#3416d9bea748052cfcb63dbcc27368105b1ed862" + integrity sha512-O0L6v/HvqbdJawj0iBEfVQMc3/6WP+AeOsovsIgBFyJaG+W2w7eqvZB7puddATmWuARlm1SX7DwxJ/JJUnDpEA== + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.14.5.tgz#4b467302e1548ed3b1be43beae2cc9cf45e0bb7e" + integrity sha512-ZoJS2XCKPBfTmL122iP6NM9dOg+d4lc9fFk3zxc8iDjvt8Pk4+TlsHSKhIPf6X+L5ORCdBzqMZDjL/WHj7WknQ== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.14.5" + "@babel/plugin-proposal-optional-chaining" "^7.14.5" + +"@babel/plugin-proposal-async-generator-functions@^7.14.9": + version "7.14.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.14.9.tgz#7028dc4fa21dc199bbacf98b39bab1267d0eaf9a" + integrity sha512-d1lnh+ZnKrFKwtTYdw320+sQWCTwgkB9fmUhNXRADA4akR6wLjaruSGnIEUjpt9HCOwTr4ynFTKu19b7rFRpmw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-remap-async-to-generator" "^7.14.5" + "@babel/plugin-syntax-async-generators" "^7.8.4" + +"@babel/plugin-proposal-class-properties@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.14.5.tgz#40d1ee140c5b1e31a350f4f5eed945096559b42e" + integrity sha512-q/PLpv5Ko4dVc1LYMpCY7RVAAO4uk55qPwrIuJ5QJ8c6cVuAmhu7I/49JOppXL6gXf7ZHzpRVEUZdYoPLM04Gg== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-proposal-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.14.5.tgz#158e9e10d449c3849ef3ecde94a03d9f1841b681" + integrity sha512-KBAH5ksEnYHCegqseI5N9skTdxgJdmDoAOc0uXa+4QMYKeZD0w5IARh4FMlTNtaHhbB8v+KzMdTgxMMzsIy6Yg== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + +"@babel/plugin-proposal-dynamic-import@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.14.5.tgz#0c6617df461c0c1f8fff3b47cd59772360101d2c" + integrity sha512-ExjiNYc3HDN5PXJx+bwC50GIx/KKanX2HiggnIUAYedbARdImiCU4RhhHfdf0Kd7JNXGpsBBBCOm+bBVy3Gb0g== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + +"@babel/plugin-proposal-export-namespace-from@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.14.5.tgz#dbad244310ce6ccd083072167d8cea83a52faf76" + integrity sha512-g5POA32bXPMmSBu5Dx/iZGLGnKmKPc5AiY7qfZgurzrCYgIztDlHFbznSNCoQuv57YQLnQfaDi7dxCtLDIdXdA== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + +"@babel/plugin-proposal-json-strings@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.14.5.tgz#38de60db362e83a3d8c944ac858ddf9f0c2239eb" + integrity sha512-NSq2fczJYKVRIsUJyNxrVUMhB27zb7N7pOFGQOhBKJrChbGcgEAqyZrmZswkPk18VMurEeJAaICbfm57vUeTbQ== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-json-strings" "^7.8.3" + +"@babel/plugin-proposal-logical-assignment-operators@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.14.5.tgz#6e6229c2a99b02ab2915f82571e0cc646a40c738" + integrity sha512-YGn2AvZAo9TwyhlLvCCWxD90Xq8xJ4aSgaX3G5D/8DW94L8aaT+dS5cSP+Z06+rCJERGSr9GxMBZ601xoc2taw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + +"@babel/plugin-proposal-nullish-coalescing-operator@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.14.5.tgz#ee38589ce00e2cc59b299ec3ea406fcd3a0fdaf6" + integrity sha512-gun/SOnMqjSb98Nkaq2rTKMwervfdAoz6NphdY0vTfuzMfryj+tDGb2n6UkDKwez+Y8PZDhE3D143v6Gepp4Hg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + +"@babel/plugin-proposal-numeric-separator@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.14.5.tgz#83631bf33d9a51df184c2102a069ac0c58c05f18" + integrity sha512-yiclALKe0vyZRZE0pS6RXgjUOt87GWv6FYa5zqj15PvhOGFO69R5DusPlgK/1K5dVnCtegTiWu9UaBSrLLJJBg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + +"@babel/plugin-proposal-object-rest-spread@^7.14.7": + version "7.14.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.14.7.tgz#5920a2b3df7f7901df0205974c0641b13fd9d363" + integrity sha512-082hsZz+sVabfmDWo1Oct1u1AgbKbUAyVgmX4otIc7bdsRgHBXwTwb3DpDmD4Eyyx6DNiuz5UAATT655k+kL5g== + dependencies: + "@babel/compat-data" "^7.14.7" + "@babel/helper-compilation-targets" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.14.5" + +"@babel/plugin-proposal-optional-catch-binding@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.14.5.tgz#939dd6eddeff3a67fdf7b3f044b5347262598c3c" + integrity sha512-3Oyiixm0ur7bzO5ybNcZFlmVsygSIQgdOa7cTfOYCMY+wEPAYhZAJxi3mixKFCTCKUhQXuCTtQ1MzrpL3WT8ZQ== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + +"@babel/plugin-proposal-optional-chaining@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.14.5.tgz#fa83651e60a360e3f13797eef00b8d519695b603" + integrity sha512-ycz+VOzo2UbWNI1rQXxIuMOzrDdHGrI23fRiz/Si2R4kv2XZQ1BK8ccdHwehMKBlcH/joGW/tzrUmo67gbJHlQ== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.14.5" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + +"@babel/plugin-proposal-private-methods@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.14.5.tgz#37446495996b2945f30f5be5b60d5e2aa4f5792d" + integrity sha512-838DkdUA1u+QTCplatfq4B7+1lnDa/+QMI89x5WZHBcnNv+47N8QEj2k9I2MUU9xIv8XJ4XvPCviM/Dj7Uwt9g== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-proposal-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.14.5.tgz#9f65a4d0493a940b4c01f8aa9d3f1894a587f636" + integrity sha512-62EyfyA3WA0mZiF2e2IV9mc9Ghwxcg8YTu8BS4Wss4Y3PY725OmS9M0qLORbJwLqFtGh+jiE4wAmocK2CTUK2Q== + dependencies: + "@babel/helper-annotate-as-pure" "^7.14.5" + "@babel/helper-create-class-features-plugin" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + +"@babel/plugin-proposal-unicode-property-regex@^7.14.5", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.14.5.tgz#0f95ee0e757a5d647f378daa0eca7e93faa8bbe8" + integrity sha512-6axIeOU5LnY471KenAB9vI8I5j7NQ2d652hIYwVyRfgaZT5UpiqFKCuVXCDMSrU+3VFafnu2c5m3lrWIlr6A5Q== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-dynamic-import@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" + integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.14.5.tgz#000e2e25d8673cce49300517a3eda44c263e4201" + integrity sha512-ohuFIsOMXJnbOMRfX7/w7LocdR6R7whhuRD4ax8IipLcLPlZGJKkBxgHp++U4N/vKyU16/YDQr2f5seajD3jIw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-arrow-functions@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.14.5.tgz#f7187d9588a768dd080bf4c9ffe117ea62f7862a" + integrity sha512-KOnO0l4+tD5IfOdi4x8C1XmEIRWUjNRV8wc6K2vz/3e8yAOoZZvsRXRRIF/yo/MAOFb4QjtAw9xSxMXbSMRy8A== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-async-to-generator@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.14.5.tgz#72c789084d8f2094acb945633943ef8443d39e67" + integrity sha512-szkbzQ0mNk0rpu76fzDdqSyPu0MuvpXgC+6rz5rpMb5OIRxdmHfQxrktL8CYolL2d8luMCZTR0DpIMIdL27IjA== + dependencies: + "@babel/helper-module-imports" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-remap-async-to-generator" "^7.14.5" + +"@babel/plugin-transform-block-scoped-functions@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.14.5.tgz#e48641d999d4bc157a67ef336aeb54bc44fd3ad4" + integrity sha512-dtqWqdWZ5NqBX3KzsVCWfQI3A53Ft5pWFCT2eCVUftWZgjc5DpDponbIF1+c+7cSGk2wN0YK7HGL/ezfRbpKBQ== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-block-scoping@^7.14.5": + version "7.15.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.15.3.tgz#94c81a6e2fc230bcce6ef537ac96a1e4d2b3afaf" + integrity sha512-nBAzfZwZb4DkaGtOes1Up1nOAp9TDRRFw4XBzBBSG9QK7KVFmYzgj9o9sbPv7TX5ofL4Auq4wZnxCoPnI/lz2Q== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-classes@^7.14.9": + version "7.14.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.14.9.tgz#2a391ffb1e5292710b00f2e2c210e1435e7d449f" + integrity sha512-NfZpTcxU3foGWbl4wxmZ35mTsYJy8oQocbeIMoDAGGFarAmSQlL+LWMkDx/tj6pNotpbX3rltIA4dprgAPOq5A== + dependencies: + "@babel/helper-annotate-as-pure" "^7.14.5" + "@babel/helper-function-name" "^7.14.5" + "@babel/helper-optimise-call-expression" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-replace-supers" "^7.14.5" + "@babel/helper-split-export-declaration" "^7.14.5" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.14.5.tgz#1b9d78987420d11223d41195461cc43b974b204f" + integrity sha512-pWM+E4283UxaVzLb8UBXv4EIxMovU4zxT1OPnpHJcmnvyY9QbPPTKZfEj31EUvG3/EQRbYAGaYEUZ4yWOBC2xg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-destructuring@^7.14.7": + version "7.14.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.14.7.tgz#0ad58ed37e23e22084d109f185260835e5557576" + integrity sha512-0mDE99nK+kVh3xlc5vKwB6wnP9ecuSj+zQCa/n0voENtP/zymdT4HH6QEb65wjjcbqr1Jb/7z9Qp7TF5FtwYGw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-dotall-regex@^7.14.5", "@babel/plugin-transform-dotall-regex@^7.4.4": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.14.5.tgz#2f6bf76e46bdf8043b4e7e16cf24532629ba0c7a" + integrity sha512-loGlnBdj02MDsFaHhAIJzh7euK89lBrGIdM9EAtHFo6xKygCUGuuWe07o1oZVk287amtW1n0808sQM99aZt3gw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-duplicate-keys@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.14.5.tgz#365a4844881bdf1501e3a9f0270e7f0f91177954" + integrity sha512-iJjbI53huKbPDAsJ8EmVmvCKeeq21bAze4fu9GBQtSLqfvzj2oRuHVx4ZkDwEhg1htQ+5OBZh/Ab0XDf5iBZ7A== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-exponentiation-operator@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.14.5.tgz#5154b8dd6a3dfe6d90923d61724bd3deeb90b493" + integrity sha512-jFazJhMBc9D27o9jDnIE5ZErI0R0m7PbKXVq77FFvqFbzvTMuv8jaAwLZ5PviOLSFttqKIW0/wxNSDbjLk0tYA== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-for-of@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.14.5.tgz#dae384613de8f77c196a8869cbf602a44f7fc0eb" + integrity sha512-CfmqxSUZzBl0rSjpoQSFoR9UEj3HzbGuGNL21/iFTmjb5gFggJp3ph0xR1YBhexmLoKRHzgxuFvty2xdSt6gTA== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-function-name@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.14.5.tgz#e81c65ecb900746d7f31802f6bed1f52d915d6f2" + integrity sha512-vbO6kv0fIzZ1GpmGQuvbwwm+O4Cbm2NrPzwlup9+/3fdkuzo1YqOZcXw26+YUJB84Ja7j9yURWposEHLYwxUfQ== + dependencies: + "@babel/helper-function-name" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-literals@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.14.5.tgz#41d06c7ff5d4d09e3cf4587bd3ecf3930c730f78" + integrity sha512-ql33+epql2F49bi8aHXxvLURHkxJbSmMKl9J5yHqg4PLtdE6Uc48CH1GS6TQvZ86eoB/ApZXwm7jlA+B3kra7A== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-member-expression-literals@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.14.5.tgz#b39cd5212a2bf235a617d320ec2b48bcc091b8a7" + integrity sha512-WkNXxH1VXVTKarWFqmso83xl+2V3Eo28YY5utIkbsmXoItO8Q3aZxN4BTS2k0hz9dGUloHK26mJMyQEYfkn/+Q== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-modules-amd@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.14.5.tgz#4fd9ce7e3411cb8b83848480b7041d83004858f7" + integrity sha512-3lpOU8Vxmp3roC4vzFpSdEpGUWSMsHFreTWOMMLzel2gNGfHE5UWIh/LN6ghHs2xurUp4jRFYMUIZhuFbody1g== + dependencies: + "@babel/helper-module-transforms" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + babel-plugin-dynamic-import-node "^2.3.3" + +"@babel/plugin-transform-modules-commonjs@^7.15.0": + version "7.15.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.15.0.tgz#3305896e5835f953b5cdb363acd9e8c2219a5281" + integrity sha512-3H/R9s8cXcOGE8kgMlmjYYC9nqr5ELiPkJn4q0mypBrjhYQoc+5/Maq69vV4xRPWnkzZuwJPf5rArxpB/35Cig== + dependencies: + "@babel/helper-module-transforms" "^7.15.0" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-simple-access" "^7.14.8" + babel-plugin-dynamic-import-node "^2.3.3" + +"@babel/plugin-transform-modules-systemjs@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.14.5.tgz#c75342ef8b30dcde4295d3401aae24e65638ed29" + integrity sha512-mNMQdvBEE5DcMQaL5LbzXFMANrQjd2W7FPzg34Y4yEz7dBgdaC+9B84dSO+/1Wba98zoDbInctCDo4JGxz1VYA== + dependencies: + "@babel/helper-hoist-variables" "^7.14.5" + "@babel/helper-module-transforms" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-validator-identifier" "^7.14.5" + babel-plugin-dynamic-import-node "^2.3.3" + +"@babel/plugin-transform-modules-umd@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.14.5.tgz#fb662dfee697cce274a7cda525190a79096aa6e0" + integrity sha512-RfPGoagSngC06LsGUYyM9QWSXZ8MysEjDJTAea1lqRjNECE3y0qIJF/qbvJxc4oA4s99HumIMdXOrd+TdKaAAA== + dependencies: + "@babel/helper-module-transforms" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.14.9": + version "7.14.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.14.9.tgz#c68f5c5d12d2ebaba3762e57c2c4f6347a46e7b2" + integrity sha512-l666wCVYO75mlAtGFfyFwnWmIXQm3kSH0C3IRnJqWcZbWkoihyAdDhFm2ZWaxWTqvBvhVFfJjMRQ0ez4oN1yYA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.14.5" + +"@babel/plugin-transform-new-target@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.14.5.tgz#31bdae8b925dc84076ebfcd2a9940143aed7dbf8" + integrity sha512-Nx054zovz6IIRWEB49RDRuXGI4Gy0GMgqG0cII9L3MxqgXz/+rgII+RU58qpo4g7tNEx1jG7rRVH4ihZoP4esQ== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-object-super@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.14.5.tgz#d0b5faeac9e98597a161a9cf78c527ed934cdc45" + integrity sha512-MKfOBWzK0pZIrav9z/hkRqIk/2bTv9qvxHzPQc12RcVkMOzpIKnFCNYJip00ssKWYkd8Sf5g0Wr7pqJ+cmtuFg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-replace-supers" "^7.14.5" + +"@babel/plugin-transform-parameters@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.14.5.tgz#49662e86a1f3ddccac6363a7dfb1ff0a158afeb3" + integrity sha512-Tl7LWdr6HUxTmzQtzuU14SqbgrSKmaR77M0OKyq4njZLQTPfOvzblNKyNkGwOfEFCEx7KeYHQHDI0P3F02IVkA== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-property-literals@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.14.5.tgz#0ddbaa1f83db3606f1cdf4846fa1dfb473458b34" + integrity sha512-r1uilDthkgXW8Z1vJz2dKYLV1tuw2xsbrp3MrZmD99Wh9vsfKoob+JTgri5VUb/JqyKRXotlOtwgu4stIYCmnw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-react-display-name@^7.14.5": + version "7.15.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.15.1.tgz#6aaac6099f1fcf6589d35ae6be1b6e10c8c602b9" + integrity sha512-yQZ/i/pUCJAHI/LbtZr413S3VT26qNrEm0M5RRxQJA947/YNYwbZbBaXGDrq6CG5QsZycI1VIP6d7pQaBfP+8Q== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-react-jsx-development@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.14.5.tgz#1a6c73e2f7ed2c42eebc3d2ad60b0c7494fcb9af" + integrity sha512-rdwG/9jC6QybWxVe2UVOa7q6cnTpw8JRRHOxntG/h6g/guAOe6AhtQHJuJh5FwmnXIT1bdm5vC2/5huV8ZOorQ== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.14.5" + +"@babel/plugin-transform-react-jsx@^7.14.5": + version "7.14.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.14.9.tgz#3314b2163033abac5200a869c4de242cd50a914c" + integrity sha512-30PeETvS+AeD1f58i1OVyoDlVYQhap/K20ZrMjLmmzmC2AYR/G43D4sdJAaDAqCD3MYpSWbmrz3kES158QSLjw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.14.5" + "@babel/helper-module-imports" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-jsx" "^7.14.5" + "@babel/types" "^7.14.9" + +"@babel/plugin-transform-react-pure-annotations@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.14.5.tgz#18de612b84021e3a9802cbc212c9d9f46d0d11fc" + integrity sha512-3X4HpBJimNxW4rhUy/SONPyNQHp5YRr0HhJdT2OH1BRp0of7u3Dkirc7x9FRJMKMqTBI079VZ1hzv7Ouuz///g== + dependencies: + "@babel/helper-annotate-as-pure" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-regenerator@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.14.5.tgz#9676fd5707ed28f522727c5b3c0aa8544440b04f" + integrity sha512-NVIY1W3ITDP5xQl50NgTKlZ0GrotKtLna08/uGY6ErQt6VEQZXla86x/CTddm5gZdcr+5GSsvMeTmWA5Ii6pkg== + dependencies: + regenerator-transform "^0.14.2" + +"@babel/plugin-transform-reserved-words@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.14.5.tgz#c44589b661cfdbef8d4300dcc7469dffa92f8304" + integrity sha512-cv4F2rv1nD4qdexOGsRQXJrOcyb5CrgjUH9PKrrtyhSDBNWGxd0UIitjyJiWagS+EbUGjG++22mGH1Pub8D6Vg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-shorthand-properties@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.14.5.tgz#97f13855f1409338d8cadcbaca670ad79e091a58" + integrity sha512-xLucks6T1VmGsTB+GWK5Pl9Jl5+nRXD1uoFdA5TSO6xtiNjtXTjKkmPdFXVLGlK5A2/or/wQMKfmQ2Y0XJfn5g== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-spread@^7.14.6": + version "7.14.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.14.6.tgz#6bd40e57fe7de94aa904851963b5616652f73144" + integrity sha512-Zr0x0YroFJku7n7+/HH3A2eIrGMjbmAIbJSVv0IZ+t3U2WUQUA64S/oeied2e+MaGSjmt4alzBCsK9E8gh+fag== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.14.5" + +"@babel/plugin-transform-sticky-regex@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.14.5.tgz#5b617542675e8b7761294381f3c28c633f40aeb9" + integrity sha512-Z7F7GyvEMzIIbwnziAZmnSNpdijdr4dWt+FJNBnBLz5mwDFkqIXU9wmBcWWad3QeJF5hMTkRe4dAq2sUZiG+8A== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-template-literals@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.14.5.tgz#a5f2bc233937d8453885dc736bdd8d9ffabf3d93" + integrity sha512-22btZeURqiepOfuy/VkFr+zStqlujWaarpMErvay7goJS6BWwdd6BY9zQyDLDa4x2S3VugxFb162IZ4m/S/+Gg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-typeof-symbol@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.14.5.tgz#39af2739e989a2bd291bf6b53f16981423d457d4" + integrity sha512-lXzLD30ffCWseTbMQzrvDWqljvZlHkXU+CnseMhkMNqU1sASnCsz3tSzAaH3vCUXb9PHeUb90ZT1BdFTm1xxJw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-unicode-escapes@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.14.5.tgz#9d4bd2a681e3c5d7acf4f57fa9e51175d91d0c6b" + integrity sha512-crTo4jATEOjxj7bt9lbYXcBAM3LZaUrbP2uUdxb6WIorLmjNKSpHfIybgY4B8SRpbf8tEVIWH3Vtm7ayCrKocA== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-unicode-regex@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.14.5.tgz#4cd09b6c8425dd81255c7ceb3fb1836e7414382e" + integrity sha512-UygduJpC5kHeCiRw/xDVzC+wj8VaYSoKl5JNVmbP7MadpNinAm3SvZCxZ42H37KZBKztz46YC73i9yV34d0Tzw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/preset-env@^7.12.13": + version "7.15.0" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.15.0.tgz#e2165bf16594c9c05e52517a194bf6187d6fe464" + integrity sha512-FhEpCNFCcWW3iZLg0L2NPE9UerdtsCR6ZcsGHUX6Om6kbCQeL5QZDqFDmeNHC6/fy6UH3jEge7K4qG5uC9In0Q== + dependencies: + "@babel/compat-data" "^7.15.0" + "@babel/helper-compilation-targets" "^7.15.0" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-validator-option" "^7.14.5" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.14.5" + "@babel/plugin-proposal-async-generator-functions" "^7.14.9" + "@babel/plugin-proposal-class-properties" "^7.14.5" + "@babel/plugin-proposal-class-static-block" "^7.14.5" + "@babel/plugin-proposal-dynamic-import" "^7.14.5" + "@babel/plugin-proposal-export-namespace-from" "^7.14.5" + "@babel/plugin-proposal-json-strings" "^7.14.5" + "@babel/plugin-proposal-logical-assignment-operators" "^7.14.5" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.14.5" + "@babel/plugin-proposal-numeric-separator" "^7.14.5" + "@babel/plugin-proposal-object-rest-spread" "^7.14.7" + "@babel/plugin-proposal-optional-catch-binding" "^7.14.5" + "@babel/plugin-proposal-optional-chaining" "^7.14.5" + "@babel/plugin-proposal-private-methods" "^7.14.5" + "@babel/plugin-proposal-private-property-in-object" "^7.14.5" + "@babel/plugin-proposal-unicode-property-regex" "^7.14.5" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-transform-arrow-functions" "^7.14.5" + "@babel/plugin-transform-async-to-generator" "^7.14.5" + "@babel/plugin-transform-block-scoped-functions" "^7.14.5" + "@babel/plugin-transform-block-scoping" "^7.14.5" + "@babel/plugin-transform-classes" "^7.14.9" + "@babel/plugin-transform-computed-properties" "^7.14.5" + "@babel/plugin-transform-destructuring" "^7.14.7" + "@babel/plugin-transform-dotall-regex" "^7.14.5" + "@babel/plugin-transform-duplicate-keys" "^7.14.5" + "@babel/plugin-transform-exponentiation-operator" "^7.14.5" + "@babel/plugin-transform-for-of" "^7.14.5" + "@babel/plugin-transform-function-name" "^7.14.5" + "@babel/plugin-transform-literals" "^7.14.5" + "@babel/plugin-transform-member-expression-literals" "^7.14.5" + "@babel/plugin-transform-modules-amd" "^7.14.5" + "@babel/plugin-transform-modules-commonjs" "^7.15.0" + "@babel/plugin-transform-modules-systemjs" "^7.14.5" + "@babel/plugin-transform-modules-umd" "^7.14.5" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.14.9" + "@babel/plugin-transform-new-target" "^7.14.5" + "@babel/plugin-transform-object-super" "^7.14.5" + "@babel/plugin-transform-parameters" "^7.14.5" + "@babel/plugin-transform-property-literals" "^7.14.5" + "@babel/plugin-transform-regenerator" "^7.14.5" + "@babel/plugin-transform-reserved-words" "^7.14.5" + "@babel/plugin-transform-shorthand-properties" "^7.14.5" + "@babel/plugin-transform-spread" "^7.14.6" + "@babel/plugin-transform-sticky-regex" "^7.14.5" + "@babel/plugin-transform-template-literals" "^7.14.5" + "@babel/plugin-transform-typeof-symbol" "^7.14.5" + "@babel/plugin-transform-unicode-escapes" "^7.14.5" + "@babel/plugin-transform-unicode-regex" "^7.14.5" + "@babel/preset-modules" "^0.1.4" + "@babel/types" "^7.15.0" + babel-plugin-polyfill-corejs2 "^0.2.2" + babel-plugin-polyfill-corejs3 "^0.2.2" + babel-plugin-polyfill-regenerator "^0.2.2" + core-js-compat "^3.16.0" + semver "^6.3.0" + +"@babel/preset-modules@^0.1.4": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.4.tgz#362f2b68c662842970fdb5e254ffc8fc1c2e415e" + integrity sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" + "@babel/plugin-transform-dotall-regex" "^7.4.4" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/preset-react@^7.12.13": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.14.5.tgz#0fbb769513f899c2c56f3a882fa79673c2d4ab3c" + integrity sha512-XFxBkjyObLvBaAvkx1Ie95Iaq4S/GUEIrejyrntQ/VCMKUYvKLoyKxOBzJ2kjA3b6rC9/KL6KXfDC2GqvLiNqQ== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-validator-option" "^7.14.5" + "@babel/plugin-transform-react-display-name" "^7.14.5" + "@babel/plugin-transform-react-jsx" "^7.14.5" + "@babel/plugin-transform-react-jsx-development" "^7.14.5" + "@babel/plugin-transform-react-pure-annotations" "^7.14.5" + +"@babel/runtime@^7.8.4": + version "7.15.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.3.tgz#2e1c2880ca118e5b2f9988322bd8a7656a32502b" + integrity sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/template@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.14.5.tgz#a9bc9d8b33354ff6e55a9c60d1109200a68974f4" + integrity sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g== + dependencies: + "@babel/code-frame" "^7.14.5" + "@babel/parser" "^7.14.5" + "@babel/types" "^7.14.5" + +"@babel/traverse@^7.13.0", "@babel/traverse@^7.14.5", "@babel/traverse@^7.15.0": + version "7.15.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.15.0.tgz#4cca838fd1b2a03283c1f38e141f639d60b3fc98" + integrity sha512-392d8BN0C9eVxVWd8H6x9WfipgVH5IaIoLp23334Sc1vbKKWINnvwRpb4us0xtPaCumlwbTtIYNA0Dv/32sVFw== + dependencies: + "@babel/code-frame" "^7.14.5" + "@babel/generator" "^7.15.0" + "@babel/helper-function-name" "^7.14.5" + "@babel/helper-hoist-variables" "^7.14.5" + "@babel/helper-split-export-declaration" "^7.14.5" + "@babel/parser" "^7.15.0" + "@babel/types" "^7.15.0" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.14.5", "@babel/types@^7.14.8", "@babel/types@^7.14.9", "@babel/types@^7.15.0", "@babel/types@^7.4.4": + version "7.15.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.15.0.tgz#61af11f2286c4e9c69ca8deb5f4375a73c72dcbd" + integrity sha512-OBvfqnllOIdX4ojTHpwZbpvz4j3EWyjkZEdmjH0/cgsd6QOdSgU8rLSk6ard/pcW7rlmjdVSX/AWOaORR1uNOQ== + dependencies: + "@babel/helper-validator-identifier" "^7.14.9" + to-fast-properties "^2.0.0" + +"@csstools/convert-colors@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7" + integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw== + +"@eslint/eslintrc@^0.4.3": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" + integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw== + dependencies: + ajv "^6.12.4" + debug "^4.1.1" + espree "^7.3.0" + globals "^13.9.0" + ignore "^4.0.6" + import-fresh "^3.2.1" + js-yaml "^3.13.1" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + +"@f0x52/budo-express@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@f0x52/budo-express/-/budo-express-1.1.0.tgz#dbda0eab9da0c186fbb12471d14a64331e474b57" + integrity sha512-BKZOCJW32fIGehD7n7uCBlm8j5BDYQIE/xDFW86QabnnEHir4Q4/jC/XWyXfrgTHI6RjyySUZGp/kBT+tCBzsA== + dependencies: + assure-array "^1.0.0" + bluebird "^3.7.2" + browserify "^16.5.0" + budo "^11.5.0" + chalk "^3.0.0" + default-value "^1.0.0" + entities "^2.0.0" + inject-lr-script "^2.2.0" + is-stream "^2.0.0" + stacked "^1.1.1" + validatem "^0.2.0" + +"@f0x52/eslint-config-react@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@f0x52/eslint-config-react/-/eslint-config-react-1.1.0.tgz#734973a7627603b19e3cec2620658a25d405b110" + integrity sha512-5Tv6laUgU0nSQ91sg7z3eOdf3OsrRwqAIdxxZrjtRW6DcRKz+hn5x1vjZI3JP+4AlMAPddNjpO5FZCYYF6spzw== + dependencies: + "@f0x52/eslint-config" "^1.0.1" + eslint-plugin-react "^7.14.3" + eslint-plugin-react-hooks "^2.0.1" + +"@f0x52/eslint-config@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@f0x52/eslint-config/-/eslint-config-1.0.1.tgz#509e42eb6d176dded7b1632687468d39a9f0da49" + integrity sha512-/08mhJ0q7EklGAojMgkmluQw286LxVLaVAvITTC4tcTaB2qufGFWjFejGuC214BUtJCYlRw8JTa5S2zrju9s8w== + dependencies: + "@joepie91/eslint-config" "^1.1.0" + +"@humanwhocodes/config-array@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" + integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg== + dependencies: + "@humanwhocodes/object-schema" "^1.2.0" + debug "^4.1.1" + minimatch "^3.0.4" + +"@humanwhocodes/object-schema@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf" + integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w== + +"@joepie91/eslint-config@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@joepie91/eslint-config/-/eslint-config-1.1.0.tgz#9397e6ce0a010cb57dcf8aef8754d3a5ce0ae36a" + integrity sha512-XliasRSUfOz1/bAvTBaUlCjWDbceCW4y1DnvFfW7Yw9p2FbNRR0w8WoPdTxTCjKuoZ7/OQMeBxIe2y9Qy6rbYw== + +"@mrmlnc/readdir-enhanced@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" + integrity sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g== + dependencies: + call-me-maybe "^1.0.1" + glob-to-regexp "^0.3.0" + +"@nodelib/fs.stat@^1.1.2": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" + integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== + +JSONStream@^1.0.3: + version "1.3.5" + resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" + integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== + dependencies: + jsonparse "^1.2.0" + through ">=2.2.7 <3" + +JSONStream@~0.7.1: + version "0.7.4" + resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-0.7.4.tgz#734290e41511eea7c2cfe151fbf9a563a97b9786" + integrity sha512-hVgF0Ox1AtvxJmpwzb2dOEUz2ms1J8DZVbqKUSIGSyPBPy0MuxCJsQxj8y5dadTzsjI+T4TpyyhXORPdz15m9w== + dependencies: + jsonparse "0.0.5" + through ">=2.2.7 <3" + +JSONStream@~0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-0.8.4.tgz#91657dfe6ff857483066132b4618b62e8f4887bd" + integrity sha512-l0NN3IcqrZfZBJp7JWDJIHsnPV7yzJWqsYxQzL8Fwdx1BmEMjLuvtYkv+P9pbvpyfP75/f4MeDZhWNU4is32uA== + dependencies: + jsonparse "0.0.5" + through ">=2.2.7 <3" + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-jsx@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn-node@^1.2.0, acorn-node@^1.3.0, acorn-node@^1.5.2, acorn-node@^1.8.2: + version "1.8.2" + resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8" + integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A== + dependencies: + acorn "^7.0.0" + acorn-walk "^7.0.0" + xtend "^4.0.2" + +acorn-walk@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" + integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== + +acorn@^5.2.1: + version "5.7.4" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" + integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg== + +acorn@^7.0.0, acorn@^7.4.0: + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + +ajv@^6.10.0, ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.1: + version "8.6.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.2.tgz#2fb45e0e5fcbc0813326c1c3da535d1881bb0571" + integrity sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +amdefine@>=0.0.4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" + integrity sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg== + +ansi-colors@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-regex@^0.2.0, ansi-regex@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-0.2.1.tgz#0d8e946967a3d8143f93e24e298525fc1b2235f9" + integrity sha512-sGwIGMjhYdW26/IhwK2gkWWI8DRCVO6uj3hYgHT+zD+QL1pa37tM3ujhyfcJIYSbsxp7Gxhy7zrRW/1AHm4BmA== + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + +ansi-styles@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.0.1.tgz#b033f57f93e2d28adeb8bc11138fa13da0fd20a3" + integrity sha512-0zjsXMhnTibRx8YrLgLKb5NvWEcHN/OZEe1NzR8VVrEM6xr7/NyLsoMVelAhaoJhOtpuexaeRGD8MF8Z64+9LQ== + +ansi-styles@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.1.0.tgz#eaecbf66cd706882760b2f4691582b8f55d7a7de" + integrity sha512-f2PKUkN5QngiSemowa6Mrk9MPCdtFiOSmibjZ+j1qhLGHHYsqZwmBMRF3IRMVXo8sybDqx2fJl2d/8OphBoWkA== + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@^3.1.0, anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array-from@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/array-from/-/array-from-2.1.1.tgz#cfe9d8c26628b9dc5aecc62a9f5d8f1f352c1195" + integrity sha1-z+nYwmYoudxa7MYqn12PHzUsEZU= + +array-includes@^3.1.2, array-includes@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.3.tgz#c7f619b382ad2afaf5326cddfdc0afc61af7690a" + integrity sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.2" + get-intrinsic "^1.1.1" + is-string "^1.0.5" + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= + +array.prototype.flatmap@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz#94cfd47cc1556ec0747d97f7c7738c58122004c9" + integrity sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.1" + function-bind "^1.1.1" + +arrify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= + +asn1.js@^5.2.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" + integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + safer-buffer "^2.1.0" + +assert@^1.4.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" + integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== + dependencies: + object-assign "^4.1.1" + util "0.10.3" + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + +assure-array@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assure-array/-/assure-array-1.0.0.tgz#4f4ad16a87659d6200a4fb7103462033d216ec1f" + integrity sha1-T0rRaodlnWIApPtxA0YgM9IW7B8= + +ast-types@0.9.6: + version "0.9.6" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9" + integrity sha512-qEdtR2UH78yyHX/AUNfXmJTlM48XoFZKBdwi1nzkI1mJL21cmbu0cvjxjpkXJ5NENMq42H+hNs8VLJcqXLerBQ== + +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + +async-limiter@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +autoprefixer@^9.8.0: + version "9.8.6" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f" + integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg== + dependencies: + browserslist "^4.12.0" + caniuse-lite "^1.0.30001109" + colorette "^1.2.1" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^7.0.32" + postcss-value-parser "^4.1.0" + +available-typed-arrays@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" + integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== + +babel-plugin-dynamic-import-node@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" + integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== + dependencies: + object.assign "^4.1.0" + +babel-plugin-polyfill-corejs2@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.2.tgz#e9124785e6fd94f94b618a7954e5693053bf5327" + integrity sha512-kISrENsJ0z5dNPq5eRvcctITNHYXWOA4DUZRFYCz3jYCcvTb/A546LIddmoGNMVYg2U38OyFeNosQwI9ENTqIQ== + dependencies: + "@babel/compat-data" "^7.13.11" + "@babel/helper-define-polyfill-provider" "^0.2.2" + semver "^6.1.1" + +babel-plugin-polyfill-corejs3@^0.2.2: + version "0.2.4" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.2.4.tgz#68cb81316b0e8d9d721a92e0009ec6ecd4cd2ca9" + integrity sha512-z3HnJE5TY/j4EFEa/qpQMSbcUJZ5JQi+3UFjXzn6pQCmIKc5Ug5j98SuYyH+m4xQnvKlMDIW4plLfgyVnd0IcQ== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.2.2" + core-js-compat "^3.14.0" + +babel-plugin-polyfill-regenerator@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.2.2.tgz#b310c8d642acada348c1fa3b3e6ce0e851bee077" + integrity sha512-Goy5ghsc21HgPDFtzRkSirpZVW35meGoTmTOb2bxqdl60ghub4xOidgNTHaZfQ2FaxQsKmwvXtOAkcIS4SMBWg== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.2.2" + +babelify@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/babelify/-/babelify-10.0.0.tgz#fe73b1a22583f06680d8d072e25a1e0d1d1d7fb5" + integrity sha512-X40FaxyH7t3X+JFAKvb1H9wooWKLRCi8pg3m8poqtdZaIng+bjzp9RvKQCvRjF9isHiPkXspbbXT/zwXLtwgwg== + +balanced-match@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.1.0.tgz#b504bd05869b39259dd0c5efc35d843176dccc4a" + integrity sha1-tQS9BYabOSWd0MXvw12EMXbczEo= + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base62@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/base62/-/base62-0.1.1.tgz#7b4174c2f94449753b11c2651c083da841a7b084" + integrity sha512-QtExujIOq/F672OkHmDi3CdkphOA1kSQ38gv03Ro3cplYQk831dq9GM3Q1oXAxpR5HNJjGjjjT2pHtBGAJu1jw== + +base64-js@^1.0.2, base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +bl@^4.0.0, bl@^4.0.2: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + +bluebird@^3.7.1, bluebird@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== + +bn.js@^5.0.0, bn.js@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" + integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== + +body-parser@1.20.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5" + integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.10.3" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +bole@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/bole/-/bole-2.0.0.tgz#d8aa1c690467bfb4fe11b874acb2e8387e382615" + integrity sha512-/7aKG4IlOS3Gv15ccrSFiXwXjm5vPAFNfkncSNLYLpq4bH9m9N8Ef4QiTu8NN1lld8p7V1q8l8kOkbExEOv94A== + dependencies: + core-util-is ">=1.0.1 <1.1.0-0" + individual ">=3.0.0 <3.1.0-0" + json-stringify-safe ">=5.0.0 <5.1.0-0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +brorand@^1.0.1, brorand@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== + +browser-pack@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/browser-pack/-/browser-pack-5.0.1.tgz#4197719b20c6e0aaa09451c5111e53efb6fbc18d" + integrity sha512-BFMQuYXCcwr3Uvna1y1hikqd3r2dQpWIQBIN3m5YwE3ClfnXDeF3tqP6Wqjhs1LRUeBJpgHn8yD+fPX/YSEgMQ== + dependencies: + JSONStream "^1.0.3" + combine-source-map "~0.6.1" + defined "^1.0.0" + through2 "^1.0.0" + umd "^3.0.0" + +browser-pack@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/browser-pack/-/browser-pack-6.1.0.tgz#c34ba10d0b9ce162b5af227c7131c92c2ecd5774" + integrity sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA== + dependencies: + JSONStream "^1.0.3" + combine-source-map "~0.8.0" + defined "^1.0.0" + safe-buffer "^5.1.1" + through2 "^2.0.0" + umd "^3.0.0" + +browser-resolve@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-2.0.0.tgz#99b7304cb392f8d73dba741bb2d7da28c6d7842b" + integrity sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ== + dependencies: + resolve "^1.17.0" + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" + integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +browserify-cipher@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" + integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" + integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d" + integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog== + dependencies: + bn.js "^5.0.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3" + integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg== + dependencies: + bn.js "^5.1.1" + browserify-rsa "^4.0.1" + create-hash "^1.2.0" + create-hmac "^1.1.7" + elliptic "^6.5.3" + inherits "^2.0.4" + parse-asn1 "^5.1.5" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" + +browserify-zlib@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" + integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== + dependencies: + pako "~1.0.5" + +browserify@^16.2.3, browserify@^16.5.0: + version "16.5.2" + resolved "https://registry.yarnpkg.com/browserify/-/browserify-16.5.2.tgz#d926835e9280fa5fd57f5bc301f2ef24a972ddfe" + integrity sha512-TkOR1cQGdmXU9zW4YukWzWVSJwrxmNdADFbqbE3HFgQWe5wqZmOawqZ7J/8MPCwk/W8yY7Y0h+7mOtcZxLP23g== + dependencies: + JSONStream "^1.0.3" + assert "^1.4.0" + browser-pack "^6.0.1" + browser-resolve "^2.0.0" + browserify-zlib "~0.2.0" + buffer "~5.2.1" + cached-path-relative "^1.0.0" + concat-stream "^1.6.0" + console-browserify "^1.1.0" + constants-browserify "~1.0.0" + crypto-browserify "^3.0.0" + defined "^1.0.0" + deps-sort "^2.0.0" + domain-browser "^1.2.0" + duplexer2 "~0.1.2" + events "^2.0.0" + glob "^7.1.0" + has "^1.0.0" + htmlescape "^1.1.0" + https-browserify "^1.0.0" + inherits "~2.0.1" + insert-module-globals "^7.0.0" + labeled-stream-splicer "^2.0.0" + mkdirp-classic "^0.5.2" + module-deps "^6.2.3" + os-browserify "~0.3.0" + parents "^1.0.1" + path-browserify "~0.0.0" + process "~0.11.0" + punycode "^1.3.2" + querystring-es3 "~0.2.0" + read-only-stream "^2.0.0" + readable-stream "^2.0.2" + resolve "^1.1.4" + shasum "^1.0.0" + shell-quote "^1.6.1" + stream-browserify "^2.0.0" + stream-http "^3.0.0" + string_decoder "^1.1.1" + subarg "^1.0.0" + syntax-error "^1.1.1" + through2 "^2.0.0" + timers-browserify "^1.0.1" + tty-browserify "0.0.1" + url "~0.11.0" + util "~0.10.1" + vm-browserify "^1.0.0" + xtend "^4.0.0" + +browserify@^17.0.0: + version "17.0.0" + resolved "https://registry.yarnpkg.com/browserify/-/browserify-17.0.0.tgz#4c48fed6c02bfa2b51fd3b670fddb805723cdc22" + integrity sha512-SaHqzhku9v/j6XsQMRxPyBrSP3gnwmE27gLJYZgMT2GeK3J0+0toN+MnuNYDfHwVGQfLiMZ7KSNSIXHemy905w== + dependencies: + JSONStream "^1.0.3" + assert "^1.4.0" + browser-pack "^6.0.1" + browser-resolve "^2.0.0" + browserify-zlib "~0.2.0" + buffer "~5.2.1" + cached-path-relative "^1.0.0" + concat-stream "^1.6.0" + console-browserify "^1.1.0" + constants-browserify "~1.0.0" + crypto-browserify "^3.0.0" + defined "^1.0.0" + deps-sort "^2.0.1" + domain-browser "^1.2.0" + duplexer2 "~0.1.2" + events "^3.0.0" + glob "^7.1.0" + has "^1.0.0" + htmlescape "^1.1.0" + https-browserify "^1.0.0" + inherits "~2.0.1" + insert-module-globals "^7.2.1" + labeled-stream-splicer "^2.0.0" + mkdirp-classic "^0.5.2" + module-deps "^6.2.3" + os-browserify "~0.3.0" + parents "^1.0.1" + path-browserify "^1.0.0" + process "~0.11.0" + punycode "^1.3.2" + querystring-es3 "~0.2.0" + read-only-stream "^2.0.0" + readable-stream "^2.0.2" + resolve "^1.1.4" + shasum-object "^1.0.0" + shell-quote "^1.6.1" + stream-browserify "^3.0.0" + stream-http "^3.0.0" + string_decoder "^1.1.1" + subarg "^1.0.0" + syntax-error "^1.1.1" + through2 "^2.0.0" + timers-browserify "^1.0.1" + tty-browserify "0.0.1" + url "~0.11.0" + util "~0.12.0" + vm-browserify "^1.0.0" + xtend "^4.0.0" + +browserlist@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/browserlist/-/browserlist-1.0.1.tgz#8c52c4fd5ba49c7464b9e0f85d519598b5833ed9" + integrity sha512-nYq9jiWv+qXcgrJxQzivfEc7Wo2GvAKkeRViE5L3cUJpq4SZO6NZR710I/8T+OjE5BPECbzpm8rpUkwslE3nTg== + dependencies: + chalk "^2.4.1" + +browserslist@^4.12.0, browserslist@^4.16.6, browserslist@^4.16.7: + version "4.16.7" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.7.tgz#108b0d1ef33c4af1b587c54f390e7041178e4335" + integrity sha512-7I4qVwqZltJ7j37wObBe3SoTz+nS8APaNcrBOlgoirb6/HbEU2XxW/LpUDTCngM6iauwFqmRTuOMfyKnFGY5JA== + dependencies: + caniuse-lite "^1.0.30001248" + colorette "^1.2.2" + electron-to-chromium "^1.3.793" + escalade "^3.1.1" + node-releases "^1.1.73" + +budo@^11.5.0: + version "11.7.0" + resolved "https://registry.yarnpkg.com/budo/-/budo-11.7.0.tgz#544a11e424972de9bdcf28102c0f0675f7176648" + integrity sha512-nkulzTNOulJR7PBL+obojWvkSmblxlpSWz6dVsAG2QWyEJmCmtdIUYMpqIEzpFBvR9I/2sW2S0Q1xkDBIQ4pgA== + dependencies: + bole "^2.0.0" + browserify "^16.2.3" + chokidar "^3.5.2" + connect-pushstate "^1.1.0" + escape-html "^1.0.3" + events "^1.0.2" + garnish "^5.0.0" + get-ports "^1.0.2" + inject-lr-script "^2.1.0" + internal-ip "^3.0.1" + micromatch "^3.1.10" + on-finished "^2.3.0" + on-headers "^1.0.1" + once "^1.3.2" + opn "^3.0.2" + path-is-absolute "^1.0.1" + pem "^1.13.2" + reload-css "^1.0.0" + resolve "^1.1.6" + serve-static "^1.10.0" + simple-html-index "^1.4.0" + stacked "^1.1.1" + stdout-stream "^1.4.0" + strip-ansi "^3.0.0" + subarg "^1.0.0" + term-color "^1.0.1" + url-trim "^1.0.0" + watchify-middleware "^1.8.2" + ws "^6.2.2" + xtend "^4.0.0" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + integrity sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ== + +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +buffer@~5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.2.1.tgz#dd57fa0f109ac59c602479044dca7b8b3d0b71d6" + integrity sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + integrity sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +cached-path-relative@^1.0.0, cached-path-relative@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.1.0.tgz#865576dfef39c0d6a7defde794d078f5308e3ef3" + integrity sha512-WF0LihfemtesFcJgO7xfOoOcnWzY/QHR4qeDqV44jPU3HTI54+LnfXK3SA27AVVGCdZFgjjFFaqUA9Jx7dMJZA== + +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +call-me-maybe@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" + integrity sha1-JtII6onje1y95gJQoV8DHBak1ms= + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase-css@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" + integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== + +caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001248: + version "1.0.30001346" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001346.tgz" + integrity sha512-q6ibZUO2t88QCIPayP/euuDREq+aMAxFE5S70PkrLh0iTDj/zEhgvJRKC2+CvXY6EWc6oQwUR48lL5vCW6jiXQ== + +chalk@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.5.1.tgz#663b3a648b68b55d04690d49167aa837858f2174" + integrity sha512-bIKA54hP8iZhyDT81TOsJiQvR1gW+ZYSXFaZUAvoD4wCHdbHY2actmpTE4x344ZlFqHbvoxKOaESULTZN2gstg== + dependencies: + ansi-styles "^1.1.0" + escape-string-regexp "^1.0.0" + has-ansi "^0.1.0" + strip-ansi "^0.3.0" + supports-color "^0.2.0" + +chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +charenc@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA== + +chokidar@^3.4.0, chokidar@^3.5.2: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.3.0, color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@^1.0.0, color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991" + integrity sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE= + dependencies: + color-name "^1.0.0" + +color@^0.11.0: + version "0.11.4" + resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764" + integrity sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q= + dependencies: + clone "^1.0.2" + color-convert "^1.3.0" + color-string "^0.3.0" + +colorette@^1.2.1, colorette@^1.2.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.3.0.tgz#ff45d2f0edb244069d3b772adeb04fed38d0a0af" + integrity sha512-ecORCqbSFP7Wm8Y6lyqMJjexBQqXSF7SSeaTyGGphogUjBlFP9m9o08wy86HL2uB7fMTxtOUzLMk7ogKcxMg1w== + +combine-source-map@^0.8.0, combine-source-map@~0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/combine-source-map/-/combine-source-map-0.8.0.tgz#a58d0df042c186fcf822a8e8015f5450d2d79a8b" + integrity sha512-UlxQ9Vw0b/Bt/KYwCFqdEwsQ1eL8d1gibiFb7lxQJFdvTgc2hIZi6ugsg+kyhzhPV+QEpUiEIwInIAIrgoEkrg== + dependencies: + convert-source-map "~1.1.0" + inline-source-map "~0.6.0" + lodash.memoize "~3.0.3" + source-map "~0.5.3" + +combine-source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/combine-source-map/-/combine-source-map-0.6.1.tgz#9b4a09c316033d768e0f11e029fa2730e079ad96" + integrity sha512-XKRNtuZRlVDTuSGKsfZpXYz80y0XDbYS4a+FzafTgmYHy/ckruFBx7Nd6WaQnFHVI3O6IseWVdXUvZutMpjSkQ== + dependencies: + convert-source-map "~1.1.0" + inline-source-map "~0.5.0" + lodash.memoize "~3.0.3" + source-map "~0.4.2" + +commander@^2.19.0, commander@^2.5.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commoner@^0.10.0: + version "0.10.8" + resolved "https://registry.yarnpkg.com/commoner/-/commoner-0.10.8.tgz#34fc3672cd24393e8bb47e70caa0293811f4f2c5" + integrity sha512-3/qHkNMM6o/KGXHITA14y78PcfmXh4+AOCJpSoF73h4VY1JpdGv3CHMS5+JW6SwLhfJt4RhNmLAa7+RRX/62EQ== + dependencies: + commander "^2.5.0" + detective "^4.3.1" + glob "^5.0.15" + graceful-fs "^4.1.2" + iconv-lite "^0.4.5" + mkdirp "^0.5.0" + private "^0.1.6" + q "^1.1.2" + recast "^0.11.17" + +component-emitter@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +concat-stream@^1.5.0, concat-stream@^1.6.0, concat-stream@^1.6.1, concat-stream@~1.6.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +connect-pushstate@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/connect-pushstate/-/connect-pushstate-1.1.0.tgz#bcab224271c439604a0fb0f614c0a5f563e88e24" + integrity sha512-5p2H2+eXkCiqcSZqZbTh5TLcLsl1wub7VKrRnfHyorC+pxXjF6nfswda4YMluYhGMo+33eR/58weorzSi9uSbA== + +console-browserify@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" + integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== + +constants-browserify@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + integrity sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +convert-source-map@^1.5.1, convert-source-map@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" + integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== + dependencies: + safe-buffer "~5.1.1" + +convert-source-map@~1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860" + integrity sha512-Y8L5rp6jo+g9VEPgvqNfEopjTR4OTYct8lXlS8iVQdmnjDvbdbzYe9rjtFCB9egC86JoNCU61WRY+ScjkZpnIg== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= + +core-js-compat@^3.14.0, core-js-compat@^3.16.0: + version "3.16.1" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.16.1.tgz#c44b7caa2dcb94b673a98f27eee1c8312f55bc2d" + integrity sha512-NHXQXvRbd4nxp9TEmooTJLUf94ySUG6+DSsscBpTftN1lQLQ4LjnWvc7AoIo4UjDsFF3hB8Uh5LLCRRdaiT5MQ== + dependencies: + browserslist "^4.16.7" + semver "7.0.0" + +"core-util-is@>=1.0.1 <1.1.0-0", core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +create-ecdh@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" + integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== + dependencies: + bn.js "^4.1.0" + elliptic "^6.5.3" + +create-error@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/create-error/-/create-error-0.3.1.tgz#69810245a629e654432bf04377360003a5351a23" + integrity sha1-aYECRaYp5lRDK/BDdzYAA6U1GiM= + +create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" + integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + md5.js "^1.3.4" + ripemd160 "^2.0.1" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" + integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +cross-spawn@^6.0.0: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +cross-spawn@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +crypt@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== + +crypto-browserify@^3.0.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" + integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + randomfill "^1.0.3" + +css-color-function@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/css-color-function/-/css-color-function-1.3.3.tgz#8ed24c2c0205073339fafa004bc8c141fccb282e" + integrity sha1-jtJMLAIFBzM5+voAS8jBQfzLKC4= + dependencies: + balanced-match "0.1.0" + color "^0.11.0" + debug "^3.1.0" + rgb "~0.1.0" + +css-extract@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/css-extract/-/css-extract-2.0.0.tgz#8e9769a95e0dac8ddfc052cfe899c569e4aa17fe" + integrity sha512-foKNaeU6/6xJRRQx0qyXVndANsM2MT6y9O+hmF3lcnqkVO7awaDDj/hPQWdFlEMF0hPkLIDWPEWtV8S4Ep2Y9g== + dependencies: + bl "^4.0.2" + defined "^1.0.0" + from2-string "^1.1.0" + static-module "^3.0.0" + through2 "^4.0.2" + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +d@1, d@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" + integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== + dependencies: + es5-ext "^0.10.50" + type "^1.0.1" + +dash-ast@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dash-ast/-/dash-ast-1.0.0.tgz#12029ba5fb2f8aa6f0a861795b23c1b4b6c27d37" + integrity sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA== + +debounce@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" + integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== + +debug@2.6.9, debug@^2.2.0, debug@^2.3.3: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^3.1.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" + integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== + dependencies: + ms "2.1.2" + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + +deep-is@^0.1.3, deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + +default-gateway@^2.6.0: + version "2.7.2" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-2.7.2.tgz#b7ef339e5e024b045467af403d50348db4642d0f" + integrity sha512-lAc4i9QJR0YHSDFdzeBQKfZ1SRDG3hsJNEkrpcZa8QhBfidLAilT60BDEIVUUGqosFp425KOgB3uYqcnQrWafQ== + dependencies: + execa "^0.10.0" + ip-regex "^2.1.0" + +default-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/default-value/-/default-value-1.0.0.tgz#8c6f52a5a1193fe78fdc9f86eb71d16c9757c83a" + integrity sha1-jG9SpaEZP+eP3J+G63HRbJdXyDo= + dependencies: + es6-promise-try "0.0.1" + +define-properties@^1.1.3, define-properties@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" + integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== + dependencies: + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + +defined@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/defined/-/defined-0.0.0.tgz#f35eea7d705e933baf13b2f03b3f83d921403b3e" + integrity sha512-zpqiCT8bODLu3QSmLLic8xJnYWBFjOSu/fBCm189oAiTtPq/PSanNACKZDS7kgSyCJY7P+IcODzlIogBK/9RBg== + +defined@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" + integrity sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +deps-sort@^2.0.0, deps-sort@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/deps-sort/-/deps-sort-2.0.1.tgz#9dfdc876d2bcec3386b6829ac52162cda9fa208d" + integrity sha512-1orqXQr5po+3KI6kQb9A4jnXT1PBwggGl2d7Sq2xsnOeI9GPcE/tGcF9UiSZtZBM7MukY4cAh7MemS6tZYipfw== + dependencies: + JSONStream "^1.0.3" + shasum-object "^1.0.0" + subarg "^1.0.0" + through2 "^2.0.0" + +deps-topo-sort@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/deps-topo-sort/-/deps-topo-sort-0.2.1.tgz#4be8af07476971649c8b0c484fd7d4a87b8b012a" + integrity sha512-I0M1G/coBKGpA6OlrZyHKyq6d2jOQtm7HmjkJwLhlmZBgw17dUA9zf0mb5IowrhaJAAkEs37v3YDb9JKC7OH9w== + dependencies: + JSONStream "~0.7.1" + minimist "0.0.5" + through "~2.3.4" + +des.js@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" + integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detective@^4.3.1: + version "4.7.1" + resolved "https://registry.yarnpkg.com/detective/-/detective-4.7.1.tgz#0eca7314338442febb6d65da54c10bb1c82b246e" + integrity sha512-H6PmeeUcZloWtdt4DAkFyzFL94arpHr3NOwwmVILFiy+9Qd4JTxxXrzfyGk/lmct2qVGBwTSwSXagqu2BxmWig== + dependencies: + acorn "^5.2.1" + defined "^1.0.0" + +detective@^5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.1.tgz#6af01eeda11015acb0e73f933242b70f24f91034" + integrity sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw== + dependencies: + acorn-node "^1.8.2" + defined "^1.0.0" + minimist "^1.2.6" + +diffie-hellman@^5.0.0: + version "5.0.3" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" + integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + +dir-glob@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034" + integrity sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag== + dependencies: + arrify "^1.0.1" + path-type "^3.0.0" + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +domain-browser@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" + integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== + +duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2, duplexer2@~0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + integrity sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA== + dependencies: + readable-stream "^2.0.2" + +duplexer@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" + integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +electron-to-chromium@^1.3.793: + version "1.3.803" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.803.tgz#78993a991d096500f21a77e91cd2a44295fe3cbe" + integrity sha512-tmRK9qB8Zs8eLMtTBp+w2zVS9MUe62gQQQHjYdAc5Zljam3ZIokNb+vZLPRz9RCREp6EFRwyhOFwbt1fEriQ2Q== + +elliptic@^6.5.3: + version "6.5.4" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +enquirer@^2.3.5: + version "2.3.6" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" + integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + dependencies: + ansi-colors "^4.1.1" + +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2: + version "1.18.5" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.5.tgz#9b10de7d4c206a3581fd5b2124233e04db49ae19" + integrity sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + get-intrinsic "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.2" + internal-slot "^1.0.3" + is-callable "^1.2.3" + is-negative-zero "^2.0.1" + is-regex "^1.1.3" + is-string "^1.0.6" + object-inspect "^1.11.0" + object-keys "^1.1.1" + object.assign "^4.1.2" + string.prototype.trimend "^1.0.4" + string.prototype.trimstart "^1.0.4" + unbox-primitive "^1.0.1" + +es-abstract@^1.19.0, es-abstract@^1.19.5, es-abstract@^1.20.0: + version "1.20.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814" + integrity sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.1.1" + get-symbol-description "^1.0.0" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-symbols "^1.0.3" + internal-slot "^1.0.3" + is-callable "^1.2.4" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-weakref "^1.0.2" + object-inspect "^1.12.0" + object-keys "^1.1.1" + object.assign "^4.1.2" + regexp.prototype.flags "^1.4.3" + string.prototype.trimend "^1.0.5" + string.prototype.trimstart "^1.0.5" + unbox-primitive "^1.0.2" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +es5-ext@^0.10.35, es5-ext@^0.10.50, es5-ext@~0.10.14: + version "0.10.53" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1" + integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q== + dependencies: + es6-iterator "~2.0.3" + es6-symbol "~3.1.3" + next-tick "~1.0.0" + +es6-iterator@~2.0.1, es6-iterator@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-map@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0" + integrity sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA= + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-set "~0.1.5" + es6-symbol "~3.1.1" + event-emitter "~0.3.5" + +es6-promise-try@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/es6-promise-try/-/es6-promise-try-0.0.1.tgz#10f140dad27459cef949973e5d21a087f7274b20" + integrity sha1-EPFA2tJ0Wc75SZc+XSGgh/cnSyA= + +es6-promisify@^6.0.0: + version "6.1.1" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-6.1.1.tgz#46837651b7b06bf6fff893d03f29393668d01621" + integrity sha512-HBL8I3mIki5C1Cc9QjKUenHtnG0A5/xA8Q/AllRcfiwl2CZFXGK7ddBiCoRwAix4i2KxcQfjtIVcrVbB3vbmwg== + +es6-set@^0.1.5, es6-set@~0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1" + integrity sha1-0rPsXU2ADO2BjbU40ol02wpzzLE= + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-symbol "3.1.1" + event-emitter "~0.3.5" + +es6-symbol@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" + integrity sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc= + dependencies: + d "1" + es5-ext "~0.10.14" + +es6-symbol@^3.1.1, es6-symbol@~3.1.1, es6-symbol@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" + integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== + dependencies: + d "^1.0.1" + ext "^1.1.2" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-html@^1.0.3, escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +escodegen@^1.11.1: + version "1.14.3" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" + integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== + dependencies: + esprima "^4.0.1" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + +eslint-plugin-react-hooks@^2.0.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.5.1.tgz#4ef5930592588ce171abeb26f400c7fbcbc23cd0" + integrity sha512-Y2c4b55R+6ZzwtTppKwSmK/Kar8AdLiC2f9NADCuxbcTgPPg41Gyqa6b9GppgXSvCtkRw43ZE86CT5sejKC6/g== + +eslint-plugin-react-hooks@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz#8c229c268d468956334c943bb45fc860280f5556" + integrity sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ== + +eslint-plugin-react@^7.14.3, eslint-plugin-react@^7.24.0: + version "7.24.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.24.0.tgz#eadedfa351a6f36b490aa17f4fa9b14e842b9eb4" + integrity sha512-KJJIx2SYx7PBx3ONe/mEeMz4YE0Lcr7feJTCMyyKb/341NcjuAgim3Acgan89GfPv7nxXK2+0slu0CWXYM4x+Q== + dependencies: + array-includes "^3.1.3" + array.prototype.flatmap "^1.2.4" + doctrine "^2.1.0" + has "^1.0.3" + jsx-ast-utils "^2.4.1 || ^3.0.0" + minimatch "^3.0.4" + object.entries "^1.1.4" + object.fromentries "^2.0.4" + object.values "^1.1.4" + prop-types "^15.7.2" + resolve "^2.0.0-next.3" + string.prototype.matchall "^4.0.5" + +eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" + integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== + dependencies: + eslint-visitor-keys "^1.1.0" + +eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== + +eslint-visitor-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + +eslint@^7.30.0: + version "7.32.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" + integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== + dependencies: + "@babel/code-frame" "7.12.11" + "@eslint/eslintrc" "^0.4.3" + "@humanwhocodes/config-array" "^0.5.0" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.0.1" + doctrine "^3.0.0" + enquirer "^2.3.5" + escape-string-regexp "^4.0.0" + eslint-scope "^5.1.1" + eslint-utils "^2.1.0" + eslint-visitor-keys "^2.0.0" + espree "^7.3.1" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^5.1.2" + globals "^13.6.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.0.4" + natural-compare "^1.4.0" + optionator "^0.9.1" + progress "^2.0.0" + regexpp "^3.1.0" + semver "^7.2.1" + strip-ansi "^6.0.0" + strip-json-comments "^3.1.0" + table "^6.0.9" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^7.3.0, espree@^7.3.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" + integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== + dependencies: + acorn "^7.4.0" + acorn-jsx "^5.3.1" + eslint-visitor-keys "^1.3.0" + +esprima-fb@13001.1001.0-dev-harmony-fb: + version "13001.1001.0-dev-harmony-fb" + resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-13001.1001.0-dev-harmony-fb.tgz#633acdb40d9bd4db8a1c1d68c06a942959fad2b0" + integrity sha512-u0PLCs9J36198vK7lFdvzfOiMT2v2K9/9d+J2M4d1ZEfTsXzvrzRHh95D+/sIziSabl4b6QKJOTn8+VaWc/B4A== + +esprima@^4.0.0, esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esprima@~3.1.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" + integrity sha512-AWwVMNxwhN8+NIPQzAQZCm7RkLC4RbM3B1OobMuyp3i+w73X57KCKaVIxaRZb+DYCojq7rspo+fmuQfAboyhFg== + +esquery@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1, estraverse@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" + integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== + +estree-is-function@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/estree-is-function/-/estree-is-function-1.0.0.tgz#c0adc29806d7f18a74db7df0f3b2666702e37ad2" + integrity sha512-nSCWn1jkSq2QAtkaVLJZY2ezwcFO161HVc174zL1KPW3RJ+O6C3eJb8Nx7OXzvhoEv+nLgSR1g71oWUHUDTrJA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +event-emitter@~0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + integrity sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk= + dependencies: + d "1" + es5-ext "~0.10.14" + +events@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + integrity sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw== + +events@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/events/-/events-2.1.0.tgz#2a9a1e18e6106e0e812aa9ebd4a819b3c29c0ba5" + integrity sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg== + +events@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + +execa@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50" + integrity sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw== + dependencies: + cross-spawn "^6.0.0" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +express@^4.18.1: + version "4.18.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf" + integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.0" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.10.3" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +ext@^1.1.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244" + integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A== + dependencies: + type "^2.0.0" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +factor-bundle@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/factor-bundle/-/factor-bundle-2.5.0.tgz#8ea8957da39d7586283cc3ee353cd9911a45e779" + integrity sha512-yliJvbnY+/CUpAkEvQa/33yfkGNseUXiKuiSPbA7n9r0IYlz/RVTdJRMHxvOUDV3gFbfiq/3Wlw1hmf6+UrULA== + dependencies: + JSONStream "~0.8.4" + browser-pack "^5.0.1" + defined "0.0.0" + deps-topo-sort "~0.2.1" + inherits "^2.0.1" + isarray "0.0.1" + labeled-stream-splicer "^1.0.0" + minimist "~0.2.0" + nub "0.0.0" + outpipe "^1.1.0" + reversepoint "~0.2.0" + stream-combiner "~0.2.1" + through2 "^0.5.1" + xtend "^4.0.0" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^2.0.2: + version "2.2.7" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d" + integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw== + dependencies: + "@mrmlnc/readdir-enhanced" "^2.2.1" + "@nodelib/fs.stat" "^1.1.2" + glob-parent "^3.1.0" + is-glob "^4.0.0" + merge2 "^1.2.3" + micromatch "^3.1.10" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +fast-safe-stringify@^2.0.7: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatted@^3.1.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561" + integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA== + +flatten@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b" + integrity sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg== + +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + +for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= + dependencies: + map-cache "^0.2.2" + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +from2-string@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/from2-string/-/from2-string-1.1.0.tgz#18282b27d08a267cb3030cd2b8b4b0f212af752a" + integrity sha512-m8vCh+KnXXXBtfF2VUbiYlQ+nczLcntB0BrtNgpmLkHylhObe9WF1b2LZjBBzrZzA6P4mkEla6ZYQoOUTG8cYA== + dependencies: + from2 "^2.0.3" + +from2@^2.0.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +function.prototype.name@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" + integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + functions-have-names "^1.2.2" + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + +functions-have-names@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +garnish@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/garnish/-/garnish-5.2.0.tgz#bed43659382e4b198e33c793897be7c701e65577" + integrity sha512-y0qv1q5ylEtbKW08LGDxmhrmyHGIXH2Jfcz3JPKFikMPDQ0mgBIbtOc2R3fXHYOXOfDfDpx1o5G9rMl4jpU0qA== + dependencies: + chalk "^0.5.1" + minimist "^1.1.0" + pad-left "^2.0.0" + pad-right "^0.2.2" + prettier-bytes "^1.0.3" + pretty-ms "^2.1.0" + right-now "^1.0.0" + split2 "^0.2.1" + stdout-stream "^1.4.0" + url-trim "^1.0.0" + +generic-names@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-2.0.1.tgz#f8a378ead2ccaa7a34f0317b05554832ae41b872" + integrity sha512-kPCHWa1m9wGG/OwQpeweTwM/PYiQLrUIxXbt/P4Nic3LbGjCP0YwrALHW1uNLKZ0LIMg+RF+XRlj2ekT9ZlZAQ== + dependencies: + loader-utils "^1.1.0" + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-assigned-identifiers@^1.1.0, get-assigned-identifiers@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz#6dbf411de648cbaf8d9169ebb0d2d576191e2ff1" + integrity sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ== + +get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" + integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + +get-ports@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/get-ports/-/get-ports-1.0.3.tgz#f40bd580aca7ec0efb7b96cbfcbeb03ef894b5e8" + integrity sha512-XtNFp93OT2wNEX/PkcCJ5+4PR5fxYCK+J2BsfJO8eV7hCYbqROt+8XO6iApJqJ06A2UJMUueDCoJ1Lp5vypuDw== + dependencies: + map-limit "0.0.1" + +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + integrity sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ== + +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= + +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-to-regexp@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" + integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= + +glob@^5.0.15: + version "5.0.15" + resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" + integrity sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA== + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.1.0: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.1.2, glob@^7.1.3: + version "7.1.7" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" + integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^13.6.0, globals@^13.9.0: + version "13.10.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.10.0.tgz#60ba56c3ac2ca845cfbf4faeca727ad9dd204676" + integrity sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g== + dependencies: + type-fest "^0.20.2" + +globby@^8.0.1: + version "8.0.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.2.tgz#5697619ccd95c5275dbb2d6faa42087c1a941d8d" + integrity sha512-yTzMmKygLp8RUpG1Ymu2VXPSJQZjNAZPD4ywgYEaG7e4tBJeUQBO8OpXrf1RCNcEs5alsoJYPAMiIHP0cmeC7w== + dependencies: + array-union "^1.0.1" + dir-glob "2.0.0" + fast-glob "^2.0.2" + glob "^7.1.2" + ignore "^3.3.5" + pify "^3.0.0" + slash "^1.0.0" + +graceful-fs@^4.1.2: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +has-ansi@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-0.1.0.tgz#84f265aae8c0e6a88a12d7022894b7568894c62e" + integrity sha512-1YsTg1fk2/6JToQhtZkArMkurq8UoWU1Qe0aR3VUHjgij4nOylSWLWAtBXoZ4/dXOmugfLGm1c+QhuD0JyedFA== + dependencies: + ansi-regex "^0.2.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= + dependencies: + ansi-regex "^2.0.0" + +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + +has-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" + integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo= + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" + integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + dependencies: + get-intrinsic "^1.1.1" + +has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has@^1.0.0, has@^1.0.1, has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hash-base@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" + integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== + dependencies: + inherits "^2.0.4" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + +hmac-drbg@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +htmlescape@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351" + integrity sha512-eVcrzgbR4tim7c7soKQKtxa/kQM4TzjnlU83rcZ9bHU6t31ehfV7SktN6McWgwPWg+JYMA/O3qpGxBvFq1z2Jg== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +https-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" + integrity sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg== + +iconv-lite@0.4.24, iconv-lite@^0.4.5: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +icss-utils@^4.0.0, icss-utils@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" + integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA== + dependencies: + postcss "^7.0.14" + +icssify@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/icssify/-/icssify-1.2.1.tgz#d322849021965449d566203d0350972dc148a656" + integrity sha512-E7Fogd4OQnJhCMu2U7/BcmLAdUwlQ6izPaLU3KDK/PKrzzlnOuBWJkTS9dsBiQ8RTKX6BIGX3V0qzx/qBScnnQ== + dependencies: + assure-array "^1.0.0" + bl "^4.0.0" + bluebird "^3.7.1" + default-value "^1.0.0" + generic-names "^2.0.1" + insert-css "^2.0.0" + object.fromentries "^2.0.1" + postcss "^6.0.9" + postcss-modules-extract-imports "^2.0.0" + postcss-modules-local-by-default "^3.0.2" + postcss-modules-scope "^2.1.0" + postcss-modules-values "^3.0.0" + through2 "^2.0.3" + validatem "^0.2.0" + +ieee754@^1.1.13, ieee754@^1.1.4: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore@^3.3.5: + version "3.3.10" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" + integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== + +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + +import-fresh@^3.0.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +indexes-of@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" + integrity sha512-bup+4tap3Hympa+JBJUG7XuOsdNQ6fxt0MHyXMKuLBKn0OqsTfvUxkUrroEX1+B2VsSHvCjiIcZVxRtYa4nllA== + +indexof@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + integrity sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg== + +"individual@>=3.0.0 <3.1.0-0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/individual/-/individual-3.0.0.tgz#e7ca4f85f8957b018734f285750dc22ec2f9862d" + integrity sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + integrity sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== + +inject-lr-script@^2.1.0, inject-lr-script@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/inject-lr-script/-/inject-lr-script-2.2.0.tgz#58d91cd99e5de1a3f172aa076f7db8651ee72db2" + integrity sha512-lFLjCOg2XP8233AiET5vFePo910vhNIkKHDzUptNhc+4Y7dsp/TNBiusUUpaxzaGd6UDHy0Lozfl9AwmteK6DQ== + dependencies: + resp-modifier "^6.0.0" + +inline-source-map@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/inline-source-map/-/inline-source-map-0.5.0.tgz#4a4c5dd8e4fb5e9b3cda60c822dfadcaee66e0af" + integrity sha512-2WtHG0qX9OH9TVcxsLVfq3Tzr+qtL6PtWgoh0XAAKe4KkdA/57Q+OGJuRJHA4mZ2OZnkJ/ZAaXf9krLB12/nIg== + dependencies: + source-map "~0.4.0" + +inline-source-map@~0.6.0: + version "0.6.2" + resolved "https://registry.yarnpkg.com/inline-source-map/-/inline-source-map-0.6.2.tgz#f9393471c18a79d1724f863fa38b586370ade2a5" + integrity sha512-0mVWSSbNDvedDWIN4wxLsdPM4a7cIPcpyMxj3QZ406QRwQ6ePGB1YIHxVPjqpcUGbWQ5C+nHTwGNWAGvt7ggVA== + dependencies: + source-map "~0.5.3" + +insert-css@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/insert-css/-/insert-css-2.0.0.tgz#eb5d1097b7542f4c79ea3060d3aee07d053880f4" + integrity sha1-610Ql7dUL0x56jBg067gfQU4gPQ= + +insert-module-globals@^7.0.0, insert-module-globals@^7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/insert-module-globals/-/insert-module-globals-7.2.1.tgz#d5e33185181a4e1f33b15f7bf100ee91890d5cb3" + integrity sha512-ufS5Qq9RZN+Bu899eA9QCAYThY+gGW7oRkmb0vC93Vlyu/CFGcH0OYPEjVkDXA5FEbTt1+VWzdoOD3Ny9N+8tg== + dependencies: + JSONStream "^1.0.3" + acorn-node "^1.5.2" + combine-source-map "^0.8.0" + concat-stream "^1.6.1" + is-buffer "^1.1.0" + path-is-absolute "^1.0.1" + process "~0.11.0" + through2 "^2.0.0" + undeclared-identifiers "^1.1.2" + xtend "^4.0.0" + +internal-ip@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-3.0.1.tgz#df5c99876e1d2eb2ea2d74f520e3f669a00ece27" + integrity sha512-NXXgESC2nNVtU+pqmC9e6R8B1GpKxzsAQhffvh5AL79qKnodd+L7tnEQmTiUAVngqLalPbSqRA7XGIEL5nCd0Q== + dependencies: + default-gateway "^2.6.0" + ipaddr.js "^1.5.2" + +internal-slot@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" + integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== + dependencies: + get-intrinsic "^1.1.0" + has "^1.0.3" + side-channel "^1.0.4" + +ip-regex@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" + integrity sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw== + +ipaddr.js@1.9.1, ipaddr.js@^1.5.2: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== + dependencies: + kind-of "^6.0.0" + +is-arguments@^1.0.4: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-buffer@^1.1.0, is-buffer@^1.1.5, is-buffer@~1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.3, is-callable@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" + integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== + +is-core-module@^2.2.0, is-core-module@^2.8.1: + version "2.9.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69" + integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== + dependencies: + has "^1.0.3" + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== + dependencies: + kind-of "^6.0.0" + +is-date-object@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^2.1.0, is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-finite@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" + integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-function@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" + +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= + dependencies: + is-extglob "^2.1.0" + +is-glob@^4.0.0, is-glob@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-negative-zero@^2.0.1, is-negative-zero@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= + dependencies: + kind-of "^3.0.2" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-regex@^1.1.3, is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-shared-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== + dependencies: + call-bind "^1.0.2" + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-string@^1.0.5, is-string@^1.0.6, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-typed-array@^1.1.3, is-typed-array@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.9.tgz#246d77d2871e7d9f5aeb1d54b9f52c71329ece67" + integrity sha512-kfrlnTTn8pZkfpJMUgYD7YZ3qzeJgWUn8XfVYBARc4wnmNOmLbmuuaAs3q5fvB0UJOn6yHAKaGTPM7d6ezoD/A== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-abstract "^1.20.0" + for-each "^0.3.3" + has-tostringtag "^1.0.0" + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +isarray@0.0.1, isarray@~0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== + +isarray@1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + +js-base64@^2.1.9: + version "2.6.4" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4" + integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ== + +js-file-download@^0.4.12: + version "0.4.12" + resolved "https://registry.yarnpkg.com/js-file-download/-/js-file-download-0.4.12.tgz#10c70ef362559a5b23cdbdc3bd6f399c3d91d821" + integrity sha512-rML+NkoD08p5Dllpjo0ffy4jRHeY6Zsapvr/W86N7E0yuzAO6qa5X9+xog6zQNlH102J7IXljNY2FtS6Lj3ucg== + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + +json-stable-stringify@~0.0.0: + version "0.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz#611c23e814db375527df851193db59dd2af27f45" + integrity sha512-nKtD/Qxm7tWdZqJoldEC7fF0S41v0mWbeaXG3637stOWfyGxTgWTYE2wtfKmjzpvxv2MA2xzxsXOIiwUpkX6Qw== + dependencies: + jsonify "~0.0.0" + +"json-stringify-safe@>=5.0.0 <5.1.0-0": + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== + +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + +json5@^2.1.2: + version "2.2.0" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" + integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== + dependencies: + minimist "^1.2.5" + +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + integrity sha512-trvBk1ki43VZptdBI5rIlG4YOzyeH/WefQt5rj1grasPn4iiZWKet8nkgc4GlsAylaztn0qZfUYOiTsASJFdNA== + +jsonparse@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-0.0.5.tgz#330542ad3f0a654665b778f3eb2d9a9fa507ac64" + integrity sha512-fw7Q/8gFR8iSekUi9I+HqWIap6mywuoe7hQIg3buTVjuZgALKj4HAmm0X6f+TaL4c9NJbvyFQdaI2ppr5p6dnQ== + +jsonparse@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" + integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== + +jstransform@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/jstransform/-/jstransform-10.1.0.tgz#b4c49bf63f162c108b0348399a8737c713b0a83a" + integrity sha512-hzsCrPlH8ASlARV/sjsjbnvg0AXz9DxMHry44wXF3GTvletHT8UhsmqUzSGaImlney40E1gw4D6izUzifD15IQ== + dependencies: + base62 "0.1.1" + esprima-fb "13001.1001.0-dev-harmony-fb" + source-map "0.1.31" + +"jsx-ast-utils@^2.4.1 || ^3.0.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz#41108d2cec408c3453c1bbe8a4aae9e1e2bd8f82" + integrity sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q== + dependencies: + array-includes "^3.1.2" + object.assign "^4.1.2" + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +labeled-stream-splicer@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/labeled-stream-splicer/-/labeled-stream-splicer-1.0.2.tgz#4615331537784981e8fd264e1f3a434c4e0ddd65" + integrity sha512-3KBjPRnXrYC5h2jEf/d6hO7Lcl+38QzRVTOyHA2sFzZVMYwsUFuejlrOMwAjmz13hVBr9ruDS1RwE4YEz8P58w== + dependencies: + inherits "^2.0.1" + isarray "~0.0.1" + stream-splicer "^1.1.0" + +labeled-stream-splicer@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/labeled-stream-splicer/-/labeled-stream-splicer-2.0.2.tgz#42a41a16abcd46fd046306cf4f2c3576fffb1c21" + integrity sha512-Ca4LSXFFZUjPScRaqOcFxneA0VpKZr4MMYCljyQr4LIewTLb3Y0IUTIsnBBsVubIeEfxeSZpSjSsRM8APEQaAw== + dependencies: + inherits "^2.0.1" + stream-splicer "^2.0.0" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +loader-utils@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" + integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^1.0.1" + +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= + +lodash.memoize@~3.0.3: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f" + integrity sha512-eDn9kqrAmVUC1wmZvlQ6Uhde44n+tXpqPrN8olQJbttgh0oKclk+SF54P47VEGE9CEiMeRwAP8BaM7UHvBkz2A== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.truncate@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" + integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= + +loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +magic-string@0.25.1: + version "0.25.1" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.1.tgz#b1c248b399cd7485da0fe7385c2fc7011843266e" + integrity sha512-sCuTz6pYom8Rlt4ISPFn6wuFodbKMIHUMv4Qko9P17dpxb7s52KJTmRuZZqHdGmLCK9AOcDare039nRIcfdkEg== + dependencies: + sourcemap-codec "^1.4.1" + +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= + +map-limit@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/map-limit/-/map-limit-0.0.1.tgz#eb7961031c0f0e8d001bf2d56fab685d58822f38" + integrity sha512-pJpcfLPnIF/Sk3taPW21G/RQsEEirGaFpCW3oXRwH9dnFHPHNGjNyvh++rdmC2fNqEaTw2MhYJraoJWAHx8kEg== + dependencies: + once "~1.3.0" + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= + dependencies: + object-visit "^1.0.0" + +md5.js@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" + integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +md5@^2.2.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" + integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g== + dependencies: + charenc "0.0.2" + crypt "0.0.2" + is-buffer "~1.1.6" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-source-map@1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.0.4.tgz#a5de46538dae84d4114cc5ea02b4772a6346701f" + integrity sha1-pd5GU42uhNQRTMXqArR3KmNGcB8= + dependencies: + source-map "^0.5.6" + +merge2@^1.2.3: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^3.1.10: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +miller-rabin@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" + integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== + +"minimatch@2 || 3", minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.5.tgz#d7aa327bcecf518f9106ac6b8f003fa3bcea8566" + integrity sha512-rSJ0cdmCj3qmKdObcnMcWgPVOyaOWlazLhZAJW0s6G6lx1ZEuFkraWmEH5LTvX90btkfHPclQBjvjU7A/kYRFg== + +minimist@^1.1.0, minimist@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + +minimist@^1.2.0, minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +minimist@~0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.2.1.tgz#827ba4e7593464e7c221e8c5bed930904ee2c455" + integrity sha512-GY8fANSrTMfBVfInqJAY41QkOM+upUTytK1jZ0c8+3HdHrJxBJ3rF5i9moClXTE8uUSnUo8cAsCoxDXvSY4DHg== + +mixin-deep@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mkdirp-classic@^0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + +mkdirp@^0.5.0: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +module-deps@^6.2.3: + version "6.2.3" + resolved "https://registry.yarnpkg.com/module-deps/-/module-deps-6.2.3.tgz#15490bc02af4b56cf62299c7c17cba32d71a96ee" + integrity sha512-fg7OZaQBcL4/L+AK5f4iVqf9OMbCclXfy/znXRxTVhJSeW5AIlS9AwheYwDaXM3lVW7OBeaeUEY3gbaC6cLlSA== + dependencies: + JSONStream "^1.0.3" + browser-resolve "^2.0.0" + cached-path-relative "^1.0.2" + concat-stream "~1.6.0" + defined "^1.0.0" + detective "^5.2.0" + duplexer2 "^0.1.2" + inherits "^2.0.1" + parents "^1.0.0" + readable-stream "^2.0.2" + resolve "^1.4.0" + stream-combiner2 "^1.1.1" + subarg "^1.0.0" + through2 "^2.0.0" + xtend "^4.0.0" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3, ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@^3.1.23: + version "3.1.25" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.25.tgz#09ca32747c0e543f0e1814b7d3793477f9c8e152" + integrity sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q== + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +next-tick@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" + integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +node-releases@^1.1.73: + version "1.1.74" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.74.tgz#e5866488080ebaa70a93b91144ccde06f3c3463e" + integrity sha512-caJBVempXZPepZoZAPCWRTNxYQ+xtG/KAi4ozTA5A+nJ7IU+kLQCbqaUjb5Rwy14M9upBWiQ4NutcmW04LJSRw== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw== + dependencies: + path-key "^2.0.0" + +nub@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/nub/-/nub-0.0.0.tgz#b369bd32bdde66af59605c3b0520bc219dccc04f" + integrity sha512-dK0Ss9C34R/vV0FfYJXuqDAqHlaW9fvWVufq9MmGF2umCuDbd5GRfRD9fpi/LiM0l4ZXf8IBB+RYmZExqCrf0w== + +num2fraction@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" + integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4= + +object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-inspect@^1.11.0, object-inspect@^1.12.0, object-inspect@^1.9.0: + version "1.12.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" + integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== + +object-inspect@^1.6.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1" + integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg== + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= + dependencies: + isobject "^3.0.0" + +object.assign@^4.1.0, object.assign@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" + integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + has-symbols "^1.0.1" + object-keys "^1.1.1" + +object.entries@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.4.tgz#43ccf9a50bc5fd5b649d45ab1a579f24e088cafd" + integrity sha512-h4LWKWE+wKQGhtMjZEBud7uLGhqyLwj8fpHOarZhD2uY3C9cRtk57VQ89ke3moByLXMedqs3XCHzyb4AmA2DjA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.18.2" + +object.fromentries@^2.0.1, object.fromentries@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.4.tgz#26e1ba5c4571c5c6f0890cef4473066456a120b8" + integrity sha512-EsFBshs5RUUpQEY1D4q/m59kMfz4YJvxuNCJcv/jWwOJr34EaVnG11ZrZa0UHB3wnzV1wx8m58T4hQL8IuNXlQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.2" + has "^1.0.3" + +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= + dependencies: + isobject "^3.0.1" + +object.values@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.4.tgz#0d273762833e816b693a637d30073e7051535b30" + integrity sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.18.2" + +on-finished@2.4.1, on-finished@^2.3.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-headers@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0, once@^1.3.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +once@~1.3.0: + version "1.3.3" + resolved "https://registry.yarnpkg.com/once/-/once-1.3.3.tgz#b2e261557ce4c314ec8304f3fa82663e4297ca20" + integrity sha512-6vaNInhu+CHxtONf3zw3vq4SP2DOQhjBvIa3rNcG0+P7eKWlYH6Peu7rHizSloRU2EwMz6GraLieis9Ac9+p1w== + dependencies: + wrappy "1" + +opn@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/opn/-/opn-3.0.3.tgz#b6d99e7399f78d65c3baaffef1fb288e9b85243a" + integrity sha512-YKyQo/aDk+kLY/ChqYx3DMWW8cbxvZDh+7op1oU60TmLHGWFrn2gPaRWihzDhSwCarAESa9G8dNXzjTGfLx8FQ== + dependencies: + object-assign "^4.0.1" + +optionator@^0.8.1: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + +os-browserify@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" + integrity sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A== + +os-tmpdir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== + +outpipe@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/outpipe/-/outpipe-1.1.1.tgz#50cf8616365e87e031e29a5ec9339a3da4725fa2" + integrity sha512-BnNY/RwnDrkmQdUa9U+OfN/Y7AWmKuUPCCd+hbRclZnnANvYpO72zp/a6Q4n829hPbdqEac31XCcsvlEvb+rtA== + dependencies: + shell-quote "^1.4.2" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== + +pad-left@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pad-left/-/pad-left-2.1.0.tgz#16e6a3b2d44a8e138cb0838cc7cb403a4fc9e994" + integrity sha512-HJxs9K9AztdIQIAIa/OIazRAUW/L6B9hbQDxO4X07roW3eo9XqZc2ur9bn1StH9CnbbI9EgvejHQX7CBpCF1QA== + dependencies: + repeat-string "^1.5.4" + +pad-right@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/pad-right/-/pad-right-0.2.2.tgz#6fbc924045d244f2a2a244503060d3bfc6009774" + integrity sha512-4cy8M95ioIGolCoMmm2cMntGR1lPLEbOMzOKu8bzjuJP6JpzEMQcDHmh7hHLYGgob+nKe1YHFMaG4V59HQa89g== + dependencies: + repeat-string "^1.5.2" + +pako@~1.0.5: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parents@^1.0.0, parents@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parents/-/parents-1.0.1.tgz#fedd4d2bf193a77745fe71e371d73c3307d9c751" + integrity sha512-mXKF3xkoUt5td2DoxpLmtOmZvko9VfFpwRwkKDHSNvgmpLAeBo18YDhcPbBzJq+QLCHMbGOfzia2cX4U+0v9Mg== + dependencies: + path-platform "~0.11.15" + +parse-asn1@^5.0.0, parse-asn1@^5.1.5: + version "5.1.6" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" + integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw== + dependencies: + asn1.js "^5.2.0" + browserify-aes "^1.0.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + safe-buffer "^5.1.1" + +parse-ms@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-1.0.1.tgz#56346d4749d78f23430ca0c713850aef91aa361d" + integrity sha512-LpH1Cf5EYuVjkBvCDBYvkUPh+iv2bk3FHflxHkpCYT0/FZ1d3N3uJaLiHr4yGuMcFUhv6eAivitTvWZI4B/chg== + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= + +path-browserify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" + integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== + +path-browserify@~0.0.0: + version "0.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" + integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= + +path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.6, path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-platform@~0.11.15: + version "0.11.15" + resolved "https://registry.yarnpkg.com/path-platform/-/path-platform-0.11.15.tgz#e864217f74c36850f0852b78dc7bf7d4a5721bf2" + integrity sha512-Y30dB6rab1A/nfEKsZxmr01nUotHX0c/ZiIAsCTatEe1CmS5Pm5He7fZ195bPT7RdquoaL8lLxFCMQi/bS7IJg== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== + dependencies: + pify "^3.0.0" + +pbkdf2@^3.0.3: + version "3.1.2" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" + integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +pem@^1.13.2: + version "1.14.6" + resolved "https://registry.yarnpkg.com/pem/-/pem-1.14.6.tgz#89babca3a73466fb844df70666dbf1b25eb0dc56" + integrity sha512-I5GKUer2PPv5qzUfxaZ6IGRkhp+357Kyv2t1JJg9vP8hGGI13qU34N2QupmggbpIZGPuudH0jn8KU5hjFpPk3g== + dependencies: + es6-promisify "^6.0.0" + md5 "^2.2.1" + os-tmpdir "^1.0.1" + which "^2.0.2" + +picocolors@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" + integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + +plur@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/plur/-/plur-1.0.0.tgz#db85c6814f5e5e5a3b49efc28d604fec62975156" + integrity sha512-qSnKBSZeDY8ApxwhfVIwKwF36KVJqb1/9nzYYq3j3vdwocULCXT8f8fQGkiw1Nk9BGfxiDagEe/pwakA+bOBqw== + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= + +postcss-color-function@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-color-function/-/postcss-color-function-4.1.0.tgz#b6f9355e07b12fcc5c34dab957834769b03d8f57" + integrity sha512-2/fuv6mP5Lt03XbRpVfMdGC8lRP1sykme+H1bR4ARyOmSMB8LPSjcL6EAI1iX6dqUF+jNEvKIVVXhan1w/oFDQ== + dependencies: + css-color-function "~1.3.3" + postcss "^6.0.23" + postcss-message-helpers "^2.0.0" + postcss-value-parser "^3.3.1" + +postcss-color-mod-function@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-mod-function/-/postcss-color-mod-function-3.0.0.tgz#e0993ddd68488fa2b060135caad4fee399bf7f7f" + integrity sha512-anmDjx6y/sX4PqEj2yaTCXtCKcr5wz7GHlSPZ5kol+grTYIVO87Ks7o5g7iMtkJhVqkkdVaL7GoH7ESuhg8RYQ== + dependencies: + "@csstools/convert-colors" "^1.4.0" + postcss "^7.0.2" + postcss-values-parser "^1.5.0" + +postcss-comment@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-comment/-/postcss-comment-2.0.0.tgz#6c8808e64cee25c33146518a8a82944b05f11e6f" + integrity sha512-5zT5iKU7c0tK9KJFNrVf+g1MGTkzf/4V3e0Zzm2g1uoFQC5jeTHmB9O1iAqh97+jnKpc6al204e0pwFUiCwseg== + dependencies: + postcss "^6.0.0" + +postcss-custom-prop-vars@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/postcss-custom-prop-vars/-/postcss-custom-prop-vars-0.0.3.tgz#018d7047af6c28c4d7933a778ef944c5f303c4d1" + integrity sha512-zU6o67ycLECJD7Yh+o86g4+qcVSv+qmh8jvesfvjrLqSTwKZPyz/BRiN94bFcF6ViZsmBEgf6r9i+53IAxQLjw== + +postcss-import@12: + version "12.0.1" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-12.0.1.tgz#cf8c7ab0b5ccab5649024536e565f841928b7153" + integrity sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw== + dependencies: + postcss "^7.0.1" + postcss-value-parser "^3.2.3" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-js@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-2.0.3.tgz#a96f0f23ff3d08cec7dc5b11bf11c5f8077cdab9" + integrity sha512-zS59pAk3deu6dVHyrGqmC3oDXBdNdajk4k1RyxeVXCrcEDBUBHoIhE4QTsmhxgzXxsaqFDAkUZfmMa5f/N/79w== + dependencies: + camelcase-css "^2.0.1" + postcss "^7.0.18" + +postcss-message-helpers@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz#a4f2f4fab6e4fe002f0aed000478cdf52f9ba60e" + integrity sha1-pPL0+rbk/gAvCu0ABHjN9S+bpg4= + +postcss-mixins@^6.2.3: + version "6.2.3" + resolved "https://registry.yarnpkg.com/postcss-mixins/-/postcss-mixins-6.2.3.tgz#021893ba455d04b5baa052bf196297ddd70e4af1" + integrity sha512-gfH5d09YilzDn/CLGFA9Lwv7GTezuyHgnAyXC8AfvhUMpl67ZTewhcpNuOgawClCOD+76XePE2IHO1xMgsOlvA== + dependencies: + globby "^8.0.1" + postcss "^7.0.21" + postcss-js "^2.0.3" + postcss-simple-vars "^5.0.2" + sugarss "^2.0.0" + +postcss-modules-extract-imports@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e" + integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ== + dependencies: + postcss "^7.0.5" + +postcss-modules-local-by-default@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0" + integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw== + dependencies: + icss-utils "^4.1.1" + postcss "^7.0.32" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.1.0" + +postcss-modules-scope@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee" + integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ== + dependencies: + postcss "^7.0.6" + postcss-selector-parser "^6.0.0" + +postcss-modules-values@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10" + integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg== + dependencies: + icss-utils "^4.0.0" + postcss "^7.0.6" + +postcss-nested@^4.2.1: + version "4.2.3" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-4.2.3.tgz#c6f255b0a720549776d220d00c4b70cd244136f6" + integrity sha512-rOv0W1HquRCamWy2kFl3QazJMMe1ku6rCFoAAH+9AcxdbpDeBr6k968MLWuLjvjMcGEip01ak09hKOEgpK9hvw== + dependencies: + postcss "^7.0.32" + postcss-selector-parser "^6.0.2" + +postcss-scss@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-4.0.0.tgz#6cfe09040f01e3345c9c226894c5913ac5fa9381" + integrity sha512-xakgIr5ukOEyXFcsnADKjQtrk8nQyqn5VIEAA+PmPP4kBOpknmjpJMxBNqCR1/x20AS0aSfZkWsSdbMx2Ozm5A== + +postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2: + version "6.0.6" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz#2c5bba8174ac2f6981ab631a42ab0ee54af332ea" + integrity sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-simple-vars@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-simple-vars/-/postcss-simple-vars-5.0.2.tgz#e2f81b3d0847ddd4169816b6d141b91d51e6e22e" + integrity sha512-xWIufxBoINJv6JiLb7jl5oElgp+6puJwvT5zZHliUSydoLz4DADRB3NDDsYgfKVwojn4TDLiseoC65MuS8oGGg== + dependencies: + postcss "^7.0.14" + +postcss-strip-inline-comments@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/postcss-strip-inline-comments/-/postcss-strip-inline-comments-0.1.5.tgz#7ff6bcdc14e633ed4cdfa020bae3eddad4f84b90" + integrity sha1-f/a83BTmM+1M36AguuPt2tT4S5A= + dependencies: + postcss "^5.0.18" + +postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" + integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== + +postcss-value-parser@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" + integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== + +postcss-values-parser@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-1.5.0.tgz#5d9fa63e2bcb0179ce48f3235303765eb89f3047" + integrity sha512-3M3p+2gMp0AH3da530TlX8kiO1nxdTnc3C6vr8dMxRLIlh8UYkz0/wcwptSXjhtx2Fr0TySI7a+BHDQ8NL7LaQ== + dependencies: + flatten "^1.0.2" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss@^5.0.18: + version "5.2.18" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5" + integrity sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg== + dependencies: + chalk "^1.1.3" + js-base64 "^2.1.9" + source-map "^0.5.6" + supports-color "^3.2.3" + +postcss@^6.0.0, postcss@^6.0.23, postcss@^6.0.9: + version "6.0.23" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" + integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag== + dependencies: + chalk "^2.4.1" + source-map "^0.6.1" + supports-color "^5.4.0" + +postcss@^7.0.1: + version "7.0.39" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309" + integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA== + dependencies: + picocolors "^0.2.1" + source-map "^0.6.1" + +postcss@^7.0.14, postcss@^7.0.18, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: + version "7.0.36" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.36.tgz#056f8cffa939662a8f5905950c07d5285644dfcb" + integrity sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + +postcss@^8.3.5: + version "8.3.6" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.6.tgz#2730dd76a97969f37f53b9a6096197be311cc4ea" + integrity sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A== + dependencies: + colorette "^1.2.2" + nanoid "^3.1.23" + source-map-js "^0.6.2" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + +prettier-bytes@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prettier-bytes/-/prettier-bytes-1.0.4.tgz#994b02aa46f699c50b6257b5faaa7fe2557e62d6" + integrity sha512-dLbWOa4xBn+qeWeIF60qRoB6Pk2jX5P3DIVgOQyMyvBpu931Q+8dXz8X0snJiFkQdohDDLnZQECjzsAj75hgZQ== + +pretty-bytes@^5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" + integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== + +pretty-ms@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-2.1.0.tgz#4257c256df3fb0b451d6affaab021884126981dc" + integrity sha512-H2enpsxzDhuzRl3zeSQpQMirn8dB0Z/gxW96j06tMfTviUWvX14gjKb7qd1gtkUyYhDPuoNe00K5PqNvy2oQNg== + dependencies: + is-finite "^1.0.1" + parse-ms "^1.0.0" + plur "^1.0.0" + +private@^0.1.6, private@~0.1.5: + version "0.1.8" + resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" + integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +process@~0.11.0: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + +progress@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +prop-types@^15.7.2: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.8.1" + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +public-encrypt@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" + integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + safe-buffer "^5.1.2" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + integrity sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw== + +punycode@^1.3.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +q@^1.1.2: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== + +qs@6.10.3: + version "6.10.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" + integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== + dependencies: + side-channel "^1.0.4" + +query-string@^4.2.3: + version "4.3.4" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" + integrity sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q== + dependencies: + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + +querystring-es3@~0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + integrity sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA== + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g== + +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +randomfill@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" + integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== + dependencies: + randombytes "^2.0.5" + safe-buffer "^5.1.0" + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +react-dom@^17.0.1: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" + integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + scheduler "^0.20.2" + +react-is@^16.8.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-tools@~0.13.0: + version "0.13.3" + resolved "https://registry.yarnpkg.com/react-tools/-/react-tools-0.13.3.tgz#da6ac7d4d7777a59a5e951cf46e72fd4b6b40a2c" + integrity sha1-2mrH1Nd3elml6VHPRucv1La0Ciw= + dependencies: + commoner "^0.10.0" + jstransform "^10.1.0" + +react@^17.0.1: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" + integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + +reactify@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/reactify/-/reactify-1.1.1.tgz#a8f119596273c0d4bfb1abea0c14c2601ea03bba" + integrity sha1-qPEZWWJzwNS/savqDBTCYB6gO7o= + dependencies: + react-tools "~0.13.0" + through "~2.3.4" + +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" + integrity sha1-5mTvMRYRZsl1HNvo28+GtftY93Q= + dependencies: + pify "^2.3.0" + +read-only-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-only-stream/-/read-only-stream-2.0.0.tgz#2724fd6a8113d73764ac288d4386270c1dbf17f0" + integrity sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A= + dependencies: + readable-stream "^2.0.2" + +readable-stream@3, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +"readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@~1.0.17: + version "1.0.34" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +"readable-stream@>=1.1.13-1 <1.2.0-0", readable-stream@^1.1.13-1: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.2.2, readable-stream@~2.3.3, readable-stream@~2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-wrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/readable-wrap/-/readable-wrap-1.0.0.tgz#3b5a211c631e12303a54991c806c17e7ae206bff" + integrity sha1-O1ohHGMeEjA6VJkcgGwX564ga/8= + dependencies: + readable-stream "^1.1.13-1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +recast@^0.11.17: + version "0.11.23" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3" + integrity sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM= + dependencies: + ast-types "0.9.6" + esprima "~3.1.0" + private "~0.1.5" + source-map "~0.5.0" + +regenerate-unicode-properties@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" + integrity sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA== + dependencies: + regenerate "^1.4.0" + +regenerate@^1.4.0: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regenerator-runtime@^0.13.4: + version "0.13.9" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" + integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== + +regenerator-transform@^0.14.2: + version "0.14.5" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4" + integrity sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw== + dependencies: + "@babel/runtime" "^7.8.4" + +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + +regexp.prototype.flags@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26" + integrity sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +regexp.prototype.flags@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" + integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + functions-have-names "^1.2.2" + +regexpp@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== + +regexpu-core@^4.7.1: + version "4.7.1" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6" + integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ== + dependencies: + regenerate "^1.4.0" + regenerate-unicode-properties "^8.2.0" + regjsgen "^0.5.1" + regjsparser "^0.6.4" + unicode-match-property-ecmascript "^1.0.4" + unicode-match-property-value-ecmascript "^1.2.0" + +regjsgen@^0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733" + integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A== + +regjsparser@^0.6.4: + version "0.6.9" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.9.tgz#b489eef7c9a2ce43727627011429cf833a7183e6" + integrity sha512-ZqbNRz1SNjLAiYuwY0zoXW8Ne675IX5q+YHioAGbCw4X96Mjl2+dcX9B2ciaeyYjViDAfvIjFpQjJgLttTEERQ== + dependencies: + jsesc "~0.5.0" + +reload-css@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/reload-css/-/reload-css-1.0.2.tgz#6afb11162e2314feccdad6dc5fde821fd7318331" + integrity sha512-R4IA5JujRJlvGW5B5qCKrlNgqrrLqsa0uvFaXFWtHcE7dkA9c6c8WbqKpvqQ82hOoWkHjmigZ970jTfsywzMxw== + dependencies: + query-string "^4.2.3" + +repeat-element@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9" + integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ== + +repeat-string@^1.5.2, repeat-string@^1.5.4, repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= + +resolve@^1.1.4, resolve@^1.1.6, resolve@^1.17.0, resolve@^1.4.0: + version "1.22.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" + integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== + dependencies: + is-core-module "^2.8.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +resolve@^1.1.7, resolve@^1.14.2: + version "1.20.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== + dependencies: + is-core-module "^2.2.0" + path-parse "^1.0.6" + +resolve@^2.0.0-next.3: + version "2.0.0-next.3" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.3.tgz#d41016293d4a8586a39ca5d9b5f15cbea1f55e46" + integrity sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q== + dependencies: + is-core-module "^2.2.0" + path-parse "^1.0.6" + +resp-modifier@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/resp-modifier/-/resp-modifier-6.0.2.tgz#b124de5c4fbafcba541f48ffa73970f4aa456b4f" + integrity sha512-U1+0kWC/+4ncRFYqQWTx/3qkfE6a4B/h3XXgmXypfa0SPZ3t7cbbaFk297PjQS/yov24R18h6OZe6iZwj3NSLw== + dependencies: + debug "^2.2.0" + minimatch "^3.0.2" + +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + +reversepoint@~0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/reversepoint/-/reversepoint-0.2.1.tgz#d2ac3ff4d665cf0ff72296b7a78ee7237f6593f5" + integrity sha1-0qw/9NZlzw/3Ipa3p47nI39lk/U= + +rgb@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/rgb/-/rgb-0.1.0.tgz#be27b291e8feffeac1bd99729721bfa40fc037b5" + integrity sha1-vieykej+/+rBvZlylyG/pA/AN7U= + +right-now@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/right-now/-/right-now-1.0.0.tgz#6e89609deebd7dcdaf8daecc9aea39cf585a0918" + integrity sha512-DA8+YS+sMIVpbsuKgy+Z67L9Lxb1p05mNxRpDPNksPDEFir4vmBlUtuN9jkTGn9YMMdlBuK7XQgFiz6ws+yhSg== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" + integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= + dependencies: + ret "~0.1.10" + +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +scheduler@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" + integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + +scope-analyzer@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/scope-analyzer/-/scope-analyzer-2.1.1.tgz#5156c27de084d74bf75af9e9506aaf95c6e73dd6" + integrity sha512-azEAihtQ9mEyZGhfgTJy3IbOWEzeOrYbg7NcYEshPKnKd+LZmC3TNd5dmDxbLBsTG/JVWmCp+vDJ03vJjeXMHg== + dependencies: + array-from "^2.1.1" + dash-ast "^1.0.0" + es6-map "^0.1.5" + es6-set "^0.1.5" + es6-symbol "^3.1.1" + estree-is-function "^1.0.0" + get-assigned-identifiers "^1.1.0" + +semver@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" + integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== + +semver@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^7.2.1: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serve-static@1.15.0, serve-static@^1.10.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +sha.js@^2.4.0, sha.js@^2.4.8, sha.js@~2.4.4: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +shallow-copy@~0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/shallow-copy/-/shallow-copy-0.0.1.tgz#415f42702d73d810330292cc5ee86eae1a11a170" + integrity sha1-QV9CcC1z2BAzApLMXuhurhoRoXA= + +shasum-object@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shasum-object/-/shasum-object-1.0.0.tgz#0b7b74ff5b66ecf9035475522fa05090ac47e29e" + integrity sha512-Iqo5rp/3xVi6M4YheapzZhhGPVs0yZwHj7wvwQ1B9z8H6zk+FEnI7y3Teq7qwnekfEhu8WmG2z0z4iWZaxLWVg== + dependencies: + fast-safe-stringify "^2.0.7" + +shasum@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/shasum/-/shasum-1.0.2.tgz#e7012310d8f417f4deb5712150e5678b87ae565f" + integrity sha1-5wEjENj0F/TetXEhUOVni4euVl8= + dependencies: + json-stable-stringify "~0.0.0" + sha.js "~2.4.4" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@^1.4.2, shell-quote@^1.6.1: + version "1.7.3" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" + integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +signal-exit@^3.0.0: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +simple-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" + integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== + +simple-html-index@^1.4.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/simple-html-index/-/simple-html-index-1.5.0.tgz#2c93eeaebac001d8a135fc0022bd4ade8f58996f" + integrity sha1-LJPurrrAAdihNfwAIr1K3o9YmW8= + dependencies: + from2-string "^1.1.0" + +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +source-map-js@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e" + integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug== + +source-map-resolve@^0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" + integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-support@~0.5.10: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-url@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" + integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== + +source-map@0.1.31: + version "0.1.31" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.31.tgz#9f704d0d69d9e138a81badf6ebb4fde33d151c61" + integrity sha1-n3BNDWnZ4TioG63267T94z0VHGE= + dependencies: + amdefine ">=0.0.4" + +source-map@^0.5.0, source-map@^0.5.6, source-map@~0.5.0, source-map@~0.5.3: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@~0.4.0, source-map@~0.4.2: + version "0.4.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" + integrity sha1-66T12pwNyZneaAMti092FzZSA2s= + dependencies: + amdefine ">=0.0.4" + +sourcemap-codec@^1.4.1: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^3.0.0" + +split2@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/split2/-/split2-0.2.1.tgz#02ddac9adc03ec0bb78c1282ec079ca6e85ae900" + integrity sha1-At2smtwD7Au3jBKC7Aecpuha6QA= + dependencies: + through2 "~0.6.1" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +stacked@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stacked/-/stacked-1.1.1.tgz#2c7fa38cc7e37a3411a77cd8e792de448f9f6975" + integrity sha1-LH+jjMfjejQRp3zY55LeRI+faXU= + +static-eval@^2.0.5: + version "2.1.0" + resolved "https://registry.yarnpkg.com/static-eval/-/static-eval-2.1.0.tgz#a16dbe54522d7fa5ef1389129d813fd47b148014" + integrity sha512-agtxZ/kWSsCkI5E4QifRwsaPs0P0JmZV6dkLz6ILYfFYQGn+5plctanRN+IC8dJRiFkyXHrwEE3W9Wmx67uDbw== + dependencies: + escodegen "^1.11.1" + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +static-module@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/static-module/-/static-module-3.0.4.tgz#bfbd1d1c38dd1fbbf0bb4af0c1b3ae18a93a2b68" + integrity sha512-gb0v0rrgpBkifXCa3yZXxqVmXDVE+ETXj6YlC/jt5VzOnGXR2C15+++eXuMDUYsePnbhf+lwW0pE1UXyOLtGCw== + dependencies: + acorn-node "^1.3.0" + concat-stream "~1.6.0" + convert-source-map "^1.5.1" + duplexer2 "~0.1.4" + escodegen "^1.11.1" + has "^1.0.1" + magic-string "0.25.1" + merge-source-map "1.0.4" + object-inspect "^1.6.0" + readable-stream "~2.3.3" + scope-analyzer "^2.0.1" + shallow-copy "~0.0.1" + static-eval "^2.0.5" + through2 "~2.0.3" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +stdout-stream@^1.4.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.1.tgz#5ac174cdd5cd726104aa0c0b2bd83815d8d535de" + integrity sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA== + dependencies: + readable-stream "^2.0.1" + +stream-browserify@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" + integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-browserify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" + integrity sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA== + dependencies: + inherits "~2.0.4" + readable-stream "^3.5.0" + +stream-combiner2@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stream-combiner2/-/stream-combiner2-1.1.1.tgz#fb4d8a1420ea362764e21ad4780397bebcb41cbe" + integrity sha1-+02KFCDqNidk4hrUeAOXvry0HL4= + dependencies: + duplexer2 "~0.1.0" + readable-stream "^2.0.2" + +stream-combiner@~0.2.1: + version "0.2.2" + resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.2.2.tgz#aec8cbac177b56b6f4fa479ced8c1912cee52858" + integrity sha1-rsjLrBd7Vrb0+kec7YwZEs7lKFg= + dependencies: + duplexer "~0.1.1" + through "~2.3.4" + +stream-http@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-3.2.0.tgz#1872dfcf24cb15752677e40e5c3f9cc1926028b5" + integrity sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A== + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.4" + readable-stream "^3.6.0" + xtend "^4.0.2" + +stream-splicer@^1.1.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/stream-splicer/-/stream-splicer-1.3.2.tgz#3c0441be15b9bf4e226275e6dc83964745546661" + integrity sha1-PARBvhW5v04iYnXm3IOWR0VUZmE= + dependencies: + indexof "0.0.1" + inherits "^2.0.1" + isarray "~0.0.1" + readable-stream "^1.1.13-1" + readable-wrap "^1.0.0" + through2 "^1.0.0" + +stream-splicer@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/stream-splicer/-/stream-splicer-2.0.1.tgz#0b13b7ee2b5ac7e0609a7463d83899589a363fcd" + integrity sha512-Xizh4/NPuYSyAXyT7g8IvdJ9HJpxIGL9PjyhtywCZvvP0OPIdqyrr4dMikeuvY8xahpdKEBlBTySe583totajg== + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.2" + +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= + +string-width@^4.2.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" + integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + +string.prototype.matchall@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.5.tgz#59370644e1db7e4c0c045277690cf7b01203c4da" + integrity sha512-Z5ZaXO0svs0M2xd/6By3qpeKpLKd9mO4v4q3oMEQrk8Ck4xOD5d5XeBOOjGrmVZZ/AHB1S0CgG4N5r1G9N3E2Q== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.18.2" + get-intrinsic "^1.1.1" + has-symbols "^1.0.2" + internal-slot "^1.0.3" + regexp.prototype.flags "^1.3.1" + side-channel "^1.0.4" + +string.prototype.trimend@^1.0.4, string.prototype.trimend@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz#914a65baaab25fbdd4ee291ca7dde57e869cb8d0" + integrity sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.19.5" + +string.prototype.trimstart@^1.0.4, string.prototype.trimstart@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz#5466d93ba58cfa2134839f81d7f42437e8c01fef" + integrity sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.19.5" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.3.0.tgz#25f48ea22ca79187f3174a4db8759347bb126220" + integrity sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA= + dependencies: + ansi-regex "^0.2.1" + +strip-ansi@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= + +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +subarg@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2" + integrity sha1-9izxdYHplrSPyWVpn1TAauJouNI= + dependencies: + minimist "^1.1.0" + +sugarss@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/sugarss/-/sugarss-2.0.0.tgz#ddd76e0124b297d40bf3cca31c8b22ecb43bc61d" + integrity sha512-WfxjozUk0UVA4jm+U1d736AUpzSrNsQcIbyOkoE364GrtWmIrFdk5lksEupgWMD4VaT/0kVx1dobpiDumSgmJQ== + dependencies: + postcss "^7.0.2" + +supports-color@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-1.3.1.tgz#15758df09d8ff3b4acc307539fabe27095e1042d" + integrity sha1-FXWN8J2P87SswwdTn6vicJXhBC0= + +supports-color@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-0.2.0.tgz#d92de2694eb3f67323973d7ae3d8b55b4c22190a" + integrity sha1-2S3iaU6z9nMjlz1649i1W0wiGQo= + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= + +supports-color@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" + integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY= + dependencies: + has-flag "^1.0.0" + +supports-color@^5.3.0, supports-color@^5.4.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +syntax-error@^1.1.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/syntax-error/-/syntax-error-1.4.0.tgz#2d9d4ff5c064acb711594a3e3b95054ad51d907c" + integrity sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w== + dependencies: + acorn-node "^1.2.0" + +table@^6.0.9: + version "6.7.1" + resolved "https://registry.yarnpkg.com/table/-/table-6.7.1.tgz#ee05592b7143831a8c94f3cee6aae4c1ccef33e2" + integrity sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg== + dependencies: + ajv "^8.0.1" + lodash.clonedeep "^4.5.0" + lodash.truncate "^4.4.2" + slice-ansi "^4.0.0" + string-width "^4.2.0" + strip-ansi "^6.0.0" + +term-color@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/term-color/-/term-color-1.0.1.tgz#38e192553a473e35e41604ff5199846bf8117a3a" + integrity sha1-OOGSVTpHPjXkFgT/UZmEa/gRejo= + dependencies: + ansi-styles "2.0.1" + supports-color "1.3.1" + +terser@^3.7.5: + version "3.17.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-3.17.0.tgz#f88ffbeda0deb5637f9d24b0da66f4e15ab10cb2" + integrity sha512-/FQzzPJmCpjAH9Xvk2paiWrFq+5M6aVOf+2KRbwhByISDX/EujxsK+BAvrhb6H+2rtrLCHK9N01wO014vrIwVQ== + dependencies: + commander "^2.19.0" + source-map "~0.6.1" + source-map-support "~0.5.10" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + +through2@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/through2/-/through2-0.5.1.tgz#dfdd012eb9c700e2323fd334f38ac622ab372da7" + integrity sha1-390BLrnHAOIyP9M084rGIqs3Lac= + dependencies: + readable-stream "~1.0.17" + xtend "~3.0.0" + +through2@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/through2/-/through2-1.1.1.tgz#0847cbc4449f3405574dbdccd9bb841b83ac3545" + integrity sha1-CEfLxESfNAVXTb3M2buEG4OsNUU= + dependencies: + readable-stream ">=1.1.13-1 <1.2.0-0" + xtend ">=4.0.0 <4.1.0-0" + +through2@^2.0.0, through2@^2.0.3, through2@~2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + +through2@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/through2/-/through2-4.0.2.tgz#a7ce3ac2a7a8b0b966c80e7c49f0484c3b239764" + integrity sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw== + dependencies: + readable-stream "3" + +through2@~0.6.1: + version "0.6.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48" + integrity sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg= + dependencies: + readable-stream ">=1.0.33-1 <1.1.0-0" + xtend ">=4.0.0 <4.1.0-0" + +"through@>=2.2.7 <3", through@~2.3.4: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +timers-browserify@^1.0.1: + version "1.4.2" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-1.4.2.tgz#c9c58b575be8407375cb5e2462dacee74359f41d" + integrity sha1-ycWLV1voQHN1y14kYtrO50NZ9B0= + dependencies: + process "~0.11.0" + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +tty-browserify@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811" + integrity sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw== + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + dependencies: + prelude-ls "~1.1.2" + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +type@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" + integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== + +type@^2.0.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/type/-/type-2.5.0.tgz#0a2e78c2e77907b252abe5f298c1b01c63f0db3d" + integrity sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw== + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + +uglifyify@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/uglifyify/-/uglifyify-5.0.2.tgz#7d0269885e09faa963208a9ec6721afcaf45fc50" + integrity sha512-NcSk6pgoC+IgwZZ2tVLVHq+VNKSvLPlLkF5oUiHPVOJI0s/OlSVYEGXG9PCAH0hcyFZLyvt4KBdPAQBRlVDn1Q== + dependencies: + convert-source-map "~1.1.0" + minimatch "^3.0.2" + terser "^3.7.5" + through "~2.3.4" + xtend "^4.0.1" + +umd@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/umd/-/umd-3.0.3.tgz#aa9fe653c42b9097678489c01000acb69f0b26cf" + integrity sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow== + +unbox-primitive@^1.0.1, unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + +undeclared-identifiers@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/undeclared-identifiers/-/undeclared-identifiers-1.1.3.tgz#9254c1d37bdac0ac2b52de4b6722792d2a91e30f" + integrity sha512-pJOW4nxjlmfwKApE4zvxLScM/njmwj/DiUBv7EabwE4O8kRUy+HIwxQtZLBPll/jx1LJyBcqNfB3/cpv9EZwOw== + dependencies: + acorn-node "^1.3.0" + dash-ast "^1.0.0" + get-assigned-identifiers "^1.2.0" + simple-concat "^1.0.0" + xtend "^4.0.1" + +unicode-canonical-property-names-ecmascript@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" + integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ== + +unicode-match-property-ecmascript@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c" + integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg== + dependencies: + unicode-canonical-property-names-ecmascript "^1.0.4" + unicode-property-aliases-ecmascript "^1.0.4" + +unicode-match-property-value-ecmascript@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz#0d91f600eeeb3096aa962b1d6fc88876e64ea531" + integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ== + +unicode-property-aliases-ecmascript@^1.0.4: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4" + integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg== + +union-value@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + +uniq@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= + +url-trim@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/url-trim/-/url-trim-1.0.0.tgz#40057e2f164b88e5daca7269da47e6d1dd837adc" + integrity sha1-QAV+LxZLiOXaynJp2kfm0d2Detw= + +url@~0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +util@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= + dependencies: + inherits "2.0.1" + +util@~0.10.1: + version "0.10.4" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" + integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== + dependencies: + inherits "2.0.3" + +util@~0.12.0: + version "0.12.4" + resolved "https://registry.yarnpkg.com/util/-/util-0.12.4.tgz#66121a31420df8f01ca0c464be15dfa1d1850253" + integrity sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw== + dependencies: + inherits "^2.0.3" + is-arguments "^1.0.4" + is-generator-function "^1.0.7" + is-typed-array "^1.1.3" + safe-buffer "^5.1.2" + which-typed-array "^1.1.2" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +v8-compile-cache@^2.0.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" + integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== + +validatem@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/validatem/-/validatem-0.2.1.tgz#fd85d7b035a6c3cf553e8d9292ff584743e25e98" + integrity sha512-1K/jV6NJe7+9+ukxjhoDVtaAXzqswjUKVw8iGiWJn8QNFYltteprrg+f9Q3cueo6kBNlybeYYiDs/PS14bffrA== + dependencies: + assure-array "^1.0.0" + create-error "^0.3.1" + default-value "^1.0.0" + is-plain-obj "^2.0.0" + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +vm-browserify@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" + integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== + +watchify-middleware@^1.8.2: + version "1.9.0" + resolved "https://registry.yarnpkg.com/watchify-middleware/-/watchify-middleware-1.9.0.tgz#27d1964687bbd9385a4e57a39fef0d06d32a1267" + integrity sha512-KI+MJlzbgl/FgnjY+9VTn3uZMhZKv6SXTBXcN3pxbA1KUt67IMOx4WANng5RUoZ2y9+496guR8CMxDsfSyM2KA== + dependencies: + concat-stream "^1.5.0" + debounce "^1.0.0" + events "^1.0.2" + object-assign "^4.0.1" + strip-ansi "^3.0.0" + watchify "^4.0.0" + +watchify@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/watchify/-/watchify-4.0.0.tgz#53b002d51e7b0eb640b851bb4de517a689973392" + integrity sha512-2Z04dxwoOeNxa11qzWumBTgSAohTC0+ScuY7XMenPnH+W2lhTcpEOJP4g2EIG/SWeLadPk47x++Yh+8BqPM/lA== + dependencies: + anymatch "^3.1.0" + browserify "^17.0.0" + chokidar "^3.4.0" + defined "^1.0.0" + outpipe "^1.1.0" + through2 "^4.0.2" + xtend "^4.0.2" + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which-typed-array@^1.1.2: + version "1.1.8" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.8.tgz#0cfd53401a6f334d90ed1125754a42ed663eb01f" + integrity sha512-Jn4e5PItbcAHyLoRDwvPj1ypu27DJbtdYXUa5zsinrUx77Uvfb0cXwwnGMTn7cjUfhhqgVQnVJCwF+7cgU7tpw== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-abstract "^1.20.0" + for-each "^0.3.3" + has-tostringtag "^1.0.0" + is-typed-array "^1.1.9" + +which@^1.2.9: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1, which@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.3, word-wrap@~1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +ws@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e" + integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw== + dependencies: + async-limiter "~1.0.0" + +"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +xtend@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-3.0.0.tgz#5cce7407baf642cba7becda568111c493f59665a" + integrity sha1-XM50B7r2Qsunvs2laBEcST9ZZlo= + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== diff --git a/web/template/footer.tmpl b/web/template/footer.tmpl index f95d8b71b..075682f60 100644 --- a/web/template/footer.tmpl +++ b/web/template/footer.tmpl @@ -1,19 +1,25 @@ - - + + + + {{ if .javascript }} + {{ range .javascript }} + + {{ end }} + {{ end }} \ No newline at end of file diff --git a/web/template/frontend.tmpl b/web/template/frontend.tmpl new file mode 100644 index 000000000..b667bc191 --- /dev/null +++ b/web/template/frontend.tmpl @@ -0,0 +1,5 @@ +{{ template "header.tmpl" .}} +
+
+
+{{ template "footer.tmpl" .}} \ No newline at end of file diff --git a/web/template/header.tmpl b/web/template/header.tmpl index 72f17d2a4..86f5cca89 100644 --- a/web/template/header.tmpl +++ b/web/template/header.tmpl @@ -1,29 +1,31 @@ - + - - + + - + + {{range .stylesheets}} {{end}} {{.instance.Title}} - GoToSocial - -
- Instance Logo -
-

- {{.instance.Title}} -

-
-
-
-
\ No newline at end of file +
+ +
+ Instance Logo +
+

+ {{.instance.Title}} +

+
+
+
+
diff --git a/web/template/profile.tmpl b/web/template/profile.tmpl index 71914b7e7..b1c3aa571 100644 --- a/web/template/profile.tmpl +++ b/web/template/profile.tmpl @@ -31,19 +31,4 @@ {{end}}
- {{ template "footer.tmpl" .}} diff --git a/web/template/thread.tmpl b/web/template/thread.tmpl index b6b91fd31..d0f003b91 100644 --- a/web/template/thread.tmpl +++ b/web/template/thread.tmpl @@ -16,19 +16,4 @@ {{end}} - {{ template "footer.tmpl" .}} \ No newline at end of file From 5864954e2ea6e9ef944f04b101f2c388df990d2a Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Thu, 9 Jun 2022 13:28:15 +0200 Subject: [PATCH 11/13] [bugfix] Fix domain blocks get regression (#642) * fix domain blocks get regression * add missing license text headers --- .../api/client/admin/domainblockcreate.go | 18 +++++++++++++ .../api/client/admin/domainblockdelete.go | 18 +++++++++++++ internal/api/client/admin/domainblockget.go | 18 +++++++++++++ internal/api/client/admin/domainblocksget.go | 26 +++++++++++++------ internal/api/client/admin/emojicreate_test.go | 18 +++++++++++++ 5 files changed, 90 insertions(+), 8 deletions(-) diff --git a/internal/api/client/admin/domainblockcreate.go b/internal/api/client/admin/domainblockcreate.go index e23376a91..cc97f52ca 100644 --- a/internal/api/client/admin/domainblockcreate.go +++ b/internal/api/client/admin/domainblockcreate.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package admin import ( diff --git a/internal/api/client/admin/domainblockdelete.go b/internal/api/client/admin/domainblockdelete.go index 416db002b..d57018f72 100644 --- a/internal/api/client/admin/domainblockdelete.go +++ b/internal/api/client/admin/domainblockdelete.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package admin import ( diff --git a/internal/api/client/admin/domainblockget.go b/internal/api/client/admin/domainblockget.go index 15456242c..e9fdf549b 100644 --- a/internal/api/client/admin/domainblockget.go +++ b/internal/api/client/admin/domainblockget.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package admin import ( diff --git a/internal/api/client/admin/domainblocksget.go b/internal/api/client/admin/domainblocksget.go index d585e837d..c3b3ec595 100644 --- a/internal/api/client/admin/domainblocksget.go +++ b/internal/api/client/admin/domainblocksget.go @@ -1,7 +1,24 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package admin import ( - "errors" "fmt" "net/http" "strconv" @@ -75,13 +92,6 @@ func (m *Module) DomainBlocksGETHandler(c *gin.Context) { 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) - return - } - export := false exportString := c.Query(ExportQueryKey) if exportString != "" { diff --git a/internal/api/client/admin/emojicreate_test.go b/internal/api/client/admin/emojicreate_test.go index 1b6ddf96f..872b18a75 100644 --- a/internal/api/client/admin/emojicreate_test.go +++ b/internal/api/client/admin/emojicreate_test.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package admin_test import ( From 2385b51d5851195f6c418e72c35d1526519f23b0 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Fri, 10 Jun 2022 10:56:49 +0200 Subject: [PATCH 12/13] [bugfix] Make accounts media_only query also work with pg (#643) --- internal/db/bundb/account.go | 26 +++++++++++++++++++------- internal/db/bundb/account_test.go | 6 ++++++ internal/db/bundb/bundbnew_test.go | 1 + 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/internal/db/bundb/account.go b/internal/db/bundb/account.go index 6061676c5..30510bb8e 100644 --- a/internal/db/bundb/account.go +++ b/internal/db/bundb/account.go @@ -25,11 +25,13 @@ "strings" "time" + "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/cache" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/uptrace/bun" + "github.com/uptrace/bun/dialect" ) type accountDB struct { @@ -268,14 +270,24 @@ func (a *accountDB) GetAccountStatuses(ctx context.Context, accountID string, li if mediaOnly { // attachments are stored as a json object; // this implementation differs between sqlite and postgres, - // so we have to be very thorough to cover all eventualities + // so we have to be thorough to cover all eventualities q = q.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery { - return q. - Where("? IS NOT NULL", bun.Ident("attachments")). - Where("? != ''", bun.Ident("attachments")). - Where("? != 'null'", bun.Ident("attachments")). - Where("? != '{}'", bun.Ident("attachments")). - Where("? != '[]'", bun.Ident("attachments")) + switch a.conn.Dialect().Name() { + case dialect.PG: + return q. + Where("? IS NOT NULL", bun.Ident("attachments")). + Where("? != '{}'", bun.Ident("attachments")) + case dialect.SQLite: + return q. + Where("? IS NOT NULL", bun.Ident("attachments")). + Where("? != ''", bun.Ident("attachments")). + Where("? != 'null'", bun.Ident("attachments")). + Where("? != '{}'", bun.Ident("attachments")). + Where("? != '[]'", bun.Ident("attachments")) + default: + logrus.Panic("db dialect was neither pg nor sqlite") + return q + } }) } diff --git a/internal/db/bundb/account_test.go b/internal/db/bundb/account_test.go index 33c6b7ee6..134e38940 100644 --- a/internal/db/bundb/account_test.go +++ b/internal/db/bundb/account_test.go @@ -40,6 +40,12 @@ func (suite *AccountTestSuite) TestGetAccountStatuses() { suite.Len(statuses, 5) } +func (suite *AccountTestSuite) TestGetAccountStatusesMediaOnly() { + statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 20, false, false, "", "", false, true, false) + suite.NoError(err) + suite.Len(statuses, 1) +} + func (suite *AccountTestSuite) TestGetAccountByIDWithExtras() { account, err := suite.db.GetAccountByID(context.Background(), suite.testAccounts["local_account_1"].ID) if err != nil { diff --git a/internal/db/bundb/bundbnew_test.go b/internal/db/bundb/bundbnew_test.go index d5e413a4f..2bd945864 100644 --- a/internal/db/bundb/bundbnew_test.go +++ b/internal/db/bundb/bundbnew_test.go @@ -41,6 +41,7 @@ func (suite *BundbNewTestSuite) TestCreateNewDB() { func (suite *BundbNewTestSuite) TestCreateNewSqliteDBNoAddress() { // create a new db with no address specified config.SetDbAddress("") + config.SetDbType("sqlite") db, err := bundb.NewBunDBService(context.Background()) suite.EqualError(err, "'db-address' was not set when attempting to start sqlite") suite.Nil(db) From 694a49058951de31cca4ea061e2c08d44e712612 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Sat, 11 Jun 2022 10:39:39 +0200 Subject: [PATCH 13/13] [feature] Add `created_at` and `error_description` to `/oauth/token` endpoint (#645) * start fiddling about with oauth server * start returning more helpful errors from oauth * test helpful(ish) token errors * add missing license header --- cmd/gotosocial/action/server/server.go | 2 +- cmd/gotosocial/action/testrig/testrig.go | 2 +- internal/api/client/auth/auth.go | 5 +- internal/api/client/auth/auth_test.go | 12 +- internal/api/client/auth/authorize.go | 2 +- internal/api/client/auth/authorize_test.go | 2 +- internal/api/client/auth/token.go | 69 +++++-- internal/api/client/auth/token_test.go | 215 +++++++++++++++++++++ internal/api/errorhandling.go | 27 +++ internal/oauth/errors.go | 26 +++ internal/oauth/server.go | 24 ++- internal/processing/oauth.go | 35 ++++ internal/processing/processor.go | 3 + testrig/testmodels.go | 17 ++ 14 files changed, 411 insertions(+), 30 deletions(-) create mode 100644 internal/api/client/auth/token_test.go create mode 100644 internal/oauth/errors.go create mode 100644 internal/processing/oauth.go diff --git a/cmd/gotosocial/action/server/server.go b/cmd/gotosocial/action/server/server.go index dd41f7708..abb373426 100644 --- a/cmd/gotosocial/action/server/server.go +++ b/cmd/gotosocial/action/server/server.go @@ -165,7 +165,7 @@ } // build client api modules - authModule := auth.New(dbService, oauthServer, idp, processor) + authModule := auth.New(dbService, idp, processor) accountModule := account.New(processor) instanceModule := instance.New(processor) appsModule := app.New(processor) diff --git a/cmd/gotosocial/action/testrig/testrig.go b/cmd/gotosocial/action/testrig/testrig.go index cb0557771..8fc87de34 100644 --- a/cmd/gotosocial/action/testrig/testrig.go +++ b/cmd/gotosocial/action/testrig/testrig.go @@ -108,7 +108,7 @@ } // build client api modules - authModule := auth.New(dbService, oauthServer, idp, processor) + authModule := auth.New(dbService, idp, processor) accountModule := account.New(processor) instanceModule := instance.New(processor) appsModule := app.New(processor) diff --git a/internal/api/client/auth/auth.go b/internal/api/client/auth/auth.go index 10d374838..a097f8004 100644 --- a/internal/api/client/auth/auth.go +++ b/internal/api/client/auth/auth.go @@ -23,7 +23,6 @@ "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" @@ -68,16 +67,14 @@ // Module implements the ClientAPIModule interface for type Module struct { db db.DB - server oauth.Server idp oidc.IDP processor processing.Processor } // New returns a new auth module -func New(db db.DB, server oauth.Server, idp oidc.IDP, processor processing.Processor) api.ClientModule { +func New(db db.DB, idp oidc.IDP, processor processing.Processor) api.ClientModule { return &Module{ db: db, - server: server, idp: idp, processor: processor, } diff --git a/internal/api/client/auth/auth_test.go b/internal/api/client/auth/auth_test.go index f222f714f..6a7c6ab99 100644 --- a/internal/api/client/auth/auth_test.go +++ b/internal/api/client/auth/auth_test.go @@ -19,6 +19,7 @@ package auth_test import ( + "bytes" "context" "fmt" "net/http/httptest" @@ -99,7 +100,7 @@ func (suite *AuthStandardTestSuite) SetupTest() { if err != nil { panic(err) } - suite.authModule = auth.New(suite.db, suite.oauthServer, suite.idp, suite.processor).(*auth.Module) + suite.authModule = auth.New(suite.db, suite.idp, suite.processor).(*auth.Module) testrig.StandardDBSetup(suite.db, suite.testAccounts) } @@ -107,7 +108,7 @@ func (suite *AuthStandardTestSuite) TearDownTest() { testrig.StandardDBTeardown(suite.db) } -func (suite *AuthStandardTestSuite) newContext(requestMethod string, requestPath string) (*gin.Context, *httptest.ResponseRecorder) { +func (suite *AuthStandardTestSuite) newContext(requestMethod string, requestPath string, requestBody []byte, bodyContentType string) (*gin.Context, *httptest.ResponseRecorder) { // create the recorder and gin test context recorder := httptest.NewRecorder() ctx, engine := gin.CreateTestContext(recorder) @@ -120,9 +121,14 @@ func (suite *AuthStandardTestSuite) newContext(requestMethod string, requestPath host := config.GetHost() baseURI := fmt.Sprintf("%s://%s", protocol, host) requestURI := fmt.Sprintf("%s/%s", baseURI, requestPath) - ctx.Request = httptest.NewRequest(requestMethod, requestURI, nil) // the endpoint we're hitting + + ctx.Request = httptest.NewRequest(requestMethod, requestURI, bytes.NewReader(requestBody)) // the endpoint we're hitting ctx.Request.Header.Set("accept", "text/html") + if bodyContentType != "" { + ctx.Request.Header.Set("Content-Type", bodyContentType) + } + // trigger the session middleware on the context store := memstore.NewStore(make([]byte, 32), make([]byte, 32)) store.Options(router.SessionOptions()) diff --git a/internal/api/client/auth/authorize.go b/internal/api/client/auth/authorize.go index 6f96484a8..233dacfd2 100644 --- a/internal/api/client/auth/authorize.go +++ b/internal/api/client/auth/authorize.go @@ -246,7 +246,7 @@ func (m *Module) AuthorizePOSTHandler(c *gin.Context) { sessionUserID: {userID}, } - if err := m.server.HandleAuthorizeRequest(c.Writer, c.Request); err != nil { + if err := m.processor.OAuthHandleAuthorizeRequest(c.Writer, c.Request); err != nil { api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice), m.processor.InstanceGet) } } diff --git a/internal/api/client/auth/authorize_test.go b/internal/api/client/auth/authorize_test.go index f9c1eceb6..35b995e70 100644 --- a/internal/api/client/auth/authorize_test.go +++ b/internal/api/client/auth/authorize_test.go @@ -69,7 +69,7 @@ func (suite *AuthAuthorizeTestSuite) TestAccountAuthorizeHandler() { } doTest := func(testCase authorizeHandlerTestCase) { - ctx, recorder := suite.newContext(http.MethodGet, auth.OauthAuthorizePath) + ctx, recorder := suite.newContext(http.MethodGet, auth.OauthAuthorizePath, nil, "") user := suite.testUsers["unconfirmed_account"] account := suite.testAccounts["unconfirmed_account"] diff --git a/internal/api/client/auth/token.go b/internal/api/client/auth/token.go index 34fb62940..fbbd08404 100644 --- a/internal/api/client/auth/token.go +++ b/internal/api/client/auth/token.go @@ -19,20 +19,22 @@ package auth import ( + "net/http" "net/url" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/gin-gonic/gin" ) -type tokenBody struct { +type tokenRequestForm struct { + GrantType *string `form:"grant_type" json:"grant_type" xml:"grant_type"` + Code *string `form:"code" json:"code" xml:"code"` + RedirectURI *string `form:"redirect_uri" json:"redirect_uri" xml:"redirect_uri"` ClientID *string `form:"client_id" json:"client_id" xml:"client_id"` ClientSecret *string `form:"client_secret" json:"client_secret" xml:"client_secret"` - Code *string `form:"code" json:"code" xml:"code"` - GrantType *string `form:"grant_type" json:"grant_type" xml:"grant_type"` - RedirectURI *string `form:"redirect_uri" json:"redirect_uri" xml:"redirect_uri"` Scope *string `form:"scope" json:"scope" xml:"scope"` } @@ -44,35 +46,70 @@ func (m *Module) TokenPOSTHandler(c *gin.Context) { return } - form := &tokenBody{} + help := []string{} + + form := &tokenRequestForm{} if err := c.ShouldBind(form); err != nil { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet) + api.OAuthErrorHandler(c, gtserror.NewErrorBadRequest(oauth.InvalidRequest(), err.Error())) return } c.Request.Form = url.Values{} + + var grantType string + if form.GrantType != nil { + grantType = *form.GrantType + c.Request.Form.Set("grant_type", grantType) + } else { + help = append(help, "grant_type was not set in the token request form, but must be set to authorization_code or client_credentials") + } + if form.ClientID != nil { c.Request.Form.Set("client_id", *form.ClientID) + } else { + help = append(help, "client_id was not set in the token request form") } + if form.ClientSecret != nil { c.Request.Form.Set("client_secret", *form.ClientSecret) + } else { + help = append(help, "client_secret was not set in the token request form") } - if form.Code != nil { - c.Request.Form.Set("code", *form.Code) - } - if form.GrantType != nil { - c.Request.Form.Set("grant_type", *form.GrantType) - } + if form.RedirectURI != nil { c.Request.Form.Set("redirect_uri", *form.RedirectURI) + } else { + help = append(help, "redirect_uri was not set in the token request form") } + + var code string + if form.Code != nil { + if grantType != "authorization_code" { + help = append(help, "a code was provided in the token request form, but grant_type was not set to authorization_code") + } else { + code = *form.Code + c.Request.Form.Set("code", code) + } + } else if grantType == "authorization_code" { + help = append(help, "code was not set in the token request form, but must be set since grant_type is authorization_code") + } + if form.Scope != nil { c.Request.Form.Set("scope", *form.Scope) } - // pass the writer and request into the oauth server handler, which will - // take care of writing the oauth token into the response etc - if err := m.server.HandleTokenRequest(c.Writer, c.Request); err != nil { - api.ErrorHandler(c, gtserror.NewErrorInternalError(err, helpfulAdvice), m.processor.InstanceGet) + if len(help) != 0 { + api.OAuthErrorHandler(c, gtserror.NewErrorBadRequest(oauth.InvalidRequest(), help...)) + return } + + token, errWithCode := m.processor.OAuthHandleTokenRequest(c.Request) + if errWithCode != nil { + api.OAuthErrorHandler(c, errWithCode) + return + } + + c.Header("Cache-Control", "no-store") + c.Header("Pragma", "no-cache") + c.JSON(http.StatusOK, token) } diff --git a/internal/api/client/auth/token_test.go b/internal/api/client/auth/token_test.go new file mode 100644 index 000000000..50bbd6918 --- /dev/null +++ b/internal/api/client/auth/token_test.go @@ -0,0 +1,215 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package auth_test + +import ( + "context" + "encoding/json" + "io/ioutil" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/suite" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/testrig" +) + +type TokenTestSuite struct { + AuthStandardTestSuite +} + +func (suite *TokenTestSuite) TestPOSTTokenEmptyForm() { + ctx, recorder := suite.newContext(http.MethodPost, "oauth/token", []byte{}, "") + ctx.Request.Header.Set("accept", "application/json") + + suite.authModule.TokenPOSTHandler(ctx) + + suite.Equal(http.StatusBadRequest, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + + b, err := ioutil.ReadAll(result.Body) + suite.NoError(err) + + suite.Equal(`{"error":"invalid_request","error_description":"Bad Request: grant_type was not set in the token request form, but must be set to authorization_code or client_credentials: client_id was not set in the token request form: client_secret was not set in the token request form: redirect_uri was not set in the token request form"}`, string(b)) +} + +func (suite *TokenTestSuite) TestRetrieveClientCredentialsOK() { + testClient := suite.testClients["local_account_1"] + + requestBody, w, err := testrig.CreateMultipartFormData( + "", "", + map[string]string{ + "grant_type": "client_credentials", + "client_id": testClient.ID, + "client_secret": testClient.Secret, + "redirect_uri": "http://localhost:8080", + }) + if err != nil { + panic(err) + } + bodyBytes := requestBody.Bytes() + + ctx, recorder := suite.newContext(http.MethodPost, "oauth/token", bodyBytes, w.FormDataContentType()) + ctx.Request.Header.Set("accept", "application/json") + + suite.authModule.TokenPOSTHandler(ctx) + + suite.Equal(http.StatusOK, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + + b, err := ioutil.ReadAll(result.Body) + suite.NoError(err) + + t := &apimodel.Token{} + err = json.Unmarshal(b, t) + suite.NoError(err) + + suite.Equal("Bearer", t.TokenType) + suite.NotEmpty(t.AccessToken) + suite.NotEmpty(t.CreatedAt) + suite.WithinDuration(time.Now(), time.Unix(t.CreatedAt, 0), 1*time.Minute) + + // there should be a token in the database now too + dbToken := >smodel.Token{} + err = suite.db.GetWhere(context.Background(), []db.Where{{Key: "access", Value: t.AccessToken}}, dbToken) + suite.NoError(err) + suite.NotNil(dbToken) +} + +func (suite *TokenTestSuite) TestRetrieveAuthorizationCodeOK() { + testClient := suite.testClients["local_account_1"] + testUserAuthorizationToken := suite.testTokens["local_account_1_user_authorization_token"] + + requestBody, w, err := testrig.CreateMultipartFormData( + "", "", + map[string]string{ + "grant_type": "authorization_code", + "client_id": testClient.ID, + "client_secret": testClient.Secret, + "redirect_uri": "http://localhost:8080", + "code": testUserAuthorizationToken.Code, + }) + if err != nil { + panic(err) + } + bodyBytes := requestBody.Bytes() + + ctx, recorder := suite.newContext(http.MethodPost, "oauth/token", bodyBytes, w.FormDataContentType()) + ctx.Request.Header.Set("accept", "application/json") + + suite.authModule.TokenPOSTHandler(ctx) + + suite.Equal(http.StatusOK, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + + b, err := ioutil.ReadAll(result.Body) + suite.NoError(err) + + t := &apimodel.Token{} + err = json.Unmarshal(b, t) + suite.NoError(err) + + suite.Equal("Bearer", t.TokenType) + suite.NotEmpty(t.AccessToken) + suite.NotEmpty(t.CreatedAt) + suite.WithinDuration(time.Now(), time.Unix(t.CreatedAt, 0), 1*time.Minute) + + dbToken := >smodel.Token{} + err = suite.db.GetWhere(context.Background(), []db.Where{{Key: "access", Value: t.AccessToken}}, dbToken) + suite.NoError(err) + suite.NotNil(dbToken) +} + +func (suite *TokenTestSuite) TestRetrieveAuthorizationCodeNoCode() { + testClient := suite.testClients["local_account_1"] + + requestBody, w, err := testrig.CreateMultipartFormData( + "", "", + map[string]string{ + "grant_type": "authorization_code", + "client_id": testClient.ID, + "client_secret": testClient.Secret, + "redirect_uri": "http://localhost:8080", + }) + if err != nil { + panic(err) + } + bodyBytes := requestBody.Bytes() + + ctx, recorder := suite.newContext(http.MethodPost, "oauth/token", bodyBytes, w.FormDataContentType()) + ctx.Request.Header.Set("accept", "application/json") + + suite.authModule.TokenPOSTHandler(ctx) + + suite.Equal(http.StatusBadRequest, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + + b, err := ioutil.ReadAll(result.Body) + suite.NoError(err) + + suite.Equal(`{"error":"invalid_request","error_description":"Bad Request: code was not set in the token request form, but must be set since grant_type is authorization_code"}`, string(b)) +} + +func (suite *TokenTestSuite) TestRetrieveAuthorizationCodeWrongGrantType() { + testClient := suite.testClients["local_account_1"] + + requestBody, w, err := testrig.CreateMultipartFormData( + "", "", + map[string]string{ + "grant_type": "client_credentials", + "client_id": testClient.ID, + "client_secret": testClient.Secret, + "redirect_uri": "http://localhost:8080", + "code": "peepeepoopoo", + }) + if err != nil { + panic(err) + } + bodyBytes := requestBody.Bytes() + + ctx, recorder := suite.newContext(http.MethodPost, "oauth/token", bodyBytes, w.FormDataContentType()) + ctx.Request.Header.Set("accept", "application/json") + + suite.authModule.TokenPOSTHandler(ctx) + + suite.Equal(http.StatusBadRequest, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + + b, err := ioutil.ReadAll(result.Body) + suite.NoError(err) + + suite.Equal(`{"error":"invalid_request","error_description":"Bad Request: a code was provided in the token request form, but grant_type was not set to authorization_code"}`, string(b)) +} + +func TestTokenTestSuite(t *testing.T) { + suite.Run(t, &TokenTestSuite{}) +} diff --git a/internal/api/errorhandling.go b/internal/api/errorhandling.go index 57659f83c..59b58bcc3 100644 --- a/internal/api/errorhandling.go +++ b/internal/api/errorhandling.go @@ -125,3 +125,30 @@ func ErrorHandler(c *gin.Context, errWithCode gtserror.WithCode, instanceGet fun genericErrorHandler(c, instanceGet, accept, errWithCode) } } + +// OAuthErrorHandler is a lot like ErrorHandler, but it specifically returns errors +// that are compatible with https://datatracker.ietf.org/doc/html/rfc6749#section-5.2, +// but serializing errWithCode.Error() in the 'error' field, and putting any help text +// from the error in the 'error_description' field. This means you should be careful not +// to pass any detailed errors (that might contain sensitive information) into the +// errWithCode.Error() field, since the client will see this. Use your noggin! +func OAuthErrorHandler(c *gin.Context, errWithCode gtserror.WithCode) { + l := logrus.WithFields(logrus.Fields{ + "path": c.Request.URL.Path, + "error": errWithCode.Error(), + "help": errWithCode.Safe(), + }) + + statusCode := errWithCode.Code() + + if statusCode == http.StatusInternalServerError { + l.Error("Internal Server Error") + } else { + l.Debug("handling OAuth error") + } + + c.JSON(statusCode, gin.H{ + "error": errWithCode.Error(), + "error_description": errWithCode.Safe(), + }) +} diff --git a/internal/oauth/errors.go b/internal/oauth/errors.go new file mode 100644 index 000000000..25278bdc9 --- /dev/null +++ b/internal/oauth/errors.go @@ -0,0 +1,26 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package oauth + +import "github.com/superseriousbusiness/oauth2/v4/errors" + +// InvalidRequest returns an oauth spec compliant 'invalid_request' error. +func InvalidRequest() error { + return errors.New("invalid_request") +} diff --git a/internal/oauth/server.go b/internal/oauth/server.go index bfe615832..4dcc41ceb 100644 --- a/internal/oauth/server.go +++ b/internal/oauth/server.go @@ -25,6 +25,7 @@ "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/oauth2/v4" "github.com/superseriousbusiness/oauth2/v4/errors" "github.com/superseriousbusiness/oauth2/v4/manage" @@ -52,7 +53,7 @@ // Server wraps some oauth2 server functions in an interface, exposing only what is needed type Server interface { - HandleTokenRequest(w http.ResponseWriter, r *http.Request) error + HandleTokenRequest(r *http.Request) (map[string]interface{}, gtserror.WithCode) HandleAuthorizeRequest(w http.ResponseWriter, r *http.Request) error ValidationBearerToken(r *http.Request) (oauth2.TokenInfo, error) GenerateUserAccessToken(ctx context.Context, ti oauth2.TokenInfo, clientSecret string, userID string) (accessToken oauth2.TokenInfo, err error) @@ -116,8 +117,25 @@ func New(ctx context.Context, database db.Basic) Server { } // HandleTokenRequest wraps the oauth2 library's HandleTokenRequest function -func (s *s) HandleTokenRequest(w http.ResponseWriter, r *http.Request) error { - return s.server.HandleTokenRequest(w, r) +func (s *s) HandleTokenRequest(r *http.Request) (map[string]interface{}, gtserror.WithCode) { + ctx := r.Context() + + gt, tgr, err := s.server.ValidationTokenRequest(r) + if err != nil { + help := fmt.Sprintf("could not validate token request: %s", err) + return nil, gtserror.NewErrorBadRequest(err, help) + } + + ti, err := s.server.GetAccessToken(ctx, gt, tgr) + if err != nil { + help := fmt.Sprintf("could not get access token: %s", err) + return nil, gtserror.NewErrorBadRequest(err, help) + } + + data := s.server.GetTokenData(ti) + data["created_at"] = ti.GetAccessCreateAt().Unix() + + return data, nil } // HandleAuthorizeRequest wraps the oauth2 library's HandleAuthorizeRequest function diff --git a/internal/processing/oauth.go b/internal/processing/oauth.go new file mode 100644 index 000000000..9c974f76e --- /dev/null +++ b/internal/processing/oauth.go @@ -0,0 +1,35 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package processing + +import ( + "net/http" + + "github.com/superseriousbusiness/gotosocial/internal/gtserror" +) + +func (p *processor) OAuthHandleAuthorizeRequest(w http.ResponseWriter, r *http.Request) error { + // todo: some kind of metrics stuff here + return p.oauthServer.HandleAuthorizeRequest(w, r) +} + +func (p *processor) OAuthHandleTokenRequest(r *http.Request) (map[string]interface{}, gtserror.WithCode) { + // todo: some kind of metrics stuff here + return p.oauthServer.HandleTokenRequest(r) +} diff --git a/internal/processing/processor.go b/internal/processing/processor.go index a7a1c22e0..3afc25196 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -152,6 +152,9 @@ type Processor interface { // NotificationsGet NotificationsGet(ctx context.Context, authed *oauth.Auth, limit int, maxID string, sinceID string) (*apimodel.TimelineResponse, gtserror.WithCode) + OAuthHandleTokenRequest(r *http.Request) (map[string]interface{}, gtserror.WithCode) + OAuthHandleAuthorizeRequest(w http.ResponseWriter, r *http.Request) 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) diff --git a/testrig/testmodels.go b/testrig/testmodels.go index ee601e053..b74ada751 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -56,6 +56,23 @@ func NewTestTokens() map[string]*gtsmodel.Token { AccessCreateAt: time.Now(), AccessExpiresAt: time.Now().Add(72 * time.Hour), }, + "local_account_1_client_application_token": { + ID: "01P9SVWS9J3SPHZQ3KCMBEN70N", + ClientID: "01F8MGV8AC3NGSJW0FE8W1BV70", + RedirectURI: "http://localhost:8080", + Access: "ZTK1MWMWZDGTMGMXOS0ZY2UXLWI5ZWETMWEZYZZIYTLHMZI4", + AccessCreateAt: TimeMustParse("2022-06-10T15:22:08Z"), + AccessExpiresAt: TimeMustParse("2050-01-01T15:22:08Z"), + }, + "local_account_1_user_authorization_token": { + ID: "01G574M2VTV66YZBC9AZ7HY3FV", + ClientID: "01F8MGV8AC3NGSJW0FE8W1BV70", + UserID: "01F8MGVGPHQ2D3P3X0454H54Z5", + RedirectURI: "http://localhost:8080", + Code: "ZJYYMZQ0MTQTZTU1NC0ZNJK4LWE2ZWITYTM1MDHHOTAXNJHL", + CodeCreateAt: TimeMustParse("2022-06-10T15:22:08Z"), + CodeExpiresAt: TimeMustParse("2050-01-01T15:22:08Z"), + }, "local_account_2": { ID: "01F8MGVVM1EDVYET710J27XY5R", ClientID: "01F8MGW47HN8ZXNHNZ7E47CDMQ",