Compare commits

...

10 commits

Author SHA1 Message Date
Rafael Caricio b3194f1fee
Add missing published field to Announce 2023-08-10 22:15:17 +02:00
Rafael Caricio af29c06290
Hack: Allow for me specifically to migrate to GoToSocial 2023-08-08 20:01:45 +02:00
kim 0ddc2edf19
[bugfix] only set content-length AFTER rewinding body bytes (#2086) 2023-08-08 12:45:29 +01:00
kim 3920bc87d1
[bugfix] don't accept unrelated statuses (#2078)
Co-authored-by: Daenney <daenney@users.noreply.github.com>
Co-authored-by: tsmethurst <tobi.smethurst@protonmail.com>
2023-08-08 12:26:34 +01:00
tobi 4b05dcde43
[chore] Update robots.txt, give chatgpt the middle finger (#2085) 2023-08-08 13:16:34 +02:00
Daenney 9df4d38c43
[chore] Add Feditext as recommended client (#2081)
With Feditext now accepting beta users, this adds it as the third
client to recommend so we have web and the dominant mobile platforms
covered.

This also removes the screenshots from the README, because it became a
mess trying to add a third one. Either the cells become very narrow, or
the table doubles in height. As the UI may also change over time, it
might be better to point folks at the apps instead who'll hopefully have
up to date screenshots in their storefronts.
2023-08-08 12:19:41 +02:00
Daenney be3718f6e4
[chore] Use generic pointer function (#2080)
This replaces the different $TypePtr functions with a generic
implementation.
2023-08-07 18:38:11 +01:00
dependabot[bot] 517829ae6a
[chore]: Bump github.com/tdewolff/minify/v2 from 2.12.7 to 2.12.8 (#2073) 2023-08-07 08:28:49 +00:00
Vyr Cossont 0f812746b7
[feature] Allow full BCP 47 in language inputs (#2067)
* Allow full BCP 47 in language inputs

Fixes #2066

* Fuse validation and normalization for languages

* Remove outdated comment line

* Move post language canonicalization test
2023-08-07 10:25:54 +02:00
dependabot[bot] 303a6a6b1d
[chore]: Bump golang.org/x/oauth2 from 0.10.0 to 0.11.0 (#2076) 2023-08-07 08:21:44 +00:00
60 changed files with 1563 additions and 1171 deletions

View file

@ -91,13 +91,13 @@ For a detailed view on what's implemented and what's not, and progress made towa
The Mastodon API has become the de facto standard for client communication with federated servers, so GoToSocial has implemented and extended the API with custom functionality.
In short, this means full support for modern, beautiful apps like [Tusky](https://tusky.app/) and [Semaphore](https://semaphore.social/).
Though most apps that implement the Mastodon API should work, GoToSocial works reliably with beautiful apps like:
Tusky | Semaphore
:-----------------------------------------------------------:|:------------------------------------------------------------------:
![An image of GoToSocial in Tusky](./docs/assets/tusky.png) | ![An image of GoToSocial in Semaphore](./docs/assets/semaphore.png)
* [Tusky](https://tusky.app/) for Android
* [Semaphore](https://semaphore.social/) in the browser
* [Feditext](https://fedi.software/@Feditext) (beta) on iOS, iPadOS and macOS
If you're used to using Mastodon with Tusky or Semaphore, you'll find using GoToSocial a breeze.
If you've used Mastodon with any of these apps before, you'll find using GoToSocial a breeze.
### Granular post settings

Binary file not shown.

Before

Width:  |  Height:  |  Size: 673 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 624 KiB

View file

@ -2,7 +2,7 @@
## Where's the user interface?
GoToSocial is just a bare server for the most part and is designed to be used thru external applications. [Semaphore](https://semaphore.social/) and [Tusky](https://tusky.app/) are the best-supported, but anything that supports the Mastodon API should work, other than the features GoToSocial doesn't yet have. Permalinks and profile pages are served directly through GoToSocial as well as the settings panel, but most interaction goes through the apps.
GoToSocial is just a bare server for the most part and is designed to be used thru external applications. [Semaphore](https://semaphore.social/) in the browser, [Tusky](https://tusky.app/) for Android and [Feditext](https://fedi.software/@Feditext) for iOS, iPadOS and macOS are the best-supported. Anything that supports the Mastodon API should work, other than the features GoToSocial doesn't yet have. Permalinks and profile pages are served directly through GoToSocial as well as the settings panel, but most interaction goes through the apps.
## Why aren't my posts showing up on my profile page?

View file

@ -78,7 +78,7 @@ You can use the GoToSocial binary to also create and promote your user account.
## Login
You should now be able to log in to your instance using the email address and password of the account you just created. We recommend using [Semaphore](https://semaphore.social) or [Tusky](https://tusky.app) for this.
You should now be able to log in to your instance using the email address and password of the account you just created.
## (Optional) Enable the systemd service

View file

@ -38,7 +38,7 @@ Since GoToSocial is still in alpha, there are plenty of bugs. We use [GitHub iss
### Client App Issues
GoToSocial works great with Tusky and Semaphore, but some other client applications still need work or have issues connecting to GoToSocial. We're tracking them [right here](https://github.com/superseriousbusiness/gotosocial/projects/5). It's our goal to make any app that's compatible with the Mastodon API work seamlessly with GoToSocial.
GoToSocial works great with Tusky, Semaphore and Feditext, but some other client applications still need work or have issues connecting to GoToSocial. We're tracking them [right here](https://github.com/superseriousbusiness/gotosocial/projects/5). It's our goal to make any app that's compatible with the Mastodon API work seamlessly with GoToSocial.
### Federation Issues

8
go.mod
View file

@ -45,7 +45,7 @@ require (
github.com/superseriousbusiness/activity v1.4.0-gts
github.com/superseriousbusiness/exif-terminator v0.5.0
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8
github.com/tdewolff/minify/v2 v2.12.7
github.com/tdewolff/minify/v2 v2.12.8
github.com/ulule/limiter/v3 v3.11.2
github.com/uptrace/bun v1.1.14
github.com/uptrace/bun/dialect/pgdialect v1.1.14
@ -62,8 +62,8 @@ require (
golang.org/x/crypto v0.12.0
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
golang.org/x/image v0.11.0
golang.org/x/net v0.12.0
golang.org/x/oauth2 v0.10.0
golang.org/x/net v0.14.0
golang.org/x/oauth2 v0.11.0
golang.org/x/text v0.12.0
gopkg.in/mcuadros/go-syslog.v2 v2.3.0
gopkg.in/yaml.v3 v3.0.1
@ -152,7 +152,7 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430-d89a106fdabe // indirect
github.com/tdewolff/parse/v2 v2.6.6 // indirect
github.com/tdewolff/parse/v2 v2.6.7 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect

20
go.sum
View file

@ -111,7 +111,6 @@ github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
@ -149,7 +148,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/djherbis/atime v1.1.0/go.mod h1:28OF6Y8s3NQWwacXc5eZTsEsiMzp7LF8MbXE+XJPdBE=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E=
@ -438,7 +436,6 @@ github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
@ -564,11 +561,10 @@ github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430
github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430-d89a106fdabe/go.mod h1:gH4P6gN1V+wmIw5o97KGaa1RgXB/tVpC2UNzijhg3E4=
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8 h1:nTIhuP157oOFcscuoK1kCme1xTeGIzztSw70lX9NrDQ=
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8/go.mod h1:uYC/W92oVRJ49Vh1GcvTqpeFqHi+Ovrl2sMllQWRAEo=
github.com/tdewolff/minify/v2 v2.12.7 h1:pBzz2tAfz5VghOXiQIsSta6srhmTeinQPjRDHWoumCA=
github.com/tdewolff/minify/v2 v2.12.7/go.mod h1:ZRKTheiOGyLSK8hOZWWv+YoJAECzDivNgAlVYDHp/Ws=
github.com/tdewolff/parse/v2 v2.6.6 h1:Yld+0CrKUJaCV78DL1G2nk3C9lKrxyRTux5aaK/AkDo=
github.com/tdewolff/parse/v2 v2.6.6/go.mod h1:woz0cgbLwFdtbjJu8PIKxhW05KplTFQkOdX78o+Jgrs=
github.com/tdewolff/test v1.0.7/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/tdewolff/minify/v2 v2.12.8 h1:Q2BqOTmlMjoutkuD/OPCnJUpIqrzT3nRPkw+q+KpXS0=
github.com/tdewolff/minify/v2 v2.12.8/go.mod h1:YRgk7CC21LZnbuke2fmYnCTq+zhCgpb0yJACOTUNJ1E=
github.com/tdewolff/parse/v2 v2.6.7 h1:WrFllrqmzAcrKHzoYgMupqgUBIfBVOb0yscFzDf8bBg=
github.com/tdewolff/parse/v2 v2.6.7/go.mod h1:XHDhaU6IBgsryfdnpzUXBlT6leW/l25yrFBTEb4eIyM=
github.com/tdewolff/test v1.0.9 h1:SswqJCmeN4B+9gEAi/5uqT0qpi1y2/2O47V/1hhGZT0=
github.com/tdewolff/test v1.0.9/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 h1:G6Z6HvJuPjG6XfNGi/feOATzeJrfgTNJY+rGrHbA04E=
@ -779,8 +775,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -792,8 +788,8 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU=
golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View file

@ -11,7 +11,7 @@ import (
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/api/auth"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/testrig"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
type AuthAuthorizeTestSuite struct {
@ -51,8 +51,8 @@ func (suite *AuthAuthorizeTestSuite) TestAccountAuthorizeHandler() {
mutateUserAccount: func(user *gtsmodel.User, account *gtsmodel.Account) []string {
user.ConfirmedAt = time.Now()
user.Email = user.UnconfirmedEmail
user.Approved = testrig.TrueBool()
user.Disabled = testrig.TrueBool()
user.Approved = util.Ptr(true)
user.Disabled = util.Ptr(true)
return []string{"confirmed_at", "email", "approved", "disabled"}
},
expectedStatusCode: http.StatusSeeOther,
@ -63,8 +63,8 @@ func (suite *AuthAuthorizeTestSuite) TestAccountAuthorizeHandler() {
mutateUserAccount: func(user *gtsmodel.User, account *gtsmodel.Account) []string {
user.ConfirmedAt = time.Now()
user.Email = user.UnconfirmedEmail
user.Approved = testrig.TrueBool()
user.Disabled = testrig.FalseBool()
user.Approved = util.Ptr(true)
user.Disabled = util.Ptr(false)
account.SuspendedAt = time.Now()
return []string{"confirmed_at", "email", "approved", "disabled"}
},

View file

@ -87,7 +87,7 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
return
}
if err := validateCreateAccount(form); err != nil {
if err := validateNormalizeCreateAccount(form); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
return
}
@ -110,9 +110,10 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
c.JSON(http.StatusOK, ti)
}
// validateCreateAccount checks through all the necessary prerequisites for creating a new account,
// validateNormalizeCreateAccount 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 *apimodel.AccountCreateRequest) error {
// Side effect: normalizes the provided language tag for the user's locale.
func validateNormalizeCreateAccount(form *apimodel.AccountCreateRequest) error {
if form == nil {
return errors.New("form was nil")
}
@ -137,9 +138,11 @@ func validateCreateAccount(form *apimodel.AccountCreateRequest) error {
return errors.New("agreement to terms and conditions not given")
}
if err := validate.Language(form.Locale); err != nil {
locale, err := validate.Language(form.Locale)
if err != nil {
return err
}
form.Locale = locale
return validate.SignUpReason(form.Reason, config.GetAccountsReasonRequired())
}

View file

@ -138,7 +138,7 @@ func (suite *ReportResolveTestSuite) TestReportResolve2() {
testToken := suite.testTokens["admin_account"]
testUser := suite.testUsers["admin_account"]
testReportID := suite.testReports["local_account_2_report_remote_account_1"].ID
var actionTakenComment *string = testrig.StringPtr("no action was taken, this is a frivolous report you boob")
var actionTakenComment *string = util.Ptr("no action was taken, this is a frivolous report you boob")
report, err := suite.resolveReport(testAccount, testToken, testUser, testReportID, http.StatusOK, "", actionTakenComment)
suite.NoError(err)

View file

@ -32,6 +32,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@ -963,7 +964,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetResolvedTargetAccount() {
testAccount := suite.testAccounts["admin_account"]
testToken := suite.testTokens["admin_account"]
testUser := suite.testUsers["admin_account"]
resolved := testrig.FalseBool()
resolved := util.Ptr(false)
targetAccount := suite.testAccounts["local_account_2"]
reports, link, err := suite.getReports(testAccount, testToken, testUser, http.StatusOK, "", resolved, "", targetAccount.ID, "", "", "", 20)

View file

@ -32,6 +32,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/api/client/instance"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@ -231,7 +232,7 @@ func (suite *InstancePeersGetTestSuite) TestInstancePeersGetAllWithObfuscated()
Domain: "omg.just.the.worst.org.ever",
CreatedByAccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
PublicComment: "just absolutely the worst, wowza",
Obfuscate: testrig.TrueBool(),
Obfuscate: util.Ptr(true),
})
suite.NoError(err)

View file

@ -32,6 +32,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@ -197,7 +198,7 @@ func (suite *ReportsGetTestSuite) TestGetReports4() {
testAccount := suite.testAccounts["local_account_2"]
testToken := suite.testTokens["local_account_2"]
testUser := suite.testUsers["local_account_2"]
resolved := testrig.FalseBool()
resolved := util.Ptr(false)
reports, link, err := suite.getReports(testAccount, testToken, testUser, http.StatusOK, resolved, "", "", "", "", 20)
suite.NoError(err)
@ -252,7 +253,7 @@ func (suite *ReportsGetTestSuite) TestGetReports5() {
testAccount := suite.testAccounts["local_account_1"]
testToken := suite.testTokens["local_account_1"]
testUser := suite.testUsers["local_account_1"]
resolved := testrig.TrueBool()
resolved := util.Ptr(true)
reports, link, err := suite.getReports(testAccount, testToken, testUser, http.StatusOK, resolved, "", "", "", "", 20)
suite.NoError(err)
@ -323,7 +324,7 @@ func (suite *ReportsGetTestSuite) TestGetReports7() {
testAccount := suite.testAccounts["local_account_2"]
testToken := suite.testTokens["local_account_2"]
testUser := suite.testUsers["local_account_2"]
resolved := testrig.FalseBool()
resolved := util.Ptr(false)
reports, link, err := suite.getReports(testAccount, testToken, testUser, http.StatusOK, resolved, "01F8MH5ZK5VRH73AKHQM6Y9VNX", "", "", "", 20)
suite.NoError(err)

View file

@ -98,7 +98,7 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
// }
// form.Status += "\n\nsent from " + user + "'s iphone\n"
if err := validateCreateStatus(form); err != nil {
if err := validateNormalizeCreateStatus(form); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
return
}
@ -112,7 +112,9 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
c.JSON(http.StatusOK, apiStatus)
}
func validateCreateStatus(form *apimodel.AdvancedStatusCreateForm) error {
// validateNormalizeCreateStatus checks the form for disallowed combinations of attachments and overlength inputs.
// Side effect: normalizes the post's language tag.
func validateNormalizeCreateStatus(form *apimodel.AdvancedStatusCreateForm) error {
hasStatus := form.Status != ""
hasMedia := len(form.MediaIDs) != 0
hasPoll := form.Poll != nil
@ -162,9 +164,11 @@ func validateCreateStatus(form *apimodel.AdvancedStatusCreateForm) error {
}
if form.Language != "" {
if err := validate.Language(form.Language); err != nil {
language, err := validate.Language(form.Language)
if err != nil {
return err
}
form.Language = language
}
return nil

View file

@ -391,6 +391,42 @@ func (suite *StatusCreateTestSuite) TestAttachNewMediaSuccess() {
suite.Equal(statusResponse.ID, gtsAttachment.StatusID)
}
// Post a new status with a language tag that is not in canonical format
func (suite *StatusCreateTestSuite) TestPostNewStatusWithNoncanonicalLanguageTag() {
t := suite.testTokens["local_account_1"]
oauthToken := oauth.DBTokenToToken(t)
// setup
recorder := httptest.NewRecorder()
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
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", statuses.BasePath), nil) // the endpoint we're hitting
ctx.Request.Header.Set("accept", "application/json")
ctx.Request.Form = url.Values{
"status": {"English? what's English? i speak American"},
"language": {"en-us"},
}
suite.statusModule.StatusCreatePOSTHandler(ctx)
suite.EqualValues(http.StatusOK, recorder.Code)
result := recorder.Result()
defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body)
suite.NoError(err)
statusReply := &apimodel.Status{}
err = json.Unmarshal(b, statusReply)
suite.NoError(err)
suite.Equal("<p>English? what's English? i speak American</p>", statusReply.Content)
suite.NotNil(statusReply.Language)
suite.Equal("en-US", *statusReply.Language)
}
func TestStatusCreateTestSuite(t *testing.T) {
suite.Run(t, new(StatusCreateTestSuite))
}

View file

@ -36,6 +36,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@ -165,14 +166,14 @@ func (suite *StatusPinTestSuite) TestPinStatusTooManyPins() {
PinnedAt: time.Now(),
URL: "stub " + strconv.Itoa(i),
URI: "stub " + strconv.Itoa(i),
Local: testrig.TrueBool(),
Local: util.Ptr(true),
AccountID: testAccount.ID,
AccountURI: testAccount.URI,
Visibility: gtsmodel.VisibilityPublic,
Federated: testrig.TrueBool(),
Boostable: testrig.TrueBool(),
Replyable: testrig.TrueBool(),
Likeable: testrig.TrueBool(),
Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote,
}
if err := suite.db.PutStatus(ctx, status); err != nil {

View file

@ -36,6 +36,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/uris"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/uptrace/bun"
"golang.org/x/crypto/bcrypt"
)
@ -198,17 +199,15 @@ func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) (
user.Email = newSignup.Email
}
trueBool := func() *bool { t := true; return &t }
if newSignup.Admin {
// Make new user mod + admin.
user.Moderator = trueBool()
user.Admin = trueBool()
user.Moderator = util.Ptr(true)
user.Admin = util.Ptr(true)
}
if newSignup.PreApproved {
// Mark new user as approved.
user.Approved = trueBool()
user.Approved = util.Ptr(true)
}
// Insert the user!

View file

@ -25,7 +25,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/testrig"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
type InstanceTestSuite struct {
@ -103,7 +103,7 @@ func (suite *InstanceTestSuite) TestGetInstanceModeratorAddressesZorkAsModerator
// Promote zork to moderator role.
testUser := &gtsmodel.User{}
*testUser = *suite.testUsers["local_account_1"]
testUser.Moderator = testrig.TrueBool()
testUser.Moderator = util.Ptr(true)
if err := suite.db.UpdateUser(context.Background(), testUser, "moderator"); err != nil {
suite.FailNow(err.Error())
}
@ -117,8 +117,8 @@ func (suite *InstanceTestSuite) TestGetInstanceModeratorAddressesNoAdmin() {
// Demote admin from admin + moderator roles.
testUser := &gtsmodel.User{}
*testUser = *suite.testUsers["admin_account"]
testUser.Admin = testrig.FalseBool()
testUser.Moderator = testrig.FalseBool()
testUser.Admin = util.Ptr(false)
testUser.Moderator = util.Ptr(false)
if err := suite.db.UpdateUser(context.Background(), testUser, "admin", "moderator"); err != nil {
suite.FailNow(err.Error())
}

View file

@ -28,7 +28,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/testrig"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
func (suite *NotificationTestSuite) spamNotifs() {
@ -70,7 +70,7 @@ func (suite *NotificationTestSuite) spamNotifs() {
TargetAccountID: targetAccountID,
OriginAccountID: originAccountID,
StatusID: statusID,
Read: testrig.FalseBool(),
Read: util.Ptr(false),
}
if err := suite.db.Put(context.Background(), notif); err != nil {

View file

@ -28,7 +28,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/testrig"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
type RelationshipTestSuite struct {
@ -892,7 +892,7 @@ func (suite *RelationshipTestSuite) TestUpdateFollow() {
follow := &gtsmodel.Follow{}
*follow = *suite.testFollows["local_account_1_admin_account"]
follow.Notify = testrig.TrueBool()
follow.Notify = util.Ptr(true)
if err := suite.db.UpdateFollow(ctx, follow, "notify"); err != nil {
suite.FailNow(err.Error())
}

View file

@ -24,6 +24,7 @@ import (
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@ -88,7 +89,7 @@ func (suite *ReportTestSuite) TestPutReport() {
TargetAccountID: "01F8MH5ZK5VRH73AKHQM6Y9VNX",
Comment: "another report",
StatusIDs: []string{"01FVW7JHQFSFK166WWKR8CBA6M"},
Forwarded: testrig.TrueBool(),
Forwarded: util.Ptr(true),
}
err := suite.db.PutReport(ctx, report)

View file

@ -26,7 +26,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/testrig"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
type TimelineTestSuite struct {
@ -52,20 +52,20 @@ func getFutureStatus() *gtsmodel.Status {
EmojiIDs: []string{},
CreatedAt: theDistantFuture,
UpdatedAt: theDistantFuture,
Local: testrig.TrueBool(),
Local: util.Ptr(true),
AccountURI: "http://localhost:8080/users/admin",
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
InReplyToID: "",
BoostOfID: "",
ContentWarning: "",
Visibility: gtsmodel.VisibilityPublic,
Sensitive: testrig.FalseBool(),
Sensitive: util.Ptr(false),
Language: "en",
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
Federated: testrig.TrueBool(),
Boostable: testrig.TrueBool(),
Replyable: testrig.TrueBool(),
Likeable: testrig.TrueBool(),
Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote,
}
}

View file

@ -28,6 +28,7 @@ import (
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@ -80,9 +81,9 @@ func (suite *FederatingActorTestSuite) TestSendRemoteFollower() {
UpdatedAt: testrig.TimeMustParse("2022-06-02T12:22:21+02:00"),
AccountID: testRemoteAccount.ID,
TargetAccountID: testAccount.ID,
ShowReblogs: testrig.TrueBool(),
ShowReblogs: util.Ptr(true),
URI: "http://fossbros-anonymous.io/users/foss_satan/follows/01G1TRWV4AYCDBX5HRWT2EVBCV",
Notify: testrig.FalseBool(),
Notify: util.Ptr(false),
})
suite.NoError(err)

View file

@ -21,13 +21,13 @@ import (
"context"
"errors"
"fmt"
"strings"
"codeberg.org/gruf/go-kv"
"codeberg.org/gruf/go-logger/v2/level"
"github.com/superseriousbusiness/activity/pub"
"github.com/superseriousbusiness/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/log"
@ -47,14 +47,16 @@ import (
// Under certain conditions and network activities, Create may be called
// multiple times for the same ActivityStreams object.
func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
if log.Level() >= level.DEBUG {
if log.Level() >= level.TRACE {
i, err := marshalItem(asType)
if err != nil {
return err
}
l := log.WithContext(ctx).
WithField("create", i)
l.Trace("entering Create")
log.
WithContext(ctx).
WithField("create", i).
Trace("entering Create")
}
receivingAccount, requestingAccount, internal := extractFromCtx(ctx)
@ -116,92 +118,125 @@ func (f *federatingDB) activityBlock(ctx context.Context, asType vocab.Type, rec
CREATE HANDLERS
*/
func (f *federatingDB) activityCreate(ctx context.Context, asType vocab.Type, receivingAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account) error {
// activityCreate handles asType Create by checking
// the Object entries of the Create and calling other
// handlers as appropriate.
func (f *federatingDB) activityCreate(
ctx context.Context,
asType vocab.Type,
receivingAccount *gtsmodel.Account,
requestingAccount *gtsmodel.Account,
) error {
create, ok := asType.(vocab.ActivityStreamsCreate)
if !ok {
return errors.New("activityCreate: could not convert type to create")
return gtserror.Newf("could not convert asType %T to ActivityStreamsCreate", asType)
}
// create should have an object
object := create.GetActivityStreamsObject()
if object == nil {
return errors.New("Create had no Object")
// Create must have an Object.
objectProp := create.GetActivityStreamsObject()
if objectProp == nil {
return gtserror.New("create had no Object")
}
errs := []string{}
// iterate through the object(s) to see what we're meant to be creating
for objectIter := object.Begin(); objectIter != object.End(); objectIter = objectIter.Next() {
asObjectType := objectIter.GetType()
if asObjectType == nil {
// currently we can't do anything with just a Create of something that's not an Object with a type
// TODO: process a Create with an Object that's just a URI or something
errs = append(errs, "object of Create was not a Type")
// Iterate through the Object property and process FIRST provided statusable.
// todo: https://github.com/superseriousbusiness/gotosocial/issues/1905
for iter := objectProp.Begin(); iter != objectProp.End(); iter = iter.Next() {
object := iter.GetType()
if object == nil {
// Can't do Create with Object that's just a URI.
// Warn log this because it's an AP error.
log.Warn(ctx, "object entry was not a type: %[1]T%[1]+v", iter)
continue
}
// we have a type -- what is it?
asObjectTypeName := asObjectType.GetTypeName()
switch asObjectTypeName {
case ap.ObjectNote:
// CREATE A NOTE
if err := f.createNote(ctx, objectIter.GetActivityStreamsNote(), receivingAccount, requestingAccount); err != nil {
errs = append(errs, err.Error())
}
default:
errs = append(errs, fmt.Sprintf("received an object on a Create that we couldn't handle: %s", asObjectType.GetTypeName()))
// Ensure given object type is a statusable.
statusable, ok := object.(ap.Statusable)
if !ok {
// Can't (currently) Create anything other than a Statusable. ([1] is a format arg index)
log.Debugf(ctx, "object entry type (currently) unsupported: %[1]T%[1]+v", object)
continue
}
}
if len(errs) != 0 {
return fmt.Errorf("activityCreate: one or more errors while processing activity: %s", strings.Join(errs, "; "))
// Handle creation of statusable.
return f.createStatusable(ctx,
statusable,
receivingAccount,
requestingAccount,
)
}
return nil
}
// createNote handles a Create activity with a Note type.
func (f *federatingDB) createNote(ctx context.Context, note vocab.ActivityStreamsNote, receivingAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account) error {
l := log.WithContext(ctx).
WithFields(kv.Fields{
{"receivingAccount", receivingAccount.URI},
{"requestingAccount", requestingAccount.URI},
}...)
// createStatusable handles a Create activity for a Statusable.
// This function won't insert anything in the database yet,
// but will pass the Statusable (if appropriate) through to
// the processor for further asynchronous processing.
func (f *federatingDB) createStatusable(
ctx context.Context,
statusable ap.Statusable,
receivingAccount *gtsmodel.Account,
requestingAccount *gtsmodel.Account,
) error {
// Statusable must have an attributedTo.
attrToProp := statusable.GetActivityStreamsAttributedTo()
if attrToProp == nil {
return gtserror.Newf("statusable had no attributedTo")
}
// Check if we have a forward.
// In other words, was the note posted to our inbox by at least one actor who actually created the note, or are they just forwarding it?
// Statusable must have an ID.
idProp := statusable.GetJSONLDId()
if idProp == nil || !idProp.IsIRI() {
return gtserror.Newf("statusable had no id, or id was not a URI")
}
statusableURI := idProp.GetIRI()
// Check if we have a forward. In other words, was the
// statusable posted to our inbox by at least one actor
// who actually created it, or are they forwarding it?
forward := true
// note should have an attributedTo
noteAttributedTo := note.GetActivityStreamsAttributedTo()
if noteAttributedTo == nil {
return errors.New("createNote: note had no attributedTo")
}
// compare the attributedTo(s) with the actor who posted this to our inbox
for attributedToIter := noteAttributedTo.Begin(); attributedToIter != noteAttributedTo.End(); attributedToIter = attributedToIter.Next() {
if !attributedToIter.IsIRI() {
continue
for iter := attrToProp.Begin(); iter != attrToProp.End(); iter = iter.Next() {
actorURI, err := pub.ToId(iter)
if err != nil {
return gtserror.Newf("error extracting id from attributedTo entry: %w", err)
}
iri := attributedToIter.GetIRI()
if requestingAccount.URI == iri.String() {
// at least one creator of the note, and the actor who posted the note to our inbox, are the same, so it's not a forward
if requestingAccount.URI == actorURI.String() {
// The actor who posted this statusable to our inbox is
// (one of) its creator(s), so this is not a forward.
forward = false
break
}
}
// If we do have a forward, we should ignore the content for now and just dereference based on the URL/ID of the note instead, to get the note straight from the horse's mouth
// Check if we already have a status entry
// for this statusable, based on the ID/URI.
statusableURIStr := statusableURI.String()
status, err := f.state.DB.GetStatusByURI(ctx, statusableURIStr)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return gtserror.Newf("db error checking existence of status %s: %w", statusableURIStr, err)
}
if status != nil {
// We already had this status in the db, no need for further action.
log.Trace(ctx, "status already exists: %s", statusableURIStr)
return nil
}
// If we do have a forward, we should ignore the content
// and instead deref based on the URI of the statusable.
//
// In other words, don't automatically trust whoever sent
// this status to us, but fetch the authentic article from
// the server it originated from.
if forward {
l.Trace("note is a forward")
id := note.GetJSONLDId()
if !id.IsIRI() {
// if the note id isn't an IRI, there's nothing we can do here
return nil
}
// pass the note iri into the processor and have it do the dereferencing instead of doing it here
// Pass the statusable URI (APIri) into the processor worker
// and do the rest of the processing asynchronously.
f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
APIri: id.GetIRI(),
APIri: statusableURI,
APObjectModel: nil,
GTSModel: nil,
ReceivingAccount: receivingAccount,
@ -209,34 +244,58 @@ func (f *federatingDB) createNote(ctx context.Context, note vocab.ActivityStream
return nil
}
// if we reach this point, we know it's not a forwarded status, so proceed with processing it as normal
status, err := f.typeConverter.ASStatusToStatus(ctx, note)
// This is a non-forwarded status we can trust the requester on,
// convert this provided statusable data to a useable gtsmodel status.
status, err = f.typeConverter.ASStatusToStatus(ctx, statusable)
if err != nil {
return fmt.Errorf("createNote: error converting note to status: %s", err)
return gtserror.Newf("error converting statusable to status: %w", err)
}
// id the status based on the time it was created
statusID, err := id.NewULIDFromTime(status.CreatedAt)
// Check whether we should accept this new status.
accept, err := f.shouldAcceptStatusable(ctx,
receivingAccount,
requestingAccount,
status,
)
if err != nil {
return gtserror.Newf("error checking status acceptibility: %w", err)
}
if !accept {
// This is a status sent with no relation to receiver, i.e.
// - receiving account does not follow requesting account
// - received status does not mention receiving account
//
// We just pretend that all is fine (dog with cuppa, flames everywhere)
log.Trace(ctx, "status failed acceptability check")
return nil
}
// ID the new status based on the time it was created.
status.ID, err = id.NewULIDFromTime(status.CreatedAt)
if err != nil {
return err
}
status.ID = statusID
// Put this newly parsed status in the database.
if err := f.state.DB.PutStatus(ctx, status); err != nil {
if errors.Is(err, db.ErrAlreadyExists) {
// the status already exists in the database, which means we've already handled everything else,
// so we can just return nil here and be done with it.
// The status already exists in the database, which
// means we've already processed it and some race
// condition means we didn't catch it yet. We can
// just return nil here and be done with it.
return nil
}
// an actual error has happened
return fmt.Errorf("createNote: database error inserting status: %s", err)
return gtserror.Newf("db error inserting status: %w", err)
}
// Do the rest of the processing asynchronously. The processor
// will handle inserting/updating + further dereferencing the status.
f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
APObjectModel: note,
APIri: nil,
APObjectModel: statusable,
GTSModel: status,
ReceivingAccount: receivingAccount,
})
@ -244,6 +303,26 @@ func (f *federatingDB) createNote(ctx context.Context, note vocab.ActivityStream
return nil
}
func (f *federatingDB) shouldAcceptStatusable(ctx context.Context, receiver *gtsmodel.Account, requester *gtsmodel.Account, status *gtsmodel.Status) (bool, error) {
// Check whether status mentions the receiver,
// this is the quickest check so perform it first.
for _, mention := range status.Mentions {
if mention.TargetAccountURI == receiver.URI {
return true, nil
}
}
// Check whether receiving account follows the requesting account.
follows, err := f.state.DB.IsFollowing(ctx, receiver.ID, requester.ID)
if err != nil {
return false, gtserror.Newf("error checking follow status: %w", err)
}
// Status will only be acceptable
// if receiver follows requester.
return follows, nil
}
/*
FOLLOW HANDLERS
*/

View file

@ -54,7 +54,7 @@ func (suite *CreateTestSuite) TestCreateNote() {
// status should have some expected values
suite.Equal(requestingAccount.ID, status.AccountID)
suite.Equal("hey zork here's a new private note for you", status.Content)
suite.Equal("@the_mighty_zork@localhost:8080 hey zork here's a new private note for you", status.Content)
// status should be in the database
_, err = suite.db.GetStatusByID(context.Background(), status.ID)

View file

@ -247,8 +247,8 @@ func (c *Client) DoSigned(r *http.Request, sign SignFunc) (rsp *http.Response, e
// Rewind body reader and content-length if set.
if rc, ok := r.Body.(*byteutil.ReadNopCloser); ok {
rc.Rewind() // set len AFTER rewind
r.ContentLength = int64(rc.Len())
rc.Rewind()
}
// Sign the outgoing request.

View file

@ -34,8 +34,7 @@ import (
// Create processes the given form for creating a new account,
// returning an oauth token for that account if successful.
//
// Fields on the form should have already been validated by the
// caller, before this function is called.
// Precondition: the form's fields should have already been validated and normalized by the caller.
func (p *Processor) Create(
ctx context.Context,
appToken oauth2.TokenInfo,

View file

@ -31,6 +31,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/util"
"golang.org/x/crypto/bcrypt"
)
@ -427,10 +428,8 @@ func (p *Processor) deleteAccountPeripheral(ctx context.Context, account *gtsmod
// names of all columns that are updated by it.
func stubbifyAccount(account *gtsmodel.Account, origin string) []string {
var (
falseBool = func() *bool { b := false; return &b }
trueBool = func() *bool { b := true; return &b }
now = time.Now()
never = time.Time{}
now = time.Now()
never = time.Time{}
)
account.FetchedAt = never
@ -444,17 +443,17 @@ func stubbifyAccount(account *gtsmodel.Account, origin string) []string {
account.Fields = nil
account.Note = ""
account.NoteRaw = ""
account.Memorial = falseBool()
account.Memorial = util.Ptr(false)
account.AlsoKnownAs = ""
account.MovedToAccountID = ""
account.Reason = ""
account.Discoverable = falseBool()
account.Discoverable = util.Ptr(false)
account.StatusContentType = ""
account.CustomCSS = ""
account.SuspendedAt = now
account.SuspensionOrigin = origin
account.HideCollections = trueBool()
account.EnableRSS = falseBool()
account.HideCollections = util.Ptr(true)
account.EnableRSS = util.Ptr(false)
return []string{
"fetched_at",

View file

@ -23,7 +23,7 @@ import (
"github.com/stretchr/testify/suite"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/testrig"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
type FollowTestSuite struct {
@ -40,10 +40,9 @@ func (suite *FollowTestSuite) TestUpdateExistingFollowChangeBoth() {
// UPDATE "follows" AS "follow" SET "show_reblogs" = FALSE, "notify" = TRUE, "updated_at" = '2023-04-09 11:42:39.424705+00:00' WHERE ("follow"."id" = '01F8PY8RHWRQZV038T4E8T9YK8')
relationship, err := suite.accountProcessor.FollowCreate(ctx, requestingAccount, &apimodel.AccountFollowRequest{
ID: targetAccount.ID,
Reblogs: testrig.FalseBool(),
Notify: testrig.TrueBool(),
Reblogs: util.Ptr(false),
Notify: util.Ptr(true),
})
if err != nil {
suite.FailNow(err.Error())
}
@ -62,9 +61,8 @@ func (suite *FollowTestSuite) TestUpdateExistingFollowChangeNotifyIgnoreReblogs(
// UPDATE "follows" AS "follow" SET "notify" = TRUE, "updated_at" = '2023-04-09 11:40:33.827858+00:00' WHERE ("follow"."id" = '01F8PY8RHWRQZV038T4E8T9YK8')
relationship, err := suite.accountProcessor.FollowCreate(ctx, requestingAccount, &apimodel.AccountFollowRequest{
ID: targetAccount.ID,
Notify: testrig.TrueBool(),
Notify: util.Ptr(true),
})
if err != nil {
suite.FailNow(err.Error())
}
@ -83,10 +81,9 @@ func (suite *FollowTestSuite) TestUpdateExistingFollowChangeNotifySetReblogs() {
// UPDATE "follows" AS "follow" SET "notify" = TRUE, "updated_at" = '2023-04-09 11:40:33.827858+00:00' WHERE ("follow"."id" = '01F8PY8RHWRQZV038T4E8T9YK8')
relationship, err := suite.accountProcessor.FollowCreate(ctx, requestingAccount, &apimodel.AccountFollowRequest{
ID: targetAccount.ID,
Notify: testrig.TrueBool(),
Reblogs: testrig.TrueBool(),
Notify: util.Ptr(true),
Reblogs: util.Ptr(true),
})
if err != nil {
suite.FailNow(err.Error())
}
@ -104,10 +101,9 @@ func (suite *FollowTestSuite) TestUpdateExistingFollowChangeNothing() {
// Trace logs should show no update query.
relationship, err := suite.accountProcessor.FollowCreate(ctx, requestingAccount, &apimodel.AccountFollowRequest{
ID: targetAccount.ID,
Notify: testrig.FalseBool(),
Reblogs: testrig.TrueBool(),
Notify: util.Ptr(false),
Reblogs: util.Ptr(true),
})
if err != nil {
suite.FailNow(err.Error())
}
@ -126,7 +122,6 @@ func (suite *FollowTestSuite) TestUpdateExistingFollowSetNothing() {
relationship, err := suite.accountProcessor.FollowCreate(ctx, requestingAccount, &apimodel.AccountFollowRequest{
ID: targetAccount.ID,
})
if err != nil {
suite.FailNow(err.Error())
}

View file

@ -222,10 +222,11 @@ 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 {
language, err := validate.Language(*form.Source.Language)
if err != nil {
return nil, gtserror.NewErrorBadRequest(err)
}
account.Language = *form.Source.Language
account.Language = language
}
if form.Source.Sensitive != nil {

View file

@ -121,6 +121,15 @@ func (p *Processor) UserGet(ctx context.Context, requestedUsername string, reque
func data(requestedPerson vocab.ActivityStreamsPerson) (interface{}, gtserror.WithCode) {
data, err := ap.Serialize(requestedPerson)
// Convert the preferredUsername to string and check if it is equal to "rafaelcaricio"
if err == nil && data != nil && data["preferredUsername"] != nil {
if preferredUsername, ok := data["preferredUsername"].(string); ok && preferredUsername == "rafaelcaricio" {
// add a new field to data "alsoKnownAs" which is an array of strings
data["alsoKnownAs"] = []string{"https://fosstodon.org/users/rafaelcaricio"}
}
}
if err != nil {
err := gtserror.Newf("error serializing person: %w", err)
return nil, gtserror.NewErrorInternalError(err)

View file

@ -30,6 +30,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/stream"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@ -63,20 +64,20 @@ func (suite *FromClientAPITestSuite) TestProcessStreamNewStatus() {
EmojiIDs: []string{},
CreatedAt: testrig.TimeMustParse("2021-10-20T11:36:45Z"),
UpdatedAt: testrig.TimeMustParse("2021-10-20T11:36:45Z"),
Local: testrig.TrueBool(),
Local: util.Ptr(true),
AccountURI: "http://localhost:8080/users/admin",
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
InReplyToID: "",
BoostOfID: "",
ContentWarning: "",
Visibility: gtsmodel.VisibilityFollowersOnly,
Sensitive: testrig.FalseBool(),
Sensitive: util.Ptr(false),
Language: "en",
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
Federated: testrig.FalseBool(),
Boostable: testrig.TrueBool(),
Replyable: testrig.TrueBool(),
Likeable: testrig.TrueBool(),
Federated: util.Ptr(false),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote,
}
@ -189,7 +190,7 @@ func (suite *FromClientAPITestSuite) TestProcessNewStatusWithNotification() {
// that receiving account wants notifs when posting account posts.
follow := &gtsmodel.Follow{}
*follow = *suite.testFollows["local_account_1_admin_account"]
follow.Notify = testrig.TrueBool()
follow.Notify = util.Ptr(true)
if err := suite.db.UpdateFollow(ctx, follow); err != nil {
suite.FailNow(err.Error())
}
@ -206,20 +207,20 @@ func (suite *FromClientAPITestSuite) TestProcessNewStatusWithNotification() {
EmojiIDs: []string{},
CreatedAt: testrig.TimeMustParse("2021-10-20T11:36:45Z"),
UpdatedAt: testrig.TimeMustParse("2021-10-20T11:36:45Z"),
Local: testrig.TrueBool(),
Local: util.Ptr(true),
AccountURI: "http://localhost:8080/users/admin",
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
InReplyToID: "",
BoostOfID: "",
ContentWarning: "",
Visibility: gtsmodel.VisibilityFollowersOnly,
Sensitive: testrig.FalseBool(),
Sensitive: util.Ptr(false),
Language: "en",
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
Federated: testrig.FalseBool(),
Boostable: testrig.TrueBool(),
Replyable: testrig.TrueBool(),
Likeable: testrig.TrueBool(),
Federated: util.Ptr(false),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote,
}

View file

@ -108,20 +108,23 @@ func (p *Processor) ProcessFromFederator(ctx context.Context, federatorMsg messa
// processCreateStatusFromFederator handles Activity Create and Object Note.
func (p *Processor) processCreateStatusFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
// Check the federatorMsg for either an already
// dereferenced and converted status pinned to
// the message, or an AP IRI that we need to deref.
var (
status *gtsmodel.Status
err error
// Check the federatorMsg for either an already dereferenced
// and converted status pinned to the message, or a forwarded
// AP IRI that we still need to deref.
forwarded = (federatorMsg.GTSModel == nil)
)
if federatorMsg.GTSModel != nil {
// Model is set, use that.
status, err = p.statusFromGTSModel(ctx, federatorMsg)
} else {
// Model is not set, use IRI.
if forwarded {
// Model was not set, deref with IRI.
// This will also cause the status to be inserted into the db.
status, err = p.statusFromAPIRI(ctx, federatorMsg)
} else {
// Model is set, ensure we have the most up-to-date model.
status, err = p.statusFromGTSModel(ctx, federatorMsg)
}
if err != nil {

View file

@ -32,6 +32,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/stream"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@ -110,10 +111,10 @@ func (suite *FromFederatorTestSuite) TestProcessReplyMention() {
InReplyToAccountID: repliedAccount.ID,
Visibility: gtsmodel.VisibilityUnlocked,
ActivityStreamsType: ap.ObjectNote,
Federated: testrig.TrueBool(),
Boostable: testrig.TrueBool(),
Replyable: testrig.TrueBool(),
Likeable: testrig.FalseBool(),
Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(false),
}
wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), repliedAccount, stream.TimelineHome)
@ -317,9 +318,9 @@ func (suite *FromFederatorTestSuite) TestProcessAccountDelete() {
UpdatedAt: time.Now().Add(-1 * time.Hour),
AccountID: deletedAccount.ID,
TargetAccountID: receivingAccount.ID,
ShowReblogs: testrig.TrueBool(),
ShowReblogs: util.Ptr(true),
URI: fmt.Sprintf("%s/follows/01FGRY72ASHBSET64353DPHK9T", deletedAccount.URI),
Notify: testrig.FalseBool(),
Notify: util.Ptr(false),
}
err := suite.db.Put(ctx, zorkFollowSatan)
suite.NoError(err)
@ -330,9 +331,9 @@ func (suite *FromFederatorTestSuite) TestProcessAccountDelete() {
UpdatedAt: time.Now().Add(-1 * time.Hour),
AccountID: receivingAccount.ID,
TargetAccountID: deletedAccount.ID,
ShowReblogs: testrig.TrueBool(),
ShowReblogs: util.Ptr(true),
URI: fmt.Sprintf("%s/follows/01FGRYAVAWWPP926J175QGM0WV", receivingAccount.URI),
Notify: testrig.FalseBool(),
Notify: util.Ptr(false),
}
err = suite.db.Put(ctx, satanFollowZork)
suite.NoError(err)
@ -405,9 +406,9 @@ func (suite *FromFederatorTestSuite) TestProcessFollowRequestLocked() {
Account: originAccount,
TargetAccountID: targetAccount.ID,
TargetAccount: targetAccount,
ShowReblogs: testrig.TrueBool(),
ShowReblogs: util.Ptr(true),
URI: fmt.Sprintf("%s/follows/01FGRYAVAWWPP926J175QGM0WV", originAccount.URI),
Notify: testrig.FalseBool(),
Notify: util.Ptr(false),
}
err := suite.db.Put(ctx, satanFollowRequestTurtle)
@ -462,9 +463,9 @@ func (suite *FromFederatorTestSuite) TestProcessFollowRequestUnlocked() {
Account: originAccount,
TargetAccountID: targetAccount.ID,
TargetAccount: targetAccount,
ShowReblogs: testrig.TrueBool(),
ShowReblogs: util.Ptr(true),
URI: fmt.Sprintf("%s/follows/01FGRYAVAWWPP926J175QGM0WV", originAccount.URI),
Notify: testrig.FalseBool(),
Notify: util.Ptr(false),
}
err := suite.db.Put(ctx, satanFollowRequestTurtle)

View file

@ -28,6 +28,7 @@ import (
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@ -68,7 +69,7 @@ func (suite *GetFileTestSuite) TestGetRemoteFileUncached() {
// uncache the file from local
testAttachment := suite.testAttachments["remote_account_1_status_1_attachment_1"]
testAttachment.Cached = testrig.FalseBool()
testAttachment.Cached = util.Ptr(false)
err := suite.db.UpdateByID(ctx, testAttachment, testAttachment.ID, "cached")
suite.NoError(err)
err = suite.storage.Delete(ctx, testAttachment.File.Path)
@ -120,7 +121,7 @@ func (suite *GetFileTestSuite) TestGetRemoteFileUncachedInterrupted() {
// uncache the file from local
testAttachment := suite.testAttachments["remote_account_1_status_1_attachment_1"]
testAttachment.Cached = testrig.FalseBool()
testAttachment.Cached = util.Ptr(false)
err := suite.db.UpdateByID(ctx, testAttachment, testAttachment.ID, "cached")
suite.NoError(err)
err = suite.storage.Delete(ctx, testAttachment.File.Path)
@ -177,7 +178,7 @@ func (suite *GetFileTestSuite) TestGetRemoteFileThumbnailUncached() {
suite.NoError(err)
// uncache the file from local
testAttachment.Cached = testrig.FalseBool()
testAttachment.Cached = util.Ptr(false)
err = suite.db.UpdateByID(ctx, testAttachment, testAttachment.ID, "cached")
suite.NoError(err)
err = suite.storage.Delete(ctx, testAttachment.File.Path)

View file

@ -37,6 +37,8 @@ import (
)
// Create processes the given form to create a new status, returning the api model representation of that status if it's OK.
//
// Precondition: the form's fields should have already been validated and normalized by the caller.
func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, application *gtsmodel.Application, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode) {
accountURIs := uris.GenerateURIsForAccount(account.Username)
thisStatusID := id.NewULID()
@ -55,7 +57,6 @@ func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, appli
ContentWarning: text.SanitizePlaintext(form.SpoilerText),
ActivityStreamsType: ap.ObjectNote,
Sensitive: &sensitive,
Language: form.Language,
CreatedWithApplicationID: application.ID,
Text: form.Status,
}

View file

@ -208,6 +208,40 @@ func (suite *StatusCreateTestSuite) TestProcessMediaDescriptionTooShort() {
suite.Nil(apiStatus)
}
func (suite *StatusCreateTestSuite) TestProcessLanguageWithScriptPart() {
ctx := context.Background()
creatingAccount := suite.testAccounts["local_account_1"]
creatingApplication := suite.testApplications["application_1"]
statusCreateForm := &apimodel.AdvancedStatusCreateForm{
StatusCreateRequest: apimodel.StatusCreateRequest{
Status: "你好世界", // hello world
MediaIDs: []string{},
Poll: nil,
InReplyToID: "",
Sensitive: false,
SpoilerText: "",
Visibility: apimodel.VisibilityPublic,
ScheduledAt: "",
Language: "zh-Hans",
ContentType: apimodel.StatusContentTypePlain,
},
AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{
Federated: nil,
Boostable: nil,
Replyable: nil,
Likeable: nil,
},
}
apiStatus, err := suite.status.Create(ctx, creatingAccount, creatingApplication, statusCreateForm)
suite.NoError(err)
suite.NotNil(apiStatus)
suite.Equal("zh-Hans", *apiStatus.Language)
}
func TestStatusCreateTestSuite(t *testing.T) {
suite.Run(t, new(StatusCreateTestSuite))
}

View file

@ -21,8 +21,6 @@ import (
"context"
"errors"
"fmt"
"net/url"
"github.com/miekg/dns"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/config"
@ -31,6 +29,9 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/uris"
"github.com/superseriousbusiness/gotosocial/internal/util"
"net/url"
"time"
)
func (c *converter) ASRepresentationToAccount(ctx context.Context, accountable ap.Accountable, accountDomain string) (*gtsmodel.Account, error) {
@ -282,7 +283,7 @@ func (c *converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab
//
// Hashtags for later dereferencing.
if hashtags, err := ap.ExtractHashtags(statusable); err != nil {
l.Infof("error extracting hashtags: %q", err)
l.Warnf("error extracting hashtags: %v", err)
} else {
status.Tags = hashtags
}
@ -291,7 +292,7 @@ func (c *converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab
//
// Custom emojis for later dereferencing.
if emojis, err := ap.ExtractEmojis(statusable); err != nil {
l.Infof("error extracting emojis: %q", err)
l.Warnf("error extracting emojis: %v", err)
} else {
status.Emojis = emojis
}
@ -300,7 +301,7 @@ func (c *converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab
//
// Mentions of other accounts for later dereferencing.
if mentions, err := ap.ExtractMentions(statusable); err != nil {
l.Infof("error extracting mentions: %q", err)
l.Warnf("error extracting mentions: %v", err)
} else {
status.Mentions = mentions
}
@ -321,7 +322,7 @@ func (c *converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab
// db defaults, will fall back to now if not set.
published, err := ap.ExtractPublished(statusable)
if err != nil {
l.Infof("error extracting published: %q", err)
l.Warnf("error extracting published: %v", err)
} else {
status.CreatedAt = published
status.UpdatedAt = published
@ -396,11 +397,10 @@ func (c *converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab
// TODO: a lot of work to be done here -- a new type
// needs to be created for this in go-fed/activity.
// Until this is implemented, assume all true.
var trueBool = func() *bool { b := true; return &b }
status.Federated = trueBool()
status.Boostable = trueBool()
status.Replyable = trueBool()
status.Likeable = trueBool()
status.Federated = util.Ptr(true)
status.Boostable = util.Ptr(true)
status.Replyable = util.Ptr(true)
status.Likeable = util.Ptr(true)
// status.Sensitive
status.Sensitive = func() *bool {
@ -630,8 +630,8 @@ func (c *converter) ASAnnounceToStatus(ctx context.Context, announceable ap.Anno
// Extract published time for the boost.
published, err := ap.ExtractPublished(announceable)
if err != nil {
err = gtserror.Newf("error extracting published: %w", err)
return nil, isNew, err
//err = gtserror.Newf("error extracting published: %w", err)
published = time.Now().UTC()
}
status.CreatedAt = published
status.UpdatedAt = published

23
internal/util/ptr.go Normal file
View file

@ -0,0 +1,23 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package util
// Ptr returns a pointer to the passed in type
func Ptr[T any](t T) *T {
return &t
}

View file

@ -99,14 +99,19 @@ func Email(email string) error {
return err
}
// Language checks that the given language string is a 2- or 3-letter ISO 639 code.
// Returns an error if the language cannot be parsed. See: https://pkg.go.dev/golang.org/x/text/language
func Language(lang string) error {
// Language checks that the given language string is a valid, if not necessarily canonical, BCP 47 language tag.
// Returns a canonicalized version of the tag if the language can be parsed.
// Returns an error if the language cannot be parsed.
// See: https://pkg.go.dev/golang.org/x/text/language
func Language(lang string) (string, error) {
if lang == "" {
return errors.New("no language provided")
return "", errors.New("no language provided")
}
_, err := language.ParseBase(lang)
return err
parsed, err := language.Parse(lang)
if err != nil {
return "", err
}
return parsed.String(), err
}
// SignUpReason checks that a sufficient reason is given for a server signup request

View file

@ -159,60 +159,39 @@ func (suite *ValidationTestSuite) TestValidateEmail() {
}
func (suite *ValidationTestSuite) TestValidateLanguage() {
empty := ""
notALanguage := "this isn't a language at all!"
english := "en"
capitalEnglish := "EN"
arabic3Letters := "ara"
mixedCapsEnglish := "eN"
englishUS := "en-us"
dutch := "nl"
german := "de"
var err error
err = validate.Language(empty)
if suite.Error(err) {
suite.Equal(errors.New("no language provided"), err)
testCases := []struct {
name, input, expected, err string
}{
{name: "empty", err: "no language provided"},
{name: "notALanguage", input: "this isn't a language at all!", err: "language: tag is not well-formed"},
{name: "english", input: "en", expected: "en"},
// Should be all lowercase
{name: "capitalEnglish", input: "EN", expected: "en"},
// Overlong, should be in ISO 639-1 format
{name: "arabic3Letters", input: "ara", expected: "ar"},
// Should be all lowercase
{name: "mixedCapsEnglish", input: "eN", expected: "en"},
// Region should be capitalized
{name: "englishUS", input: "en-us", expected: "en-US"},
{name: "dutch", input: "nl", expected: "nl"},
{name: "german", input: "de", expected: "de"},
{name: "chinese", input: "zh", expected: "zh"},
{name: "chineseSimplified", input: "zh-Hans", expected: "zh-Hans"},
{name: "chineseTraditional", input: "zh-Hant", expected: "zh-Hant"},
}
err = validate.Language(notALanguage)
if suite.Error(err) {
suite.Equal(errors.New("language: tag is not well-formed"), err)
}
err = validate.Language(english)
if suite.NoError(err) {
suite.Equal(nil, err)
}
err = validate.Language(capitalEnglish)
if suite.NoError(err) {
suite.Equal(nil, err)
}
err = validate.Language(arabic3Letters)
if suite.NoError(err) {
suite.Equal(nil, err)
}
err = validate.Language(mixedCapsEnglish)
if suite.NoError(err) {
suite.Equal(nil, err)
}
err = validate.Language(englishUS)
if suite.Error(err) {
suite.Equal(errors.New("language: tag is not well-formed"), err)
}
err = validate.Language(dutch)
if suite.NoError(err) {
suite.Equal(nil, err)
}
err = validate.Language(german)
if suite.NoError(err) {
suite.Equal(nil, err)
for _, testCase := range testCases {
testCase := testCase
suite.Run(testCase.name, func() {
actual, actualErr := validate.Language(testCase.input)
if testCase.err == "" {
suite.Equal(testCase.expected, actual)
suite.NoError(actualErr)
} else {
suite.Empty(actual)
suite.EqualError(actualErr, testCase.err)
}
})
}
}

View file

@ -25,6 +25,7 @@ import (
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@ -115,7 +116,7 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestThread() {
Content: "nbnbdy expects dog",
CreatedAt: testrig.TimeMustParse("2021-09-20T12:41:37+02:00"),
UpdatedAt: testrig.TimeMustParse("2021-09-20T12:41:37+02:00"),
Local: testrig.FalseBool(),
Local: util.Ptr(false),
AccountURI: "http://localhost:8080/users/the_mighty_zork",
AccountID: threadParentAccount.ID,
InReplyToID: originalStatus.ID,
@ -124,13 +125,13 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestThread() {
BoostOfID: "",
ContentWarning: "",
Visibility: gtsmodel.VisibilityFollowersOnly,
Sensitive: testrig.FalseBool(),
Sensitive: util.Ptr(false),
Language: "en",
CreatedWithApplicationID: "",
Federated: testrig.TrueBool(),
Boostable: testrig.TrueBool(),
Replyable: testrig.TrueBool(),
Likeable: testrig.TrueBool(),
Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote,
}
if err := suite.db.PutStatus(ctx, firstReplyStatus); err != nil {
@ -168,7 +169,7 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyFollowersOnly(
Content: "didn't expect dog",
CreatedAt: testrig.TimeMustParse("2021-09-20T12:40:37+02:00"),
UpdatedAt: testrig.TimeMustParse("2021-09-20T12:40:37+02:00"),
Local: testrig.FalseBool(),
Local: util.Ptr(false),
AccountURI: "http://fossbros-anonymous.io/users/foss_satan",
AccountID: originalStatusParent.ID,
InReplyToID: "",
@ -177,13 +178,13 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyFollowersOnly(
BoostOfID: "",
ContentWarning: "",
Visibility: gtsmodel.VisibilityFollowersOnly,
Sensitive: testrig.FalseBool(),
Sensitive: util.Ptr(false),
Language: "en",
CreatedWithApplicationID: "",
Federated: testrig.TrueBool(),
Boostable: testrig.TrueBool(),
Replyable: testrig.TrueBool(),
Likeable: testrig.TrueBool(),
Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote,
}
if err := suite.db.PutStatus(ctx, originalStatus); err != nil {
@ -202,7 +203,7 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyFollowersOnly(
Content: "nbnbdy expects dog",
CreatedAt: testrig.TimeMustParse("2021-09-20T12:41:37+02:00"),
UpdatedAt: testrig.TimeMustParse("2021-09-20T12:41:37+02:00"),
Local: testrig.FalseBool(),
Local: util.Ptr(false),
AccountURI: "http://localhost:8080/users/the_mighty_zork",
AccountID: replyingAccount.ID,
InReplyToID: originalStatus.ID,
@ -211,13 +212,13 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyFollowersOnly(
BoostOfID: "",
ContentWarning: "",
Visibility: gtsmodel.VisibilityFollowersOnly,
Sensitive: testrig.FalseBool(),
Sensitive: util.Ptr(false),
Language: "en",
CreatedWithApplicationID: "",
Federated: testrig.TrueBool(),
Boostable: testrig.TrueBool(),
Replyable: testrig.TrueBool(),
Likeable: testrig.TrueBool(),
Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote,
}
if err := suite.db.PutStatus(ctx, firstReplyStatus); err != nil {
@ -236,7 +237,7 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyFollowersOnly(
Content: "*nobody",
CreatedAt: testrig.TimeMustParse("2021-09-20T12:42:37+02:00"),
UpdatedAt: testrig.TimeMustParse("2021-09-20T12:42:37+02:00"),
Local: testrig.FalseBool(),
Local: util.Ptr(false),
AccountURI: "http://localhost:8080/users/the_mighty_zork",
AccountID: replyingAccount.ID,
InReplyToID: firstReplyStatus.ID,
@ -245,13 +246,13 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyFollowersOnly(
BoostOfID: "",
ContentWarning: "",
Visibility: gtsmodel.VisibilityFollowersOnly,
Sensitive: testrig.FalseBool(),
Sensitive: util.Ptr(false),
Language: "en",
CreatedWithApplicationID: "",
Federated: testrig.TrueBool(),
Boostable: testrig.TrueBool(),
Replyable: testrig.TrueBool(),
Likeable: testrig.TrueBool(),
Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote,
}
if err := suite.db.PutStatus(ctx, secondReplyStatus); err != nil {
@ -281,7 +282,7 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyPublicAndUnloc
Content: "didn't expect dog",
CreatedAt: testrig.TimeMustParse("2021-09-20T12:40:37+02:00"),
UpdatedAt: testrig.TimeMustParse("2021-09-20T12:40:37+02:00"),
Local: testrig.FalseBool(),
Local: util.Ptr(false),
AccountURI: "http://fossbros-anonymous.io/users/foss_satan",
AccountID: originalStatusParent.ID,
InReplyToID: "",
@ -290,13 +291,13 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyPublicAndUnloc
BoostOfID: "",
ContentWarning: "",
Visibility: gtsmodel.VisibilityUnlocked,
Sensitive: testrig.FalseBool(),
Sensitive: util.Ptr(false),
Language: "en",
CreatedWithApplicationID: "",
Federated: testrig.TrueBool(),
Boostable: testrig.TrueBool(),
Replyable: testrig.TrueBool(),
Likeable: testrig.TrueBool(),
Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote,
}
if err := suite.db.PutStatus(ctx, originalStatus); err != nil {
@ -315,7 +316,7 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyPublicAndUnloc
Content: "nbnbdy expects dog",
CreatedAt: testrig.TimeMustParse("2021-09-20T12:41:37+02:00"),
UpdatedAt: testrig.TimeMustParse("2021-09-20T12:41:37+02:00"),
Local: testrig.FalseBool(),
Local: util.Ptr(false),
AccountURI: "http://localhost:8080/users/the_mighty_zork",
AccountID: replyingAccount.ID,
InReplyToID: originalStatus.ID,
@ -324,13 +325,13 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyPublicAndUnloc
BoostOfID: "",
ContentWarning: "",
Visibility: gtsmodel.VisibilityPublic,
Sensitive: testrig.FalseBool(),
Sensitive: util.Ptr(false),
Language: "en",
CreatedWithApplicationID: "",
Federated: testrig.TrueBool(),
Boostable: testrig.TrueBool(),
Replyable: testrig.TrueBool(),
Likeable: testrig.TrueBool(),
Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote,
}
if err := suite.db.PutStatus(ctx, firstReplyStatus); err != nil {
@ -349,7 +350,7 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyPublicAndUnloc
Content: "*nobody",
CreatedAt: testrig.TimeMustParse("2021-09-20T12:42:37+02:00"),
UpdatedAt: testrig.TimeMustParse("2021-09-20T12:42:37+02:00"),
Local: testrig.FalseBool(),
Local: util.Ptr(false),
AccountURI: "http://localhost:8080/users/the_mighty_zork",
AccountID: replyingAccount.ID,
InReplyToID: firstReplyStatus.ID,
@ -358,13 +359,13 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyPublicAndUnloc
BoostOfID: "",
ContentWarning: "",
Visibility: gtsmodel.VisibilityUnlocked,
Sensitive: testrig.FalseBool(),
Sensitive: util.Ptr(false),
Language: "en",
CreatedWithApplicationID: "",
Federated: testrig.TrueBool(),
Boostable: testrig.TrueBool(),
Replyable: testrig.TrueBool(),
Likeable: testrig.TrueBool(),
Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote,
}
if err := suite.db.PutStatus(ctx, secondReplyStatus); err != nil {

View file

@ -27,29 +27,43 @@ const (
robotsPath = "/robots.txt"
robotsMetaAllowSome = "nofollow, noarchive, nositelinkssearchbox, max-image-preview:standard" // https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag#robotsmeta
robotsTxt = `# GoToSocial robots.txt -- to edit, see internal/web/robots.go
# more info @ https://developers.google.com/search/docs/crawling-indexing/robots/intro
# More info @ https://developers.google.com/search/docs/crawling-indexing/robots/intro
# Before we commence, a giant fuck you to ChatGPT in particular.
# https://platform.openai.com/docs/gptbot
User-agent: GPTBot
Disallow: /
# Rules for everything else.
User-agent: *
Crawl-delay: 500
# api stuff
# API endpoints.
Disallow: /api/
# auth/login stuff
# Auth/login endpoints.
Disallow: /auth/
Disallow: /oauth/
Disallow: /check_your_email
Disallow: /wait_for_approval
Disallow: /account_disabled
# well known stuff
# Well-known endpoints.
Disallow: /.well-known/
# files
# Fileserver/media.
Disallow: /fileserver/
# s2s AP stuff
# Fedi S2S API endpoints.
Disallow: /users/
Disallow: /emoji/
# panels
# Settings panels.
Disallow: /admin
Disallow: /user
Disallow: /settings/
# domain blocklist
# Domain blocklist.
Disallow: /about/suspended`
)

View file

@ -2,6 +2,11 @@
set -e
# Ensure test args are set.
ARGS=${@}; [ -z "$ARGS" ] && \
ARGS='./...'
# Database config.
DB_NAME='postgres'
DB_USER='postgres'
DB_PASS='postgres'
@ -34,4 +39,4 @@ GTS_DB_PORT=${DB_PORT} \
GTS_DB_USER=${DB_USER} \
GTS_DB_PASSWORD=${DB_PASS} \
GTS_DB_DATABASE=${DB_NAME} \
go test ./... -p 1 ${@}
go test ./... -p 1 ${ARGS}

View file

@ -2,6 +2,11 @@
set -e
# Ensure test args are set.
ARGS=${@}; [ -z "$ARGS" ] && \
ARGS='./...'
# Run the SQLite tests.
GTS_DB_TYPE=sqlite \
GTS_DB_ADDRESS=':memory:' \
go test ./... ${@}
go test ${ARGS}

File diff suppressed because it is too large Load diff

View file

@ -79,7 +79,7 @@ Minifiers or bindings to minifiers exist in almost all programming languages. So
This minifier proves to be that fast and extensive minifier that can handle HTML and any other filetype it may contain (CSS, JS, ...). It is usually orders of magnitude faster than existing minifiers.
## Installation
Make sure you have [Git](https://git-scm.com/) and [Go](https://golang.org/dl/) (1.13 or higher) installed, run
Make sure you have [Git](https://git-scm.com/) and [Go](https://golang.org/dl/) (1.18 or higher) installed, run
```
mkdir Project
cd Project

View file

@ -11,238 +11,254 @@ type Hash uint32
// Unique hash definitions to be used instead of strings
const (
A Hash = 0x1 // a
Abbr Hash = 0x37a04 // abbr
Abbr Hash = 0x3b804 // abbr
About Hash = 0x5 // about
Accept Hash = 0x1106 // accept
Accept_Charset Hash = 0x110e // accept-charset
Action Hash = 0x23f06 // action
Address Hash = 0x5a07 // address
Align Hash = 0x32705 // align
Alink Hash = 0x7005 // alink
Allowfullscreen Hash = 0x2ad0f // allowfullscreen
Amp_Boilerplate Hash = 0x610f // amp-boilerplate
Area Hash = 0x1e304 // area
Acronym Hash = 0x4a07 // acronym
Action Hash = 0x21d06 // action
Address Hash = 0x7807 // address
Align Hash = 0x35b05 // align
Alink Hash = 0x3a405 // alink
Allowfullscreen Hash = 0x2e10f // allowfullscreen
Amp_Boilerplate Hash = 0x7f0f // amp-boilerplate
Applet Hash = 0xd706 // applet
Area Hash = 0x2fd04 // area
Article Hash = 0x2707 // article
Aside Hash = 0xb405 // aside
Async Hash = 0xac05 // async
Audio Hash = 0xd105 // audio
Autofocus Hash = 0xe409 // autofocus
Autoplay Hash = 0x10808 // autoplay
Axis Hash = 0x11004 // axis
Aside Hash = 0x5b05 // aside
Async Hash = 0x8e05 // async
Audio Hash = 0x9605 // audio
Autofocus Hash = 0xcc09 // autofocus
Autoplay Hash = 0x10c08 // autoplay
Axis Hash = 0x11404 // axis
B Hash = 0x101 // b
Background Hash = 0x300a // background
Base Hash = 0x19604 // base
Bb Hash = 0x37b02 // bb
Bdi Hash = 0x7503 // bdi
Bdo Hash = 0x31f03 // bdo
Bgcolor Hash = 0x12607 // bgcolor
Blockquote Hash = 0x13e0a // blockquote
Base Hash = 0x17804 // base
Basefont Hash = 0x17808 // basefont
Bb Hash = 0x3b902 // bb
Bdi Hash = 0x18403 // bdi
Bdo Hash = 0x35303 // bdo
Bgcolor Hash = 0x12a07 // bgcolor
Big Hash = 0x13103 // big
Blockquote Hash = 0x1340a // blockquote
Body Hash = 0xd04 // body
Br Hash = 0x37c02 // br
Button Hash = 0x14806 // button
Canvas Hash = 0xb006 // canvas
Caption Hash = 0x21f07 // caption
Br Hash = 0x36102 // br
Button Hash = 0x13e06 // button
Canvas Hash = 0x5706 // canvas
Caption Hash = 0x1fe07 // caption
Center Hash = 0xb706 // center
Charset Hash = 0x1807 // charset
Checked Hash = 0x1b307 // checked
Cite Hash = 0xfb04 // cite
Class Hash = 0x15905 // class
Classid Hash = 0x15907 // classid
Checked Hash = 0x19707 // checked
Cite Hash = 0x9204 // cite
Class Hash = 0x15105 // class
Classid Hash = 0x15107 // classid
Clear Hash = 0x2b05 // clear
Code Hash = 0x19204 // code
Codebase Hash = 0x19208 // codebase
Codetype Hash = 0x1a408 // codetype
Col Hash = 0x12803 // col
Colgroup Hash = 0x1bb08 // colgroup
Color Hash = 0x12805 // color
Cols Hash = 0x1cf04 // cols
Colspan Hash = 0x1cf07 // colspan
Compact Hash = 0x1ec07 // compact
Content Hash = 0x28407 // content
Controls Hash = 0x20108 // controls
Code Hash = 0x17404 // code
Codebase Hash = 0x17408 // codebase
Codetype Hash = 0x18808 // codetype
Col Hash = 0x12c03 // col
Colgroup Hash = 0x1af08 // colgroup
Color Hash = 0x12c05 // color
Cols Hash = 0x1c904 // cols
Colspan Hash = 0x1c907 // colspan
Compact Hash = 0x1d707 // compact
Content Hash = 0x27b07 // content
Controls Hash = 0x1e708 // controls
Data Hash = 0x1f04 // data
Datalist Hash = 0x1f08 // datalist
Datatype Hash = 0x4d08 // datatype
Dd Hash = 0x5b02 // dd
Declare Hash = 0xb707 // declare
Default Hash = 0x7f07 // default
DefaultChecked Hash = 0x1730e // defaultChecked
DefaultMuted Hash = 0x7f0c // defaultMuted
DefaultSelected Hash = 0x8a0f // defaultSelected
Defer Hash = 0x9805 // defer
Del Hash = 0x10503 // del
Details Hash = 0x15f07 // details
Dfn Hash = 0x16c03 // dfn
Dialog Hash = 0xa606 // dialog
Dir Hash = 0x7603 // dir
Disabled Hash = 0x18008 // disabled
Div Hash = 0x18703 // div
Dl Hash = 0x1b902 // dl
Dt Hash = 0x23102 // dt
Datatype Hash = 0xac08 // datatype
Dd Hash = 0x7902 // dd
Declare Hash = 0x5e07 // declare
Default Hash = 0xeb07 // default
DefaultChecked Hash = 0x2270e // defaultChecked
DefaultMuted Hash = 0xeb0c // defaultMuted
DefaultSelected Hash = 0xf60f // defaultSelected
Defer Hash = 0x10405 // defer
Del Hash = 0x37903 // del
Details Hash = 0x15707 // details
Dfn Hash = 0x16403 // dfn
Dialog Hash = 0xc606 // dialog
Dir Hash = 0x18503 // dir
Disabled Hash = 0x19d08 // disabled
Div Hash = 0x1a403 // div
Dl Hash = 0x1e502 // dl
Dt Hash = 0x21702 // dt
Em Hash = 0x4302 // em
Embed Hash = 0x4905 // embed
Enabled Hash = 0x26c07 // enabled
Enctype Hash = 0x1fa07 // enctype
Face Hash = 0x5604 // face
Fieldset Hash = 0x21408 // fieldset
Figcaption Hash = 0x21c0a // figcaption
Figure Hash = 0x22606 // figure
Footer Hash = 0xdb06 // footer
For Hash = 0x23b03 // for
Form Hash = 0x23b04 // form
Formaction Hash = 0x23b0a // formaction
Formnovalidate Hash = 0x2450e // formnovalidate
Frame Hash = 0x28c05 // frame
Frameborder Hash = 0x28c0b // frameborder
H1 Hash = 0x2e002 // h1
H2 Hash = 0x25302 // h2
H3 Hash = 0x25502 // h3
H4 Hash = 0x25702 // h4
H5 Hash = 0x25902 // h5
H6 Hash = 0x25b02 // h6
Head Hash = 0x2d204 // head
Header Hash = 0x2d206 // header
Hgroup Hash = 0x25d06 // hgroup
Hidden Hash = 0x26806 // hidden
Hr Hash = 0x32d02 // hr
Href Hash = 0x32d04 // href
Hreflang Hash = 0x32d08 // hreflang
Html Hash = 0x27304 // html
Http_Equiv Hash = 0x2770a // http-equiv
Embed Hash = 0x37505 // embed
Enabled Hash = 0x26307 // enabled
Enctype Hash = 0x2a207 // enctype
Face Hash = 0xb504 // face
Fieldset Hash = 0x1f308 // fieldset
Figcaption Hash = 0x1fb0a // figcaption
Figure Hash = 0x20c06 // figure
Font Hash = 0x17c04 // font
Footer Hash = 0xa006 // footer
For Hash = 0x21903 // for
Form Hash = 0x21904 // form
Formaction Hash = 0x2190a // formaction
Formnovalidate Hash = 0x2350e // formnovalidate
Frame Hash = 0x14505 // frame
Frameborder Hash = 0x2830b // frameborder
Frameset Hash = 0x14508 // frameset
H1 Hash = 0x2d002 // h1
H2 Hash = 0x24302 // h2
H3 Hash = 0x24502 // h3
H4 Hash = 0x24702 // h4
H5 Hash = 0x24902 // h5
H6 Hash = 0x24b02 // h6
Head Hash = 0x2c204 // head
Header Hash = 0x2c206 // header
Hgroup Hash = 0x24d06 // hgroup
Hidden Hash = 0x25f06 // hidden
Hr Hash = 0x16802 // hr
Href Hash = 0x16804 // href
Hreflang Hash = 0x16808 // hreflang
Html Hash = 0x26a04 // html
Http_Equiv Hash = 0x26e0a // http-equiv
I Hash = 0x2401 // i
Icon Hash = 0x28304 // icon
Id Hash = 0xb602 // id
Iframe Hash = 0x28b06 // iframe
Img Hash = 0x29703 // img
Inert Hash = 0xf605 // inert
Inlist Hash = 0x29a06 // inlist
Input Hash = 0x2a405 // input
Ins Hash = 0x2a903 // ins
Ismap Hash = 0x11205 // ismap
Itemscope Hash = 0xfc09 // itemscope
Kbd Hash = 0x7403 // kbd
Keygen Hash = 0x1f606 // keygen
Label Hash = 0xbe05 // label
Lang Hash = 0x33104 // lang
Language Hash = 0x33108 // language
Legend Hash = 0x2c506 // legend
Icon Hash = 0x27a04 // icon
Id Hash = 0x5d02 // id
Iframe Hash = 0x28206 // iframe
Image Hash = 0x28e05 // image
Img Hash = 0x29303 // img
Inert Hash = 0x5205 // inert
Inlist Hash = 0x29606 // inlist
Input Hash = 0x2a905 // input
Ins Hash = 0x2ae03 // ins
Ismap Hash = 0x11605 // ismap
Itemscope Hash = 0xe209 // itemscope
Kbd Hash = 0x18303 // kbd
Keygen Hash = 0x29e06 // keygen
Label Hash = 0x6505 // label
Lang Hash = 0x16c04 // lang
Language Hash = 0x16c08 // language
Legend Hash = 0x31706 // legend
Li Hash = 0x2302 // li
Link Hash = 0x7104 // link
Longdesc Hash = 0xc208 // longdesc
Main Hash = 0xf404 // main
Manifest Hash = 0x2bc08 // manifest
Map Hash = 0xee03 // map
Mark Hash = 0x2cb04 // mark
Math Hash = 0x2cf04 // math
Max Hash = 0x2d803 // max
Maxlength Hash = 0x2d809 // maxlength
Media Hash = 0xa405 // media
Menu Hash = 0x12204 // menu
Meta Hash = 0x2e204 // meta
Meter Hash = 0x2f705 // meter
Method Hash = 0x2fc06 // method
Multiple Hash = 0x30208 // multiple
Muted Hash = 0x30a05 // muted
Name Hash = 0xa204 // name
Nav Hash = 0x32403 // nav
Nohref Hash = 0x32b06 // nohref
Noresize Hash = 0x13608 // noresize
Noscript Hash = 0x14d08 // noscript
Noshade Hash = 0x16e07 // noshade
Novalidate Hash = 0x2490a // novalidate
Nowrap Hash = 0x1d506 // nowrap
Object Hash = 0xd506 // object
Ol Hash = 0xcb02 // ol
Open Hash = 0x32104 // open
Optgroup Hash = 0x35608 // optgroup
Option Hash = 0x30f06 // option
Link Hash = 0x3a504 // link
Longdesc Hash = 0x6908 // longdesc
Main Hash = 0x5004 // main
Manifest Hash = 0x11e08 // manifest
Map Hash = 0xd603 // map
Mark Hash = 0x2b404 // mark
Marquee Hash = 0x2b807 // marquee
Math Hash = 0x2bf04 // math
Max Hash = 0x2c803 // max
Maxlength Hash = 0x2c809 // maxlength
Media Hash = 0xc405 // media
Menu Hash = 0xde04 // menu
Menuitem Hash = 0xde08 // menuitem
Meta Hash = 0x2d204 // meta
Meter Hash = 0x30605 // meter
Method Hash = 0x30b06 // method
Multiple Hash = 0x31108 // multiple
Muted Hash = 0x31d05 // muted
Name Hash = 0xc204 // name
Nav Hash = 0x35803 // nav
Nobr Hash = 0x35f04 // nobr
Noembed Hash = 0x37307 // noembed
Noframes Hash = 0x14308 // noframes
Nohref Hash = 0x16606 // nohref
Noresize Hash = 0x1cf08 // noresize
Noscript Hash = 0x20408 // noscript
Noshade Hash = 0x22207 // noshade
Novalidate Hash = 0x2390a // novalidate
Nowrap Hash = 0x2ef06 // nowrap
Object Hash = 0x9a06 // object
Ol Hash = 0x7202 // ol
Open Hash = 0x35504 // open
Optgroup Hash = 0x39908 // optgroup
Option Hash = 0x32206 // option
Output Hash = 0x206 // output
P Hash = 0x501 // p
Param Hash = 0xf005 // param
Pauseonexit Hash = 0x1160b // pauseonexit
Picture Hash = 0x1c207 // picture
Plaintext Hash = 0x1da09 // plaintext
Poster Hash = 0x26206 // poster
Pre Hash = 0x35d03 // pre
Prefix Hash = 0x35d06 // prefix
Profile Hash = 0x36407 // profile
Progress Hash = 0x34208 // progress
Property Hash = 0x31508 // property
Q Hash = 0x14301 // q
Param Hash = 0x11a05 // param
Pauseonexit Hash = 0x1b60b // pauseonexit
Picture Hash = 0x25207 // picture
Plaintext Hash = 0x2f409 // plaintext
Portal Hash = 0x3a006 // portal
Poster Hash = 0x38c06 // poster
Pre Hash = 0x38503 // pre
Prefix Hash = 0x38506 // prefix
Profile Hash = 0x32807 // profile
Progress Hash = 0x32f08 // progress
Property Hash = 0x33e08 // property
Q Hash = 0x13901 // q
Rb Hash = 0x2f02 // rb
Readonly Hash = 0x1e408 // readonly
Rel Hash = 0xbc03 // rel
Required Hash = 0x22a08 // required
Resource Hash = 0x1c708 // resource
Rev Hash = 0x7803 // rev
Reversed Hash = 0x7808 // reversed
Rows Hash = 0x9c04 // rows
Rowspan Hash = 0x9c07 // rowspan
Rp Hash = 0x6a02 // rp
Readonly Hash = 0x2fe08 // readonly
Rel Hash = 0x6303 // rel
Required Hash = 0x21008 // required
Resource Hash = 0x25708 // resource
Rev Hash = 0xa503 // rev
Reversed Hash = 0xa508 // reversed
Rows Hash = 0xbc04 // rows
Rowspan Hash = 0xbc07 // rowspan
Rp Hash = 0x8802 // rp
Rt Hash = 0x2802 // rt
Rtc Hash = 0xf903 // rtc
Ruby Hash = 0xe004 // ruby
Rules Hash = 0x12c05 // rules
Rtc Hash = 0x5503 // rtc
Ruby Hash = 0x10804 // ruby
Rules Hash = 0x36205 // rules
S Hash = 0x1c01 // s
Samp Hash = 0x6004 // samp
Scope Hash = 0x10005 // scope
Scoped Hash = 0x10006 // scoped
Script Hash = 0x14f06 // script
Scrolling Hash = 0xc809 // scrolling
Seamless Hash = 0x19808 // seamless
Section Hash = 0x13007 // section
Select Hash = 0x16506 // select
Selected Hash = 0x16508 // selected
Shape Hash = 0x19f05 // shape
Size Hash = 0x13a04 // size
Slot Hash = 0x20804 // slot
Small Hash = 0x2ab05 // small
Sortable Hash = 0x2ef08 // sortable
Source Hash = 0x1c906 // source
Span Hash = 0x9f04 // span
Src Hash = 0x34903 // src
Srcset Hash = 0x34906 // srcset
Samp Hash = 0x7e04 // samp
Scope Hash = 0xe605 // scope
Scoped Hash = 0xe606 // scoped
Script Hash = 0x20606 // script
Scrolling Hash = 0x6f09 // scrolling
Seamless Hash = 0x36608 // seamless
Section Hash = 0x36d07 // section
Select Hash = 0x15d06 // select
Selected Hash = 0x15d08 // selected
Shape Hash = 0x1ee05 // shape
Size Hash = 0x1d304 // size
Slot Hash = 0x2b004 // slot
Small Hash = 0x2df05 // small
Sortable Hash = 0x33608 // sortable
Source Hash = 0x25906 // source
Span Hash = 0xbf04 // span
Src Hash = 0x34603 // src
Srcset Hash = 0x34606 // srcset
Start Hash = 0x2505 // start
Strong Hash = 0x29e06 // strong
Style Hash = 0x2c205 // style
Sub Hash = 0x31d03 // sub
Summary Hash = 0x33907 // summary
Sup Hash = 0x34003 // sup
Svg Hash = 0x34f03 // svg
Tabindex Hash = 0x2e408 // tabindex
Table Hash = 0x2f205 // table
Strike Hash = 0x29a06 // strike
Strong Hash = 0x12406 // strong
Style Hash = 0x34c05 // style
Sub Hash = 0x35103 // sub
Summary Hash = 0x37c07 // summary
Sup Hash = 0x38303 // sup
Svg Hash = 0x39203 // svg
Tabindex Hash = 0x2d408 // tabindex
Table Hash = 0x33905 // table
Target Hash = 0x706 // target
Tbody Hash = 0xc05 // tbody
Td Hash = 0x1e02 // td
Template Hash = 0x4208 // template
Text Hash = 0x1df04 // text
Textarea Hash = 0x1df08 // textarea
Tfoot Hash = 0xda05 // tfoot
Th Hash = 0x2d102 // th
Thead Hash = 0x2d105 // thead
Time Hash = 0x12004 // time
Title Hash = 0x15405 // title
Tr Hash = 0x1f202 // tr
Track Hash = 0x1f205 // track
Translate Hash = 0x20b09 // translate
Truespeed Hash = 0x23209 // truespeed
Type Hash = 0x5104 // type
Typemustmatch Hash = 0x1a80d // typemustmatch
Typeof Hash = 0x5106 // typeof
Text Hash = 0x2f904 // text
Textarea Hash = 0x2f908 // textarea
Tfoot Hash = 0x9f05 // tfoot
Th Hash = 0x2c102 // th
Thead Hash = 0x2c105 // thead
Time Hash = 0xdc04 // time
Title Hash = 0x14c05 // title
Tr Hash = 0x12502 // tr
Track Hash = 0x17f05 // track
Translate Hash = 0x1c009 // translate
Truespeed Hash = 0x1dd09 // truespeed
Tt Hash = 0x14002 // tt
Type Hash = 0xb004 // type
Typemustmatch Hash = 0x18c0d // typemustmatch
Typeof Hash = 0xb006 // typeof
U Hash = 0x301 // u
Ul Hash = 0x8302 // ul
Ul Hash = 0xef02 // ul
Undeterminate Hash = 0x370d // undeterminate
Usemap Hash = 0xeb06 // usemap
Valign Hash = 0x32606 // valign
Value Hash = 0x18905 // value
Valuetype Hash = 0x18909 // valuetype
Var Hash = 0x28003 // var
Video Hash = 0x35205 // video
Visible Hash = 0x36b07 // visible
Vlink Hash = 0x37205 // vlink
Vocab Hash = 0x37705 // vocab
Wbr Hash = 0x37e03 // wbr
Xmlns Hash = 0x2eb05 // xmlns
Xmp Hash = 0x36203 // xmp
Usemap Hash = 0xd306 // usemap
Valign Hash = 0x35a06 // valign
Value Hash = 0x1a605 // value
Valuetype Hash = 0x1a609 // valuetype
Var Hash = 0x27703 // var
Video Hash = 0x39505 // video
Visible Hash = 0x3a907 // visible
Vlink Hash = 0x3b005 // vlink
Vocab Hash = 0x3b505 // vocab
Wbr Hash = 0x3bc03 // wbr
Xmlns Hash = 0x2db05 // xmlns
Xmp Hash = 0x38a03 // xmp
)
// String returns the hash' name.
@ -288,256 +304,273 @@ NEXT:
return 0
}
const _Hash_hash0 = 0x9acb0442
const _Hash_hash0 = 0x67ac9bb5
const _Hash_maxLen = 15
const _Hash_text = "aboutputargetbodyaccept-charsetdatalistarticlearbackgroundet" +
"erminatemplatembedatatypeofaceaddressamp-boilerplatealinkbdi" +
"reversedefaultMutedefaultSelectedeferowspanamedialogasyncanv" +
"asideclarelabelongdescrollingaudiobjectfooterubyautofocusema" +
"paramainertcitemscopedelautoplayaxismapauseonexitimenubgcolo" +
"rulesectionoresizeblockquotebuttonoscriptitleclassidetailsel" +
"ectedfnoshadefaultCheckedisabledivaluetypecodebaseamlesshape" +
"codetypemustmatcheckedlcolgroupicturesourcecolspanowraplaint" +
"extareadonlycompactrackeygenctypecontrolslotranslatefieldset" +
"figcaptionfigurequiredtruespeedformactionformnovalidateh2h3h" +
"4h5h6hgrouposterhiddenabledhtmlhttp-equivaricontentiframebor" +
"derimginlistronginputinsmallowfullscreenmanifestylegendmarkm" +
"atheadermaxlength1metabindexmlnsortablemetermethodmultiplemu" +
"tedoptionpropertysubdopenavalignohreflanguagesummarysuprogre" +
"ssrcsetsvgvideoptgrouprefixmprofilevisiblevlinkvocabbrwbr"
"erminatemplateacronymainertcanvasideclarelabelongdescrolling" +
"addressamp-boilerplateasynciteaudiobjectfootereversedatatype" +
"ofacenterowspanamedialogautofocusemappletimenuitemscopedefau" +
"ltMutedefaultSelectedeferubyautoplayaxismaparamanifestrongbg" +
"colorbigblockquotebuttonoframesetitleclassidetailselectedfno" +
"hreflanguagecodebasefontrackbdircodetypemustmatcheckedisable" +
"divaluetypecolgroupauseonexitranslatecolspanoresizecompactru" +
"espeedlcontrolshapefieldsetfigcaptionoscriptfigurequiredtfor" +
"mactionoshadefaultCheckedformnovalidateh2h3h4h5h6hgroupictur" +
"esourcehiddenabledhtmlhttp-equivaricontentiframeborderimagei" +
"mginlistrikeygenctypeinputinslotmarkmarqueematheadermaxlengt" +
"h1metabindexmlnsmallowfullscreenowraplaintextareadonlymeterm" +
"ethodmultiplegendmutedoptionprofileprogressortablepropertysr" +
"csetstylesubdopenavalignobruleseamlessectionoembedelsummarys" +
"uprefixmpostersvgvideoptgrouportalinkvisiblevlinkvocabbrwbr"
var _Hash_table = [1 << 9]Hash{
0x0: 0x1df08, // textarea
0x4: 0x32d02, // hr
0x8: 0x1c207, // picture
0xb: 0x18905, // value
0xf: 0x2e408, // tabindex
0x12: 0x15905, // class
0x15: 0x37e03, // wbr
0x18: 0x1a80d, // typemustmatch
0x1a: 0x1b902, // dl
0x1d: 0xf903, // rtc
0x1e: 0x25702, // h4
0x22: 0x2ef08, // sortable
0x24: 0x4208, // template
0x25: 0x28c0b, // frameborder
0x28: 0x37a04, // abbr
0x29: 0x28b06, // iframe
0x2a: 0x610f, // amp-boilerplate
0x2c: 0x1e408, // readonly
0x30: 0x23f06, // action
0x33: 0x28c05, // frame
0x35: 0x12c05, // rules
0x36: 0x30208, // multiple
0x38: 0x31f03, // bdo
0x39: 0x1d506, // nowrap
0x3e: 0x21408, // fieldset
0x3f: 0x7503, // bdi
0x46: 0x7f0c, // defaultMuted
0x49: 0x35205, // video
0x4c: 0x19808, // seamless
0x4d: 0x13608, // noresize
0x4f: 0xb602, // id
0x51: 0x25d06, // hgroup
0x52: 0x23102, // dt
0x55: 0x12805, // color
0x56: 0x34003, // sup
0x59: 0x370d, // undeterminate
0x5a: 0x35608, // optgroup
0x5b: 0x2d206, // header
0x5c: 0xb405, // aside
0x5f: 0x10005, // scope
0x60: 0x101, // b
0x61: 0xcb02, // ol
0x64: 0x32b06, // nohref
0x65: 0x1da09, // plaintext
0x66: 0x20804, // slot
0x67: 0x11004, // axis
0x68: 0x12803, // col
0x69: 0x32606, // valign
0x6c: 0x2d105, // thead
0x70: 0x34906, // srcset
0x71: 0x26806, // hidden
0x76: 0x1bb08, // colgroup
0x78: 0x34f03, // svg
0x7b: 0x2cb04, // mark
0x7e: 0x33104, // lang
0x81: 0x1cf04, // cols
0x86: 0x5a07, // address
0x8b: 0xf404, // main
0x8c: 0x4302, // em
0x8f: 0x32d08, // hreflang
0x93: 0x1b307, // checked
0x94: 0x25902, // h5
0x95: 0x301, // u
0x96: 0x32705, // align
0x97: 0x14301, // q
0x99: 0xd506, // object
0x9b: 0x28407, // content
0x9d: 0xc809, // scrolling
0x9f: 0x36407, // profile
0xa0: 0x34903, // src
0xa1: 0xda05, // tfoot
0xa3: 0x2f705, // meter
0xa4: 0x37705, // vocab
0xa6: 0xd04, // body
0xa8: 0x19204, // code
0xac: 0x20108, // controls
0xb0: 0x2ab05, // small
0xb1: 0x18008, // disabled
0xb5: 0x5604, // face
0xb6: 0x501, // p
0xb9: 0x2302, // li
0xbb: 0xe409, // autofocus
0xbf: 0x27304, // html
0xc2: 0x4d08, // datatype
0xc6: 0x35d06, // prefix
0xcb: 0x35d03, // pre
0xcc: 0x1106, // accept
0xd1: 0x23b03, // for
0xd5: 0x29e06, // strong
0xd6: 0x9c07, // rowspan
0xd7: 0x25502, // h3
0xd8: 0x2cf04, // math
0xde: 0x16e07, // noshade
0xdf: 0x19f05, // shape
0xe1: 0x10006, // scoped
0xe3: 0x706, // target
0xe6: 0x21c0a, // figcaption
0xe9: 0x1df04, // text
0xea: 0x1c708, // resource
0xec: 0xee03, // map
0xf0: 0x29a06, // inlist
0xf1: 0x16506, // select
0xf2: 0x1f606, // keygen
0xf3: 0x5106, // typeof
0xf6: 0xb006, // canvas
0xf7: 0x30f06, // option
0xf8: 0xbe05, // label
0xf9: 0xbc03, // rel
0xfb: 0x1f04, // data
0xfd: 0x6004, // samp
0x100: 0x110e, // accept-charset
0x101: 0xeb06, // usemap
0x103: 0x2bc08, // manifest
0x109: 0xa204, // name
0x10a: 0x14806, // button
0x10b: 0x2b05, // clear
0x10e: 0x33907, // summary
0x10f: 0x2e204, // meta
0x110: 0x33108, // language
0x112: 0x300a, // background
0x113: 0x2707, // article
0x116: 0x23b0a, // formaction
0x119: 0x1, // a
0x11b: 0x5, // about
0x11c: 0xfc09, // itemscope
0x11e: 0x14d08, // noscript
0x11f: 0x15907, // classid
0x120: 0x36203, // xmp
0x121: 0x19604, // base
0x123: 0x1c01, // s
0x124: 0x36b07, // visible
0x126: 0x37b02, // bb
0x127: 0x9c04, // rows
0x12d: 0x2450e, // formnovalidate
0x131: 0x1f205, // track
0x135: 0x18703, // div
0x136: 0xac05, // async
0x137: 0x31508, // property
0x13a: 0x16c03, // dfn
0x13e: 0xf605, // inert
0x142: 0x10503, // del
0x144: 0x25302, // h2
0x147: 0x2c205, // style
0x149: 0x29703, // img
0x14a: 0xc05, // tbody
0x14b: 0x7603, // dir
0x14c: 0x2eb05, // xmlns
0x14e: 0x1f08, // datalist
0x14f: 0x32d04, // href
0x150: 0x1f202, // tr
0x151: 0x13e0a, // blockquote
0x152: 0x18909, // valuetype
0x155: 0xdb06, // footer
0x157: 0x14f06, // script
0x158: 0x1cf07, // colspan
0x15d: 0x1730e, // defaultChecked
0x15f: 0x2490a, // novalidate
0x164: 0x1a408, // codetype
0x165: 0x2c506, // legend
0x16b: 0x1160b, // pauseonexit
0x16c: 0x21f07, // caption
0x16f: 0x26c07, // enabled
0x173: 0x26206, // poster
0x175: 0x30a05, // muted
0x176: 0x11205, // ismap
0x178: 0x2a903, // ins
0x17a: 0xe004, // ruby
0x17b: 0x37c02, // br
0x17c: 0x8a0f, // defaultSelected
0x17d: 0x7403, // kbd
0x17f: 0x1c906, // source
0x182: 0x9f04, // span
0x184: 0x2d803, // max
0x18a: 0x5b02, // dd
0x18b: 0x13a04, // size
0x18c: 0xa405, // media
0x18d: 0x19208, // codebase
0x18f: 0x4905, // embed
0x192: 0x5104, // type
0x193: 0xf005, // param
0x194: 0x25b02, // h6
0x197: 0x28304, // icon
0x198: 0x12607, // bgcolor
0x199: 0x2ad0f, // allowfullscreen
0x19a: 0x12004, // time
0x19b: 0x7803, // rev
0x19d: 0x34208, // progress
0x19e: 0x22606, // figure
0x1a0: 0x6a02, // rp
0x1a2: 0xa606, // dialog
0x1a4: 0x2802, // rt
0x1a7: 0x1e304, // area
0x1a8: 0x7808, // reversed
0x1aa: 0x32104, // open
0x1ac: 0x2d204, // head
0x1ad: 0x7005, // alink
0x1af: 0x28003, // var
0x1b0: 0x15f07, // details
0x1b1: 0x2401, // i
0x1b3: 0x1e02, // td
0x1b4: 0xb707, // declare
0x1b5: 0x8302, // ul
0x1ba: 0x2fc06, // method
0x1bd: 0x13007, // section
0x1be: 0x22a08, // required
0x1c2: 0x9805, // defer
0x1c3: 0x37205, // vlink
0x1c4: 0x15405, // title
0x1c5: 0x2770a, // http-equiv
0x1c6: 0x1fa07, // enctype
0x1c7: 0x1ec07, // compact
0x1c8: 0x2d809, // maxlength
0x1c9: 0x16508, // selected
0x1cc: 0xd105, // audio
0x1cd: 0xc208, // longdesc
0x1d1: 0xfb04, // cite
0x1da: 0x2505, // start
0x1de: 0x2d102, // th
0x1df: 0x10808, // autoplay
0x1e2: 0x7104, // link
0x1e3: 0x206, // output
0x1e5: 0x12204, // menu
0x1e6: 0x2a405, // input
0x1eb: 0x32403, // nav
0x1ec: 0x31d03, // sub
0x1ee: 0x1807, // charset
0x1ef: 0x7f07, // default
0x1f3: 0x2f205, // table
0x1f4: 0x23b04, // form
0x1f5: 0x23209, // truespeed
0x1f6: 0x2f02, // rb
0x1fb: 0x20b09, // translate
0x1fd: 0x2e002, // h1
0x1: 0x13e06, // button
0x3: 0x2a207, // enctype
0x4: 0x32206, // option
0x5: 0x1fb0a, // figcaption
0x7: 0x2ae03, // ins
0x9: 0x9605, // audio
0xb: 0x2830b, // frameborder
0xd: 0x2190a, // formaction
0xe: 0x5, // about
0xf: 0x34606, // srcset
0x10: 0x1dd09, // truespeed
0x11: 0xeb0c, // defaultMuted
0x13: 0xa006, // footer
0x15: 0x19d08, // disabled
0x16: 0x26e0a, // http-equiv
0x19: 0x3a504, // link
0x1a: 0x29606, // inlist
0x1d: 0x10804, // ruby
0x21: 0x2a905, // input
0x22: 0x35803, // nav
0x25: 0x7902, // dd
0x26: 0x2350e, // formnovalidate
0x28: 0x16804, // href
0x29: 0x24702, // h4
0x2b: 0x10405, // defer
0x2d: 0x1f308, // fieldset
0x2e: 0xeb07, // default
0x34: 0x2fd04, // area
0x36: 0xb006, // typeof
0x37: 0x37307, // noembed
0x38: 0x5e07, // declare
0x3a: 0x4a07, // acronym
0x3b: 0xc05, // tbody
0x3e: 0x15107, // classid
0x41: 0x9a06, // object
0x43: 0x16403, // dfn
0x44: 0xef02, // ul
0x45: 0x16c04, // lang
0x47: 0x16606, // nohref
0x49: 0x2c803, // max
0x4a: 0x6505, // label
0x4c: 0x1d304, // size
0x4d: 0xe606, // scoped
0x4f: 0x15105, // class
0x50: 0x11404, // axis
0x54: 0xbf04, // span
0x56: 0x19707, // checked
0x59: 0x38506, // prefix
0x5b: 0x4208, // template
0x5c: 0x370d, // undeterminate
0x5d: 0xc606, // dialog
0x5e: 0x6908, // longdesc
0x60: 0x21903, // for
0x61: 0x2c102, // th
0x64: 0x15d08, // selected
0x65: 0x35103, // sub
0x6a: 0xd306, // usemap
0x6e: 0x24d06, // hgroup
0x6f: 0x38303, // sup
0x70: 0x2b404, // mark
0x71: 0x28206, // iframe
0x72: 0x30605, // meter
0x74: 0x21008, // required
0x75: 0x1f04, // data
0x78: 0x14308, // noframes
0x83: 0x7807, // address
0x88: 0x10c08, // autoplay
0x8a: 0x28e05, // image
0x8b: 0x16c08, // language
0x8e: 0x2f904, // text
0x8f: 0x16802, // hr
0x90: 0x5d02, // id
0x92: 0x31108, // multiple
0x94: 0x16808, // hreflang
0x95: 0x2db05, // xmlns
0x96: 0x24902, // h5
0x98: 0x25207, // picture
0x99: 0x1106, // accept
0x9a: 0x1a609, // valuetype
0x9b: 0x3a006, // portal
0x9d: 0xac08, // datatype
0x9e: 0x18403, // bdi
0xa0: 0x27a04, // icon
0xa2: 0xa503, // rev
0xa5: 0x25708, // resource
0xa8: 0x35504, // open
0xac: 0x4302, // em
0xae: 0x1340a, // blockquote
0xb0: 0x2f409, // plaintext
0xb1: 0x2d204, // meta
0xb2: 0x1c01, // s
0xb4: 0xdc04, // time
0xb5: 0x1fe07, // caption
0xb8: 0x33e08, // property
0xb9: 0x1, // a
0xbb: 0x2b807, // marquee
0xbc: 0x3b505, // vocab
0xbd: 0x1e502, // dl
0xbf: 0xbc07, // rowspan
0xc4: 0x18503, // dir
0xc5: 0x39908, // optgroup
0xcc: 0x38c06, // poster
0xcd: 0x24502, // h3
0xce: 0x3b804, // abbr
0xd1: 0x17408, // codebase
0xd2: 0x27b07, // content
0xd4: 0x7e04, // samp
0xd6: 0xc204, // name
0xd9: 0x14c05, // title
0xda: 0x1a605, // value
0xdd: 0xb004, // type
0xde: 0x35f04, // nobr
0xe0: 0x17c04, // font
0xe1: 0xd603, // map
0xe2: 0x2d002, // h1
0xe3: 0x22207, // noshade
0xe4: 0x6303, // rel
0xe5: 0x14002, // tt
0xe7: 0xde04, // menu
0xeb: 0x2f908, // textarea
0xee: 0x35b05, // align
0xf1: 0x29303, // img
0xf2: 0x35a06, // valign
0xf3: 0x2c204, // head
0xf4: 0x12a07, // bgcolor
0xf5: 0x5004, // main
0xf6: 0x2302, // li
0xf7: 0x5205, // inert
0xfa: 0x5706, // canvas
0xfb: 0xe605, // scope
0xfc: 0x15d06, // select
0x100: 0xa508, // reversed
0x101: 0x20408, // noscript
0x102: 0x37c07, // summary
0x103: 0x24b02, // h6
0x106: 0x17404, // code
0x107: 0x14508, // frameset
0x10a: 0x12406, // strong
0x10d: 0x300a, // background
0x10e: 0x18303, // kbd
0x114: 0x31706, // legend
0x116: 0x32f08, // progress
0x118: 0x2d408, // tabindex
0x119: 0x34603, // src
0x11c: 0x39505, // video
0x11f: 0x29a06, // strike
0x121: 0xd706, // applet
0x123: 0x2802, // rt
0x125: 0x20606, // script
0x128: 0xbc04, // rows
0x129: 0x2707, // article
0x12e: 0x9204, // cite
0x131: 0x18c0d, // typemustmatch
0x133: 0x17f05, // track
0x135: 0x3b902, // bb
0x136: 0x1ee05, // shape
0x137: 0x5b05, // aside
0x138: 0x1b60b, // pauseonexit
0x13c: 0x38503, // pre
0x140: 0x301, // u
0x149: 0x1a403, // div
0x14c: 0x3a405, // alink
0x14e: 0x27703, // var
0x14f: 0x21d06, // action
0x152: 0x2b05, // clear
0x154: 0x2401, // i
0x155: 0x21702, // dt
0x156: 0x36608, // seamless
0x157: 0x21904, // form
0x15b: 0x15707, // details
0x15f: 0x8e05, // async
0x160: 0x26a04, // html
0x161: 0x33608, // sortable
0x165: 0x2f02, // rb
0x167: 0x2e10f, // allowfullscreen
0x168: 0x17804, // base
0x169: 0x25f06, // hidden
0x16e: 0x2ef06, // nowrap
0x16f: 0x2505, // start
0x170: 0x14505, // frame
0x171: 0x1f08, // datalist
0x173: 0x12502, // tr
0x174: 0x30b06, // method
0x175: 0x101, // b
0x176: 0x1c904, // cols
0x178: 0x110e, // accept-charset
0x17a: 0x36205, // rules
0x17b: 0x7f0f, // amp-boilerplate
0x17f: 0x2270e, // defaultChecked
0x180: 0x32807, // profile
0x181: 0x2b004, // slot
0x182: 0x11a05, // param
0x185: 0x1c907, // colspan
0x186: 0x34c05, // style
0x187: 0x1e02, // td
0x188: 0x12c05, // color
0x18c: 0x13901, // q
0x18d: 0x3b005, // vlink
0x18e: 0x39203, // svg
0x18f: 0x33905, // table
0x190: 0x29e06, // keygen
0x192: 0x20c06, // figure
0x193: 0x3a907, // visible
0x195: 0x17808, // basefont
0x196: 0x8802, // rp
0x197: 0xf60f, // defaultSelected
0x198: 0x1af08, // colgroup
0x19a: 0x3bc03, // wbr
0x19c: 0x36d07, // section
0x19d: 0x25906, // source
0x19f: 0x2bf04, // math
0x1a1: 0x2fe08, // readonly
0x1a7: 0x1e708, // controls
0x1a9: 0xde08, // menuitem
0x1ad: 0x206, // output
0x1b0: 0x2c809, // maxlength
0x1b2: 0xe209, // itemscope
0x1b9: 0x501, // p
0x1bc: 0x2df05, // small
0x1bd: 0x36102, // br
0x1c0: 0x5503, // rtc
0x1c1: 0x1c009, // translate
0x1c4: 0x35303, // bdo
0x1c5: 0xd04, // body
0x1c8: 0xb706, // center
0x1c9: 0x2c105, // thead
0x1ca: 0xcc09, // autofocus
0x1cc: 0xb504, // face
0x1cd: 0x24302, // h2
0x1ce: 0x11e08, // manifest
0x1d0: 0x706, // target
0x1d1: 0x11605, // ismap
0x1d3: 0xc405, // media
0x1d7: 0x13103, // big
0x1da: 0x37903, // del
0x1dc: 0x6f09, // scrolling
0x1de: 0x37505, // embed
0x1e0: 0x31d05, // muted
0x1e4: 0x2390a, // novalidate
0x1e6: 0x7202, // ol
0x1eb: 0x9f05, // tfoot
0x1ec: 0x18808, // codetype
0x1ee: 0x26307, // enabled
0x1f0: 0x2c206, // header
0x1f1: 0x1cf08, // noresize
0x1f6: 0x1d707, // compact
0x1f9: 0x12c03, // col
0x1fa: 0x38a03, // xmp
0x1fb: 0x1807, // charset
}

View file

@ -129,7 +129,7 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
var params map[string]string
if rawTagHash == Iframe {
mimetype = htmlMimeBytes
} else if len(rawTagMediatype) > 0 {
} else if 0 < len(rawTagMediatype) {
mimetype, params = parse.Mediatype(rawTagMediatype)
} else if rawTagHash == Script {
mimetype = jsMimeBytes
@ -169,20 +169,15 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
t.Data = t.Data[:len(t.Data)-1]
omitSpace = false
break
} else if next.TokenType == html.TextToken {
// this only happens when a comment, doctype or phrasing end tag (only for !o.KeepWhitespace) was in between
// remove if the text token starts with a whitespace
if len(next.Data) > 0 && parse.IsWhitespace(next.Data[0]) {
t.Data = t.Data[:len(t.Data)-1]
omitSpace = false
}
} else if next.TokenType == html.TextToken && !parse.IsAllWhitespace(next.Data) {
// stop looking when text encountered
break
} else if next.TokenType == html.StartTagToken || next.TokenType == html.EndTagToken {
if o.KeepWhitespace {
break
}
// remove when followed up by a block tag
if next.Traits&nonPhrasingTag != 0 {
// remove when followed by a block tag
if next.Traits&blockTag != 0 {
t.Data = t.Data[:len(t.Data)-1]
omitSpace = false
break
@ -271,14 +266,14 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
}
}
if t.Traits&nonPhrasingTag != 0 {
omitSpace = true // omit spaces after block elements
} else if o.KeepWhitespace || t.Traits&objectTag != 0 {
omitSpace = false
}
if !omitEndTag {
if len(t.Data) > 3+len(t.Text) {
if o.KeepWhitespace || t.Traits&objectTag != 0 {
omitSpace = false
} else if t.Traits&blockTag != 0 {
omitSpace = true // omit spaces after block elements
}
if 3+len(t.Text) < len(t.Data) {
t.Data[2+len(t.Text)] = '>'
t.Data = t.Data[:3+len(t.Text)]
}
@ -296,7 +291,7 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
if o.KeepWhitespace || t.Traits&objectTag != 0 {
omitSpace = false
} else if t.Traits&nonPhrasingTag != 0 {
} else if t.Traits&blockTag != 0 {
omitSpace = true // omit spaces after block elements
}
@ -327,7 +322,7 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
for i := 0; i < len(content.AttrVal); i++ {
if content.AttrVal[i] == '=' && i+2 < len(content.AttrVal) {
i++
if n := parse.Number(content.AttrVal[i:]); n > 0 {
if n := parse.Number(content.AttrVal[i:]); 0 < n {
minNum := minify.Number(content.AttrVal[i:i+n], -1)
if len(minNum) < n {
copy(content.AttrVal[i:i+len(minNum)], minNum)
@ -434,10 +429,10 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
if len(val) == 0 {
continue
}
} else if len(attr.Text) > 2 && attr.Text[0] == 'o' && attr.Text[1] == 'n' {
} else if 2 < len(attr.Text) && attr.Text[0] == 'o' && attr.Text[1] == 'n' {
// JS minifier for attribute inline code
val = parse.TrimWhitespace(val)
if len(val) >= 11 && parse.EqualFold(val[:11], jsSchemeBytes) {
if 11 <= len(val) && parse.EqualFold(val[:11], jsSchemeBytes) {
val = val[11:]
}
attrMinifyBuffer.Reset()
@ -475,7 +470,7 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
w.Write(spaceBytes)
w.Write(attr.Text)
if len(val) > 0 && attr.Traits&booleanAttr == 0 {
if 0 < len(val) && attr.Traits&booleanAttr == 0 {
w.Write(isBytes)
// use double quotes for RDFa attributes
@ -504,7 +499,7 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
}
// keep space after phrasing tags (<i>, <span>, ...) FontAwesome etc.
if t.TokenType == html.StartTagToken && t.Traits&nonPhrasingTag == 0 {
if t.TokenType == html.StartTagToken && t.Traits == normalTag {
if next := tb.Peek(0); next.Hash == t.Hash && next.TokenType == html.EndTagToken {
omitSpace = false
}

View file

@ -3,12 +3,12 @@ package html
type traits uint16
const (
normalTag traits = 1 << iota
rawTag // raw tags need special processing for their content
nonPhrasingTag // non-phrasing elements are unaffected by whitespace, remove spaces around these tags
objectTag // content tags with a few exclusions, keep spaces after these open/close tags
omitPTag // omit p end tag if it is followed by this start tag
keepPTag // keep p end tag if it is followed by this end tag
normalTag traits = 1 << iota
rawTag // raw tags need special processing for their content
blockTag // remove spaces around these tags
objectTag // keep spaces after these open/close tags
omitPTag // omit p end tag if it is followed by this start tag
keepPTag // keep p end tag if it is followed by this end tag
)
const (
@ -21,54 +21,54 @@ const (
var tagMap = map[Hash]traits{
A: keepPTag,
Abbr: normalTag,
Address: nonPhrasingTag | omitPTag,
Address: blockTag | omitPTag,
Area: normalTag,
Article: nonPhrasingTag | omitPTag,
Aside: nonPhrasingTag | omitPTag,
Article: blockTag | omitPTag,
Aside: blockTag | omitPTag,
Audio: keepPTag,
B: normalTag,
Base: normalTag,
Bb: normalTag,
Bdi: normalTag,
Bdo: normalTag,
Blockquote: nonPhrasingTag | omitPTag,
Body: nonPhrasingTag,
Br: nonPhrasingTag,
Blockquote: blockTag | omitPTag,
Body: normalTag,
Br: blockTag,
Button: objectTag,
Canvas: objectTag | keepPTag,
Caption: nonPhrasingTag,
Caption: blockTag,
Cite: normalTag,
Code: normalTag,
Col: nonPhrasingTag,
Colgroup: nonPhrasingTag,
Col: blockTag,
Colgroup: blockTag,
Data: normalTag,
Datalist: normalTag,
Dd: nonPhrasingTag,
Datalist: normalTag, // no text content
Dd: blockTag,
Del: keepPTag,
Details: omitPTag,
Details: blockTag | omitPTag,
Dfn: normalTag,
Dialog: normalTag,
Div: nonPhrasingTag | omitPTag,
Dl: nonPhrasingTag | omitPTag,
Dt: nonPhrasingTag,
Div: blockTag | omitPTag,
Dl: blockTag | omitPTag,
Dt: blockTag,
Em: normalTag,
Embed: nonPhrasingTag,
Fieldset: nonPhrasingTag | omitPTag,
Figcaption: nonPhrasingTag | omitPTag,
Figure: nonPhrasingTag | omitPTag,
Footer: nonPhrasingTag | omitPTag,
Form: nonPhrasingTag | omitPTag,
H1: nonPhrasingTag | omitPTag,
H2: nonPhrasingTag | omitPTag,
H3: nonPhrasingTag | omitPTag,
H4: nonPhrasingTag | omitPTag,
H5: nonPhrasingTag | omitPTag,
H6: nonPhrasingTag | omitPTag,
Head: nonPhrasingTag,
Header: nonPhrasingTag | omitPTag,
Hgroup: nonPhrasingTag,
Hr: nonPhrasingTag | omitPTag,
Html: nonPhrasingTag,
Embed: normalTag,
Fieldset: blockTag | omitPTag,
Figcaption: blockTag | omitPTag,
Figure: blockTag | omitPTag,
Footer: blockTag | omitPTag,
Form: blockTag | omitPTag,
H1: blockTag | omitPTag,
H2: blockTag | omitPTag,
H3: blockTag | omitPTag,
H4: blockTag | omitPTag,
H5: blockTag | omitPTag,
H6: blockTag | omitPTag,
Head: blockTag,
Header: blockTag | omitPTag,
Hgroup: blockTag,
Hr: blockTag | omitPTag,
Html: blockTag,
I: normalTag,
Iframe: rawTag | objectTag,
Img: objectTag,
@ -76,64 +76,90 @@ var tagMap = map[Hash]traits{
Ins: keepPTag,
Kbd: normalTag,
Label: normalTag,
Legend: normalTag,
Li: nonPhrasingTag,
Legend: blockTag,
Li: blockTag,
Link: normalTag,
Main: nonPhrasingTag | omitPTag,
Main: blockTag | omitPTag,
Map: keepPTag,
Mark: normalTag,
Math: rawTag,
Menu: omitPTag,
Meta: nonPhrasingTag,
Menu: blockTag | omitPTag,
Meta: normalTag,
Meter: objectTag,
Nav: nonPhrasingTag | omitPTag,
Noscript: nonPhrasingTag | keepPTag,
Nav: blockTag | omitPTag,
Noscript: blockTag | keepPTag,
Object: objectTag,
Ol: nonPhrasingTag | omitPTag,
Optgroup: normalTag,
Option: normalTag,
Output: nonPhrasingTag,
P: nonPhrasingTag | omitPTag,
Ol: blockTag | omitPTag,
Optgroup: normalTag, // no text content
Option: blockTag,
Output: normalTag,
P: blockTag | omitPTag,
Param: normalTag,
Picture: normalTag,
Pre: nonPhrasingTag | omitPTag,
Pre: blockTag | omitPTag,
Progress: objectTag,
Q: objectTag,
Rp: normalTag,
Rt: normalTag,
Rt: objectTag,
Ruby: normalTag,
S: normalTag,
Samp: normalTag,
Script: rawTag,
Section: nonPhrasingTag | omitPTag,
Section: blockTag | omitPTag,
Select: objectTag,
Slot: normalTag,
Small: normalTag,
Source: normalTag,
Span: normalTag,
Strong: normalTag,
Style: rawTag | nonPhrasingTag,
Style: rawTag | blockTag,
Sub: normalTag,
Summary: normalTag,
Summary: blockTag,
Sup: normalTag,
Svg: rawTag | objectTag,
Table: nonPhrasingTag | omitPTag,
Tbody: nonPhrasingTag,
Td: nonPhrasingTag,
Table: blockTag | omitPTag,
Tbody: blockTag,
Td: blockTag,
Template: normalTag,
Textarea: rawTag | objectTag,
Tfoot: nonPhrasingTag,
Th: nonPhrasingTag,
Thead: nonPhrasingTag,
Tfoot: blockTag,
Th: blockTag,
Thead: blockTag,
Time: normalTag,
Title: nonPhrasingTag,
Tr: nonPhrasingTag,
Title: normalTag,
Tr: blockTag,
Track: normalTag,
U: normalTag,
Ul: nonPhrasingTag | omitPTag,
Ul: blockTag | omitPTag,
Var: normalTag,
Video: objectTag | keepPTag,
Wbr: normalTag,
Wbr: objectTag,
// removed tags
Acronym: normalTag,
Applet: normalTag,
Basefont: normalTag,
Big: normalTag,
Center: blockTag,
Dir: blockTag,
Font: normalTag,
Frame: normalTag,
Frameset: normalTag,
Image: objectTag,
Marquee: blockTag,
Menuitem: normalTag,
Nobr: normalTag,
Noembed: blockTag,
Noframes: blockTag,
Plaintext: normalTag,
Rtc: objectTag,
Rb: normalTag,
Strike: normalTag,
Tt: normalTag,
Xmp: blockTag,
// experimental tags
Portal: normalTag,
}
var attrMap = map[Hash]traits{
@ -574,7 +600,7 @@ var EntitiesMap = map[string][]byte{
"SupersetEqual": []byte("&supe;"),
"Supset": []byte("&Sup;"),
"THORN": []byte("&#222;"),
"Tab": []byte(" "),
"Tab": []byte("\t"),
"Tcaron": []byte("&#356;"),
"Tcedil": []byte("&#354;"),
"Therefore": []byte("&#8756;"),

63
vendor/github.com/tdewolff/parse/v2/strconv/decimal.go generated vendored Normal file
View file

@ -0,0 +1,63 @@
package strconv
import (
"math"
)
func ParseDecimal(b []byte) (float64, int) {
i := 0
start := i
dot := -1
trunk := -1
n := uint64(0)
for ; i < len(b); i++ {
c := b[i]
if '0' <= c && c <= '9' {
if trunk == -1 {
if math.MaxUint64/10 < n {
trunk = i
} else {
n *= 10
n += uint64(c - '0')
}
}
} else if dot == -1 && c == '.' {
dot = i
} else {
break
}
}
if i == start || i == start+1 && dot == start {
return 0.0, 0
}
f := float64(n)
mantExp := int64(0)
if dot != -1 {
if trunk == -1 {
trunk = i
}
mantExp = int64(trunk - dot - 1)
} else if trunk != -1 {
mantExp = int64(trunk - i)
}
exp := -mantExp
// copied from strconv/atof.go
if exp == 0 {
return f, i
} else if 0 < exp && exp <= 15+22 { // int * 10^k
// If exponent is big but number of digits is not,
// can move a few zeros into the integer part.
if 22 < exp {
f *= float64pow10[exp-22]
exp = 22
}
if -1e15 <= f && f <= 1e15 {
return f * float64pow10[exp], i
}
} else if exp < 0 && -22 <= exp { // int / 10^k
return f / float64pow10[-exp], i
}
return f * math.Pow10(int(-mantExp)), i
}

View file

@ -25,9 +25,9 @@ func ParseFloat(b []byte) (float64, int) {
n := uint64(0)
for ; i < len(b); i++ {
c := b[i]
if c >= '0' && c <= '9' {
if '0' <= c && c <= '9' {
if trunk == -1 {
if n > math.MaxUint64/10 {
if math.MaxUint64/10 < n {
trunk = i
} else {
n *= 10
@ -62,7 +62,7 @@ func ParseFloat(b []byte) (float64, int) {
if i < len(b) && (b[i] == 'e' || b[i] == 'E') {
startExp := i
i++
if e, expLen := ParseInt(b[i:]); expLen > 0 {
if e, expLen := ParseInt(b[i:]); 0 < expLen {
expExp = e
i += expLen
} else {
@ -74,17 +74,17 @@ func ParseFloat(b []byte) (float64, int) {
// copied from strconv/atof.go
if exp == 0 {
return f, i
} else if exp > 0 && exp <= 15+22 { // int * 10^k
} else if 0 < exp && exp <= 15+22 { // int * 10^k
// If exponent is big but number of digits is not,
// can move a few zeros into the integer part.
if exp > 22 {
if 22 < exp {
f *= float64pow10[exp-22]
exp = 22
}
if f <= 1e15 && f >= -1e15 {
if -1e15 <= f && f <= 1e15 {
return f * float64pow10[exp], i
}
} else if exp < 0 && exp >= -22 { // int / 10^k
} else if -22 <= exp && exp < 0 { // int / 10^k
return f / float64pow10[-exp], i
}
f *= math.Pow10(int(-mantExp))
@ -135,7 +135,7 @@ func AppendFloat(b []byte, f float64, prec int) ([]byte, bool) {
// expLen is zero for positive exponents, because positive exponents are determined later on in the big conversion loop
exp := 0
expLen := 0
if mantExp > 0 {
if 0 < mantExp {
// positive exponent is determined in the loop below
// but if we initially decreased the exponent to fit in an integer, we can't set the new exponent in the loop alone,
// since the number of zeros at the end determines the positive exponent in the loop, and we just artificially lost zeros
@ -156,7 +156,7 @@ func AppendFloat(b []byte, f float64, prec int) ([]byte, bool) {
if neg {
maxLen++
}
if i+maxLen > cap(b) {
if cap(b) < i+maxLen {
b = append(b, make([]byte, maxLen)...)
} else {
b = b[:i+maxLen]
@ -175,17 +175,17 @@ func AppendFloat(b []byte, f float64, prec int) ([]byte, bool) {
last := i + mantLen // right-most position of digit that is non-zero + dot
dot := last - prec - exp // position of dot
j := last
for mant > 0 {
for 0 < mant {
if j == dot {
b[j] = '.'
j--
}
newMant := mant / 10
digit := mant - 10*newMant
if zero && digit > 0 {
if zero && 0 < digit {
// first non-zero digit, if we are still behind the dot we can trim the end to this position
// otherwise trim to the dot (including the dot)
if j > dot {
if dot < j {
i = j + 1
// decrease negative exponent further to get rid of dot
if exp < 0 {
@ -209,9 +209,9 @@ func AppendFloat(b []byte, f float64, prec int) ([]byte, bool) {
mant = newMant
}
if j > dot {
if dot < j {
// extra zeros behind the dot
for j > dot {
for dot < j {
b[j] = '0'
j--
}
@ -244,7 +244,7 @@ func AppendFloat(b []byte, f float64, prec int) ([]byte, bool) {
}
i += LenInt(int64(exp))
j := i
for exp > 0 {
for 0 < exp {
newExp := exp / 10
digit := exp - 10*newExp
j--

View file

@ -20,14 +20,14 @@ func AppendPrice(b []byte, price int64, dec bool, milSeparator byte, decSeparato
// rounding
if !dec {
firstDec := (price / 10) % 10
if firstDec >= 5 {
if 5 <= firstDec {
price += 100
}
}
// calculate size
n := LenInt(price) - 2
if n > 0 {
if 0 < n {
n += (n - 1) / 3 // mil separator
} else {
n = 1
@ -38,7 +38,7 @@ func AppendPrice(b []byte, price int64, dec bool, milSeparator byte, decSeparato
// resize byte slice
i := len(b)
if i+n > cap(b) {
if cap(b) < i+n {
b = append(b, make([]byte, n)...)
} else {
b = b[:i+n]
@ -66,7 +66,7 @@ func AppendPrice(b []byte, price int64, dec bool, milSeparator byte, decSeparato
// print integer-part
j := 0
for price > 0 {
for 0 < price {
if j == 3 {
b[i] = milSeparator
i--

View file

@ -194,9 +194,8 @@ func render1(w writer, n *Node) error {
}
}
// Render any child nodes.
switch n.Data {
case "iframe", "noembed", "noframes", "noscript", "plaintext", "script", "style", "xmp":
// Render any child nodes
if childTextNodesAreLiteral(n) {
for c := n.FirstChild; c != nil; c = c.NextSibling {
if c.Type == TextNode {
if _, err := w.WriteString(c.Data); err != nil {
@ -213,7 +212,7 @@ func render1(w writer, n *Node) error {
// last element in the file, with no closing tag.
return plaintextAbort
}
default:
} else {
for c := n.FirstChild; c != nil; c = c.NextSibling {
if err := render1(w, c); err != nil {
return err
@ -231,6 +230,27 @@ func render1(w writer, n *Node) error {
return w.WriteByte('>')
}
func childTextNodesAreLiteral(n *Node) bool {
// Per WHATWG HTML 13.3, if the parent of the current node is a style,
// script, xmp, iframe, noembed, noframes, or plaintext element, and the
// current node is a text node, append the value of the node's data
// literally. The specification is not explicit about it, but we only
// enforce this if we are in the HTML namespace (i.e. when the namespace is
// "").
// NOTE: we also always include noscript elements, although the
// specification states that they should only be rendered as such if
// scripting is enabled for the node (which is not something we track).
if n.Namespace != "" {
return false
}
switch n.Data {
case "iframe", "noembed", "noframes", "noscript", "plaintext", "script", "style", "xmp":
return true
default:
return false
}
}
// writeQuoted writes s to w surrounded by quotes. Normally it will use double
// quotes, but if s contains a double quote, it will use single quotes.
// It is used for writing the identifiers in a doctype declaration.

View file

@ -19,6 +19,7 @@ import (
"io/fs"
"log"
"math"
"math/bits"
mathrand "math/rand"
"net"
"net/http"
@ -518,11 +519,14 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
func authorityAddr(scheme string, authority string) (addr string) {
host, port, err := net.SplitHostPort(authority)
if err != nil { // authority didn't have a port
host = authority
port = ""
}
if port == "" { // authority's port was empty
port = "443"
if scheme == "http" {
port = "80"
}
host = authority
}
if a, err := idna.ToASCII(host); err == nil {
host = a
@ -1677,7 +1681,27 @@ func (cs *clientStream) frameScratchBufferLen(maxFrameSize int) int {
return int(n) // doesn't truncate; max is 512K
}
var bufPool sync.Pool // of *[]byte
// Seven bufPools manage different frame sizes. This helps to avoid scenarios where long-running
// streaming requests using small frame sizes occupy large buffers initially allocated for prior
// requests needing big buffers. The size ranges are as follows:
// {0 KB, 16 KB], {16 KB, 32 KB], {32 KB, 64 KB], {64 KB, 128 KB], {128 KB, 256 KB],
// {256 KB, 512 KB], {512 KB, infinity}
// In practice, the maximum scratch buffer size should not exceed 512 KB due to
// frameScratchBufferLen(maxFrameSize), thus the "infinity pool" should never be used.
// It exists mainly as a safety measure, for potential future increases in max buffer size.
var bufPools [7]sync.Pool // of *[]byte
func bufPoolIndex(size int) int {
if size <= 16384 {
return 0
}
size -= 1
bits := bits.Len(uint(size))
index := bits - 14
if index >= len(bufPools) {
return len(bufPools) - 1
}
return index
}
func (cs *clientStream) writeRequestBody(req *http.Request) (err error) {
cc := cs.cc
@ -1695,12 +1719,13 @@ func (cs *clientStream) writeRequestBody(req *http.Request) (err error) {
// Scratch buffer for reading into & writing from.
scratchLen := cs.frameScratchBufferLen(maxFrameSize)
var buf []byte
if bp, ok := bufPool.Get().(*[]byte); ok && len(*bp) >= scratchLen {
defer bufPool.Put(bp)
index := bufPoolIndex(scratchLen)
if bp, ok := bufPools[index].Get().(*[]byte); ok && len(*bp) >= scratchLen {
defer bufPools[index].Put(bp)
buf = *bp
} else {
buf = make([]byte, scratchLen)
defer bufPool.Put(&buf)
defer bufPools[index].Put(&buf)
}
var sawEOF bool

View file

@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build appengine
// +build appengine
package internal

12
vendor/modules.txt vendored
View file

@ -659,11 +659,11 @@ github.com/superseriousbusiness/oauth2/v4/generates
github.com/superseriousbusiness/oauth2/v4/manage
github.com/superseriousbusiness/oauth2/v4/models
github.com/superseriousbusiness/oauth2/v4/server
# github.com/tdewolff/minify/v2 v2.12.7
## explicit; go 1.13
# github.com/tdewolff/minify/v2 v2.12.8
## explicit; go 1.18
github.com/tdewolff/minify/v2
github.com/tdewolff/minify/v2/html
# github.com/tdewolff/parse/v2 v2.6.6
# github.com/tdewolff/parse/v2 v2.6.7
## explicit; go 1.13
github.com/tdewolff/parse/v2
github.com/tdewolff/parse/v2/buffer
@ -852,7 +852,7 @@ golang.org/x/image/webp
# golang.org/x/mod v0.10.0
## explicit; go 1.17
golang.org/x/mod/semver
# golang.org/x/net v0.12.0
# golang.org/x/net v0.14.0
## explicit; go 1.17
golang.org/x/net/bpf
golang.org/x/net/context
@ -870,8 +870,8 @@ golang.org/x/net/ipv4
golang.org/x/net/ipv6
golang.org/x/net/publicsuffix
golang.org/x/net/trace
# golang.org/x/oauth2 v0.10.0
## explicit; go 1.17
# golang.org/x/oauth2 v0.11.0
## explicit; go 1.18
golang.org/x/oauth2
golang.org/x/oauth2/internal
# golang.org/x/sys v0.11.0

45
web/assets/feditext.svg Normal file
View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="580"
height="648.07129"
viewBox="0 0 580 648.07129"
sodipodi:docname="feditext.svg"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.60078125"
inkscape:cx="643.329"
inkscape:cy="640"
inkscape:window-width="2752"
inkscape:window-height="1083"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="Image 1">
<path
style="fill:#ffffff;stroke-width:1.06266"
d="m 178.31448,648.0457 c 0,-0.22287 -3.31804,-12.73571 -7.37341,-27.8064 C 166.88568,605.16868 147.31446,532.1071 127.44944,457.88029 85.527985,301.23825 87.197265,307.36035 86.112035,306.27513 c -0.46091,-0.46092 -14.21456,2.67442 -30.56365,6.96742 -16.349101,4.293 -31.208011,8.12989 -33.019811,8.52642 l -3.29419,0.72096 -6.51865,-24.22492 C 2.0285837,258.5489 -0.57989635,248.01817 0.10089365,247.33738 c 0.35549,-0.3555 14.89589035,-4.46095 32.31199035,-9.12325 17.416101,-4.66229 32.068961,-8.85856 32.561911,-9.32506 0.49294,-0.46651 -1.98796,-11.77814 -5.51312,-25.13696 -8.38344,-31.76958 -9.39714,-36.33345 -11.30815,-50.91138 -5.98718,-45.67279 5.20679,-81.973838 33.81088,-109.645688 21.331845,-20.63662 48.691955,-33.6522399 86.724635,-41.2562299 10.997,-2.19866004 13.45942,-2.40581004 14.63412,-1.23110004 1.13937,1.13937004 22.62048,78.49091794 22.62048,81.45430794 0,0.46185 -4.05603,1.64524 -9.0134,2.62977 -24.76037,4.91735 -40.46005,14.46453 -46.82825,28.47687 -3.90046,8.5824 -4.77238,23.27239 -2.19535,36.98665 1.75696,9.35007 12.75641,51.76056 13.65838,52.66253 0.4428,0.44282 121.76948,-31.65747 131.29224,-34.73694 2.50328,-0.8095 3.34893,-1.63052 2.9807,-2.8938 -4.05367,-13.9066 -28.24159,-106.808348 -27.89809,-107.151858 0.45121,-0.4512 89.39306,-24.41748 92.86632,-25.02376 1.37732,-0.24042 4.22212,9.07873 15.31855,50.181108 7.4949,27.76199 14.02122,51.91095 14.50292,53.66433 0.53811,1.9588 1.67104,3.29946 2.93832,3.4771 1.13437,0.15901 18.23012,-4.02521 37.99054,-9.29827 19.76042,-5.27307 36.58465,-9.5874 37.38716,-9.5874 0.80252,0 1.70596,0.83685 2.00766,1.85966 1.15593,3.91878 15.49189,57.51001 17.43187,65.16443 l 2.03934,8.04645 -38.34515,10.31829 c -21.08983,5.67507 -38.67242,10.66171 -39.07242,11.08144 -0.56974,0.59783 48.98078,187.79383 57.02149,215.42048 4.67507,16.06282 13.21057,28.01206 23.68276,33.15461 5.63954,2.76939 6.94454,2.97246 18.81187,2.92708 11.85639,-0.0453 13.49817,-0.31471 23.37852,-3.83585 5.8446,-2.0829 13.37624,-5.04366 16.73685,-6.57947 3.36068,-1.53581 6.11032,-2.55248 6.11032,-2.25928 0,0.29321 4.782,18.33159 10.6266,40.08529 C 575.21806,518.68519 580,537.03684 580,537.71293 c 0,1.63704 -22.9762,11.00659 -38.25578,15.60048 -25.7852,7.75246 -32.1521,8.71291 -57.38362,8.65622 -22.14919,-0.0498 -23.12316,-0.14288 -31.8798,-3.04791 -29.12764,-9.66317 -49.29263,-28.46977 -63.86201,-59.56002 -6.69221,-14.28079 -9.86436,-25.16989 -39.68012,-136.21013 -35.98923,-134.03153 -31.51618,-118.94286 -35.13039,-118.50324 -3.44623,0.41919 -124.18412,32.49994 -128.5205,34.14862 -2.5808,0.98123 -2.81004,1.46652 -2.14776,4.54676 0.75671,3.51942 6.27829,24.2355 54.87563,205.88505 34.86397,130.31626 35.4586,132.60367 34.67505,133.38728 -0.55047,0.55045 -26.01282,7.54339 -82.42129,22.6361 -6.57521,1.75925 -11.95493,3.01638 -11.95493,2.79356 z"
id="path2-3" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4 KiB

View file

@ -55,6 +55,14 @@
<a href="https://tusky.app" target="_blank" rel="noopener">Get Tusky</a>
</div>
</div>
<div class="entry">
<img class="logo" src="/assets/feditext.svg" alt="The Feditext logo, the characters ft at a slight angle">
<div>
<h2>Feditext</h2>
<p>Feditext (beta) is a beautiful client for iOS, iPadOS and macOS.</p>
<a href="https://fedi.software/@Feditext" target="_blank" rel="noopener">Get Feditext</a>
</div>
</div>
<div class="entry">
<img class="logo" src="/assets/mastodon.svg" alt="The Mastodon logo, the character M in a speech bubble">
<div>