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. 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 * [Tusky](https://tusky.app/) for Android
:-----------------------------------------------------------:|:------------------------------------------------------------------: * [Semaphore](https://semaphore.social/) in the browser
![An image of GoToSocial in Tusky](./docs/assets/tusky.png) | ![An image of GoToSocial in Semaphore](./docs/assets/semaphore.png) * [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 ### 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? ## 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? ## 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 ## 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 ## (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 ### 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 ### Federation Issues

8
go.mod
View file

@ -45,7 +45,7 @@ require (
github.com/superseriousbusiness/activity v1.4.0-gts github.com/superseriousbusiness/activity v1.4.0-gts
github.com/superseriousbusiness/exif-terminator v0.5.0 github.com/superseriousbusiness/exif-terminator v0.5.0
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8 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/ulule/limiter/v3 v3.11.2
github.com/uptrace/bun v1.1.14 github.com/uptrace/bun v1.1.14
github.com/uptrace/bun/dialect/pgdialect 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/crypto v0.12.0
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
golang.org/x/image v0.11.0 golang.org/x/image v0.11.0
golang.org/x/net v0.12.0 golang.org/x/net v0.14.0
golang.org/x/oauth2 v0.10.0 golang.org/x/oauth2 v0.11.0
golang.org/x/text v0.12.0 golang.org/x/text v0.12.0
gopkg.in/mcuadros/go-syslog.v2 v2.3.0 gopkg.in/mcuadros/go-syslog.v2 v2.3.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
@ -152,7 +152,7 @@ require (
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.2 // 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/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/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // 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/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 v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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-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 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 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/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 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= 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 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 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= 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/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 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 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.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-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 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/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 h1:nTIhuP157oOFcscuoK1kCme1xTeGIzztSw70lX9NrDQ=
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8/go.mod h1:uYC/W92oVRJ49Vh1GcvTqpeFqHi+Ovrl2sMllQWRAEo= 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.8 h1:Q2BqOTmlMjoutkuD/OPCnJUpIqrzT3nRPkw+q+KpXS0=
github.com/tdewolff/minify/v2 v2.12.7/go.mod h1:ZRKTheiOGyLSK8hOZWWv+YoJAECzDivNgAlVYDHp/Ws= github.com/tdewolff/minify/v2 v2.12.8/go.mod h1:YRgk7CC21LZnbuke2fmYnCTq+zhCgpb0yJACOTUNJ1E=
github.com/tdewolff/parse/v2 v2.6.6 h1:Yld+0CrKUJaCV78DL1G2nk3C9lKrxyRTux5aaK/AkDo= github.com/tdewolff/parse/v2 v2.6.7 h1:WrFllrqmzAcrKHzoYgMupqgUBIfBVOb0yscFzDf8bBg=
github.com/tdewolff/parse/v2 v2.6.6/go.mod h1:woz0cgbLwFdtbjJu8PIKxhW05KplTFQkOdX78o+Jgrs= github.com/tdewolff/parse/v2 v2.6.7/go.mod h1:XHDhaU6IBgsryfdnpzUXBlT6leW/l25yrFBTEb4eIyM=
github.com/tdewolff/test v1.0.7/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/tdewolff/test v1.0.9 h1:SswqJCmeN4B+9gEAi/5uqT0qpi1y2/2O47V/1hhGZT0= 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/tdewolff/test v1.0.9/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 h1:G6Z6HvJuPjG6XfNGi/feOATzeJrfgTNJY+rGrHbA04E= 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-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.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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= 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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/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-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-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.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU=
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= 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-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-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/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/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/api/auth" "github.com/superseriousbusiness/gotosocial/internal/api/auth"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/testrig" "github.com/superseriousbusiness/gotosocial/internal/util"
) )
type AuthAuthorizeTestSuite struct { type AuthAuthorizeTestSuite struct {
@ -51,8 +51,8 @@ func (suite *AuthAuthorizeTestSuite) TestAccountAuthorizeHandler() {
mutateUserAccount: func(user *gtsmodel.User, account *gtsmodel.Account) []string { mutateUserAccount: func(user *gtsmodel.User, account *gtsmodel.Account) []string {
user.ConfirmedAt = time.Now() user.ConfirmedAt = time.Now()
user.Email = user.UnconfirmedEmail user.Email = user.UnconfirmedEmail
user.Approved = testrig.TrueBool() user.Approved = util.Ptr(true)
user.Disabled = testrig.TrueBool() user.Disabled = util.Ptr(true)
return []string{"confirmed_at", "email", "approved", "disabled"} return []string{"confirmed_at", "email", "approved", "disabled"}
}, },
expectedStatusCode: http.StatusSeeOther, expectedStatusCode: http.StatusSeeOther,
@ -63,8 +63,8 @@ func (suite *AuthAuthorizeTestSuite) TestAccountAuthorizeHandler() {
mutateUserAccount: func(user *gtsmodel.User, account *gtsmodel.Account) []string { mutateUserAccount: func(user *gtsmodel.User, account *gtsmodel.Account) []string {
user.ConfirmedAt = time.Now() user.ConfirmedAt = time.Now()
user.Email = user.UnconfirmedEmail user.Email = user.UnconfirmedEmail
user.Approved = testrig.TrueBool() user.Approved = util.Ptr(true)
user.Disabled = testrig.FalseBool() user.Disabled = util.Ptr(false)
account.SuspendedAt = time.Now() account.SuspendedAt = time.Now()
return []string{"confirmed_at", "email", "approved", "disabled"} return []string{"confirmed_at", "email", "approved", "disabled"}
}, },

View file

@ -87,7 +87,7 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
return return
} }
if err := validateCreateAccount(form); err != nil { if err := validateNormalizeCreateAccount(form); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
return return
} }
@ -110,9 +110,10 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
c.JSON(http.StatusOK, ti) 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. // 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 { if form == nil {
return errors.New("form was 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") 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 return err
} }
form.Locale = locale
return validate.SignUpReason(form.Reason, config.GetAccountsReasonRequired()) return validate.SignUpReason(form.Reason, config.GetAccountsReasonRequired())
} }

View file

@ -138,7 +138,7 @@ func (suite *ReportResolveTestSuite) TestReportResolve2() {
testToken := suite.testTokens["admin_account"] testToken := suite.testTokens["admin_account"]
testUser := suite.testUsers["admin_account"] testUser := suite.testUsers["admin_account"]
testReportID := suite.testReports["local_account_2_report_remote_account_1"].ID 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) report, err := suite.resolveReport(testAccount, testToken, testUser, testReportID, http.StatusOK, "", actionTakenComment)
suite.NoError(err) suite.NoError(err)

View file

@ -32,6 +32,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/testrig" "github.com/superseriousbusiness/gotosocial/testrig"
) )
@ -963,7 +964,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetResolvedTargetAccount() {
testAccount := suite.testAccounts["admin_account"] testAccount := suite.testAccounts["admin_account"]
testToken := suite.testTokens["admin_account"] testToken := suite.testTokens["admin_account"]
testUser := suite.testUsers["admin_account"] testUser := suite.testUsers["admin_account"]
resolved := testrig.FalseBool() resolved := util.Ptr(false)
targetAccount := suite.testAccounts["local_account_2"] targetAccount := suite.testAccounts["local_account_2"]
reports, link, err := suite.getReports(testAccount, testToken, testUser, http.StatusOK, "", resolved, "", targetAccount.ID, "", "", "", 20) 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/api/client/instance"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/testrig" "github.com/superseriousbusiness/gotosocial/testrig"
) )
@ -231,7 +232,7 @@ func (suite *InstancePeersGetTestSuite) TestInstancePeersGetAllWithObfuscated()
Domain: "omg.just.the.worst.org.ever", Domain: "omg.just.the.worst.org.ever",
CreatedByAccountID: "01F8MH17FWEB39HZJ76B6VXSKF", CreatedByAccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
PublicComment: "just absolutely the worst, wowza", PublicComment: "just absolutely the worst, wowza",
Obfuscate: testrig.TrueBool(), Obfuscate: util.Ptr(true),
}) })
suite.NoError(err) suite.NoError(err)

View file

@ -32,6 +32,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/testrig" "github.com/superseriousbusiness/gotosocial/testrig"
) )
@ -197,7 +198,7 @@ func (suite *ReportsGetTestSuite) TestGetReports4() {
testAccount := suite.testAccounts["local_account_2"] testAccount := suite.testAccounts["local_account_2"]
testToken := suite.testTokens["local_account_2"] testToken := suite.testTokens["local_account_2"]
testUser := suite.testUsers["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) reports, link, err := suite.getReports(testAccount, testToken, testUser, http.StatusOK, resolved, "", "", "", "", 20)
suite.NoError(err) suite.NoError(err)
@ -252,7 +253,7 @@ func (suite *ReportsGetTestSuite) TestGetReports5() {
testAccount := suite.testAccounts["local_account_1"] testAccount := suite.testAccounts["local_account_1"]
testToken := suite.testTokens["local_account_1"] testToken := suite.testTokens["local_account_1"]
testUser := suite.testUsers["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) reports, link, err := suite.getReports(testAccount, testToken, testUser, http.StatusOK, resolved, "", "", "", "", 20)
suite.NoError(err) suite.NoError(err)
@ -323,7 +324,7 @@ func (suite *ReportsGetTestSuite) TestGetReports7() {
testAccount := suite.testAccounts["local_account_2"] testAccount := suite.testAccounts["local_account_2"]
testToken := suite.testTokens["local_account_2"] testToken := suite.testTokens["local_account_2"]
testUser := suite.testUsers["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) reports, link, err := suite.getReports(testAccount, testToken, testUser, http.StatusOK, resolved, "01F8MH5ZK5VRH73AKHQM6Y9VNX", "", "", "", 20)
suite.NoError(err) 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" // 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) apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
return return
} }
@ -112,7 +112,9 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
c.JSON(http.StatusOK, apiStatus) 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 != "" hasStatus := form.Status != ""
hasMedia := len(form.MediaIDs) != 0 hasMedia := len(form.MediaIDs) != 0
hasPoll := form.Poll != nil hasPoll := form.Poll != nil
@ -162,9 +164,11 @@ func validateCreateStatus(form *apimodel.AdvancedStatusCreateForm) error {
} }
if form.Language != "" { if form.Language != "" {
if err := validate.Language(form.Language); err != nil { language, err := validate.Language(form.Language)
if err != nil {
return err return err
} }
form.Language = language
} }
return nil return nil

View file

@ -391,6 +391,42 @@ func (suite *StatusCreateTestSuite) TestAttachNewMediaSuccess() {
suite.Equal(statusResponse.ID, gtsAttachment.StatusID) 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) { func TestStatusCreateTestSuite(t *testing.T) {
suite.Run(t, new(StatusCreateTestSuite)) suite.Run(t, new(StatusCreateTestSuite))
} }

View file

@ -36,6 +36,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/testrig" "github.com/superseriousbusiness/gotosocial/testrig"
) )
@ -165,14 +166,14 @@ func (suite *StatusPinTestSuite) TestPinStatusTooManyPins() {
PinnedAt: time.Now(), PinnedAt: time.Now(),
URL: "stub " + strconv.Itoa(i), URL: "stub " + strconv.Itoa(i),
URI: "stub " + strconv.Itoa(i), URI: "stub " + strconv.Itoa(i),
Local: testrig.TrueBool(), Local: util.Ptr(true),
AccountID: testAccount.ID, AccountID: testAccount.ID,
AccountURI: testAccount.URI, AccountURI: testAccount.URI,
Visibility: gtsmodel.VisibilityPublic, Visibility: gtsmodel.VisibilityPublic,
Federated: testrig.TrueBool(), Federated: util.Ptr(true),
Boostable: testrig.TrueBool(), Boostable: util.Ptr(true),
Replyable: testrig.TrueBool(), Replyable: util.Ptr(true),
Likeable: testrig.TrueBool(), Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
} }
if err := suite.db.PutStatus(ctx, status); err != nil { 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/log"
"github.com/superseriousbusiness/gotosocial/internal/state" "github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/uris" "github.com/superseriousbusiness/gotosocial/internal/uris"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/uptrace/bun" "github.com/uptrace/bun"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
@ -198,17 +199,15 @@ func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) (
user.Email = newSignup.Email user.Email = newSignup.Email
} }
trueBool := func() *bool { t := true; return &t }
if newSignup.Admin { if newSignup.Admin {
// Make new user mod + admin. // Make new user mod + admin.
user.Moderator = trueBool() user.Moderator = util.Ptr(true)
user.Admin = trueBool() user.Admin = util.Ptr(true)
} }
if newSignup.PreApproved { if newSignup.PreApproved {
// Mark new user as approved. // Mark new user as approved.
user.Approved = trueBool() user.Approved = util.Ptr(true)
} }
// Insert the user! // Insert the user!

View file

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

View file

@ -28,7 +28,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/testrig" "github.com/superseriousbusiness/gotosocial/internal/util"
) )
func (suite *NotificationTestSuite) spamNotifs() { func (suite *NotificationTestSuite) spamNotifs() {
@ -70,7 +70,7 @@ func (suite *NotificationTestSuite) spamNotifs() {
TargetAccountID: targetAccountID, TargetAccountID: targetAccountID,
OriginAccountID: originAccountID, OriginAccountID: originAccountID,
StatusID: statusID, StatusID: statusID,
Read: testrig.FalseBool(), Read: util.Ptr(false),
} }
if err := suite.db.Put(context.Background(), notif); err != nil { 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/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/testrig" "github.com/superseriousbusiness/gotosocial/internal/util"
) )
type RelationshipTestSuite struct { type RelationshipTestSuite struct {
@ -892,7 +892,7 @@ func (suite *RelationshipTestSuite) TestUpdateFollow() {
follow := &gtsmodel.Follow{} follow := &gtsmodel.Follow{}
*follow = *suite.testFollows["local_account_1_admin_account"] *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 { if err := suite.db.UpdateFollow(ctx, follow, "notify"); err != nil {
suite.FailNow(err.Error()) suite.FailNow(err.Error())
} }

View file

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

View file

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

View file

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

View file

@ -21,13 +21,13 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strings"
"codeberg.org/gruf/go-kv"
"codeberg.org/gruf/go-logger/v2/level" "codeberg.org/gruf/go-logger/v2/level"
"github.com/superseriousbusiness/activity/pub"
"github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/log"
@ -47,14 +47,16 @@ import (
// Under certain conditions and network activities, Create may be called // Under certain conditions and network activities, Create may be called
// multiple times for the same ActivityStreams object. // multiple times for the same ActivityStreams object.
func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error { 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) i, err := marshalItem(asType)
if err != nil { if err != nil {
return err return err
} }
l := log.WithContext(ctx).
WithField("create", i) log.
l.Trace("entering Create") WithContext(ctx).
WithField("create", i).
Trace("entering Create")
} }
receivingAccount, requestingAccount, internal := extractFromCtx(ctx) receivingAccount, requestingAccount, internal := extractFromCtx(ctx)
@ -116,92 +118,125 @@ func (f *federatingDB) activityBlock(ctx context.Context, asType vocab.Type, rec
CREATE HANDLERS 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) create, ok := asType.(vocab.ActivityStreamsCreate)
if !ok { 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 // Create must have an Object.
object := create.GetActivityStreamsObject() objectProp := create.GetActivityStreamsObject()
if object == nil { if objectProp == nil {
return errors.New("Create had no Object") return gtserror.New("create had no Object")
} }
errs := []string{} // Iterate through the Object property and process FIRST provided statusable.
// iterate through the object(s) to see what we're meant to be creating // todo: https://github.com/superseriousbusiness/gotosocial/issues/1905
for objectIter := object.Begin(); objectIter != object.End(); objectIter = objectIter.Next() { for iter := objectProp.Begin(); iter != objectProp.End(); iter = iter.Next() {
asObjectType := objectIter.GetType() object := iter.GetType()
if asObjectType == nil { if object == nil {
// currently we can't do anything with just a Create of something that's not an Object with a type // Can't do Create with Object that's just a URI.
// TODO: process a Create with an Object that's just a URI or something // Warn log this because it's an AP error.
errs = append(errs, "object of Create was not a Type") log.Warn(ctx, "object entry was not a type: %[1]T%[1]+v", iter)
continue continue
} }
// we have a type -- what is it? // Ensure given object type is a statusable.
asObjectTypeName := asObjectType.GetTypeName() statusable, ok := object.(ap.Statusable)
switch asObjectTypeName { if !ok {
case ap.ObjectNote: // Can't (currently) Create anything other than a Statusable. ([1] is a format arg index)
// CREATE A NOTE log.Debugf(ctx, "object entry type (currently) unsupported: %[1]T%[1]+v", object)
if err := f.createNote(ctx, objectIter.GetActivityStreamsNote(), receivingAccount, requestingAccount); err != nil { continue
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()))
} }
}
if len(errs) != 0 { // Handle creation of statusable.
return fmt.Errorf("activityCreate: one or more errors while processing activity: %s", strings.Join(errs, "; ")) return f.createStatusable(ctx,
statusable,
receivingAccount,
requestingAccount,
)
} }
return nil return nil
} }
// createNote handles a Create activity with a Note type. // createStatusable handles a Create activity for a Statusable.
func (f *federatingDB) createNote(ctx context.Context, note vocab.ActivityStreamsNote, receivingAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account) error { // This function won't insert anything in the database yet,
l := log.WithContext(ctx). // but will pass the Statusable (if appropriate) through to
WithFields(kv.Fields{ // the processor for further asynchronous processing.
{"receivingAccount", receivingAccount.URI}, func (f *federatingDB) createStatusable(
{"requestingAccount", requestingAccount.URI}, 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. // Statusable must have an ID.
// 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? 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 forward := true
for iter := attrToProp.Begin(); iter != attrToProp.End(); iter = iter.Next() {
// note should have an attributedTo actorURI, err := pub.ToId(iter)
noteAttributedTo := note.GetActivityStreamsAttributedTo() if err != nil {
if noteAttributedTo == nil { return gtserror.Newf("error extracting id from attributedTo entry: %w", err)
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
} }
iri := attributedToIter.GetIRI()
if requestingAccount.URI == iri.String() { if requestingAccount.URI == actorURI.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 // The actor who posted this statusable to our inbox is
// (one of) its creator(s), so this is not a forward.
forward = false 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 { if forward {
l.Trace("note is a forward") // Pass the statusable URI (APIri) into the processor worker
id := note.GetJSONLDId() // and do the rest of the processing asynchronously.
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
f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{ f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
APObjectType: ap.ObjectNote, APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate, APActivityType: ap.ActivityCreate,
APIri: id.GetIRI(), APIri: statusableURI,
APObjectModel: nil, APObjectModel: nil,
GTSModel: nil, GTSModel: nil,
ReceivingAccount: receivingAccount, ReceivingAccount: receivingAccount,
@ -209,34 +244,58 @@ func (f *federatingDB) createNote(ctx context.Context, note vocab.ActivityStream
return nil return nil
} }
// if we reach this point, we know it's not a forwarded status, so proceed with processing it as normal // 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, note) status, err = f.typeConverter.ASStatusToStatus(ctx, statusable)
if err != nil { 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 // Check whether we should accept this new status.
statusID, err := id.NewULIDFromTime(status.CreatedAt) 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 { if err != nil {
return err return err
} }
status.ID = statusID
// Put this newly parsed status in the database.
if err := f.state.DB.PutStatus(ctx, status); err != nil { if err := f.state.DB.PutStatus(ctx, status); err != nil {
if errors.Is(err, db.ErrAlreadyExists) { if errors.Is(err, db.ErrAlreadyExists) {
// the status already exists in the database, which means we've already handled everything else, // The status already exists in the database, which
// so we can just return nil here and be done with it. // 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 return nil
} }
// an actual error has happened return gtserror.Newf("db error inserting status: %w", err)
return fmt.Errorf("createNote: database error inserting status: %s", 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{ f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
APObjectType: ap.ObjectNote, APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate, APActivityType: ap.ActivityCreate,
APObjectModel: note, APIri: nil,
APObjectModel: statusable,
GTSModel: status, GTSModel: status,
ReceivingAccount: receivingAccount, ReceivingAccount: receivingAccount,
}) })
@ -244,6 +303,26 @@ func (f *federatingDB) createNote(ctx context.Context, note vocab.ActivityStream
return nil 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 FOLLOW HANDLERS
*/ */

View file

@ -54,7 +54,7 @@ func (suite *CreateTestSuite) TestCreateNote() {
// status should have some expected values // status should have some expected values
suite.Equal(requestingAccount.ID, status.AccountID) 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 // status should be in the database
_, err = suite.db.GetStatusByID(context.Background(), status.ID) _, 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. // Rewind body reader and content-length if set.
if rc, ok := r.Body.(*byteutil.ReadNopCloser); ok { if rc, ok := r.Body.(*byteutil.ReadNopCloser); ok {
rc.Rewind() // set len AFTER rewind
r.ContentLength = int64(rc.Len()) r.ContentLength = int64(rc.Len())
rc.Rewind()
} }
// Sign the outgoing request. // Sign the outgoing request.

View file

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

View file

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

View file

@ -23,7 +23,7 @@ import (
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/testrig" "github.com/superseriousbusiness/gotosocial/internal/util"
) )
type FollowTestSuite struct { 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') // 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{ relationship, err := suite.accountProcessor.FollowCreate(ctx, requestingAccount, &apimodel.AccountFollowRequest{
ID: targetAccount.ID, ID: targetAccount.ID,
Reblogs: testrig.FalseBool(), Reblogs: util.Ptr(false),
Notify: testrig.TrueBool(), Notify: util.Ptr(true),
}) })
if err != nil { if err != nil {
suite.FailNow(err.Error()) 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') // 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{ relationship, err := suite.accountProcessor.FollowCreate(ctx, requestingAccount, &apimodel.AccountFollowRequest{
ID: targetAccount.ID, ID: targetAccount.ID,
Notify: testrig.TrueBool(), Notify: util.Ptr(true),
}) })
if err != nil { if err != nil {
suite.FailNow(err.Error()) 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') // 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{ relationship, err := suite.accountProcessor.FollowCreate(ctx, requestingAccount, &apimodel.AccountFollowRequest{
ID: targetAccount.ID, ID: targetAccount.ID,
Notify: testrig.TrueBool(), Notify: util.Ptr(true),
Reblogs: testrig.TrueBool(), Reblogs: util.Ptr(true),
}) })
if err != nil { if err != nil {
suite.FailNow(err.Error()) suite.FailNow(err.Error())
} }
@ -104,10 +101,9 @@ func (suite *FollowTestSuite) TestUpdateExistingFollowChangeNothing() {
// Trace logs should show no update query. // Trace logs should show no update query.
relationship, err := suite.accountProcessor.FollowCreate(ctx, requestingAccount, &apimodel.AccountFollowRequest{ relationship, err := suite.accountProcessor.FollowCreate(ctx, requestingAccount, &apimodel.AccountFollowRequest{
ID: targetAccount.ID, ID: targetAccount.ID,
Notify: testrig.FalseBool(), Notify: util.Ptr(false),
Reblogs: testrig.TrueBool(), Reblogs: util.Ptr(true),
}) })
if err != nil { if err != nil {
suite.FailNow(err.Error()) suite.FailNow(err.Error())
} }
@ -126,7 +122,6 @@ func (suite *FollowTestSuite) TestUpdateExistingFollowSetNothing() {
relationship, err := suite.accountProcessor.FollowCreate(ctx, requestingAccount, &apimodel.AccountFollowRequest{ relationship, err := suite.accountProcessor.FollowCreate(ctx, requestingAccount, &apimodel.AccountFollowRequest{
ID: targetAccount.ID, ID: targetAccount.ID,
}) })
if err != nil { if err != nil {
suite.FailNow(err.Error()) 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 != nil {
if form.Source.Language != 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) return nil, gtserror.NewErrorBadRequest(err)
} }
account.Language = *form.Source.Language account.Language = language
} }
if form.Source.Sensitive != nil { 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) { func data(requestedPerson vocab.ActivityStreamsPerson) (interface{}, gtserror.WithCode) {
data, err := ap.Serialize(requestedPerson) 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 { if err != nil {
err := gtserror.Newf("error serializing person: %w", err) err := gtserror.Newf("error serializing person: %w", err)
return nil, gtserror.NewErrorInternalError(err) return nil, gtserror.NewErrorInternalError(err)

View file

@ -30,6 +30,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/messages" "github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/stream" "github.com/superseriousbusiness/gotosocial/internal/stream"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/testrig" "github.com/superseriousbusiness/gotosocial/testrig"
) )
@ -63,20 +64,20 @@ func (suite *FromClientAPITestSuite) TestProcessStreamNewStatus() {
EmojiIDs: []string{}, EmojiIDs: []string{},
CreatedAt: testrig.TimeMustParse("2021-10-20T11:36:45Z"), CreatedAt: testrig.TimeMustParse("2021-10-20T11:36:45Z"),
UpdatedAt: 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", AccountURI: "http://localhost:8080/users/admin",
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF", AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
InReplyToID: "", InReplyToID: "",
BoostOfID: "", BoostOfID: "",
ContentWarning: "", ContentWarning: "",
Visibility: gtsmodel.VisibilityFollowersOnly, Visibility: gtsmodel.VisibilityFollowersOnly,
Sensitive: testrig.FalseBool(), Sensitive: util.Ptr(false),
Language: "en", Language: "en",
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F", CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
Federated: testrig.FalseBool(), Federated: util.Ptr(false),
Boostable: testrig.TrueBool(), Boostable: util.Ptr(true),
Replyable: testrig.TrueBool(), Replyable: util.Ptr(true),
Likeable: testrig.TrueBool(), Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
} }
@ -189,7 +190,7 @@ func (suite *FromClientAPITestSuite) TestProcessNewStatusWithNotification() {
// that receiving account wants notifs when posting account posts. // that receiving account wants notifs when posting account posts.
follow := &gtsmodel.Follow{} follow := &gtsmodel.Follow{}
*follow = *suite.testFollows["local_account_1_admin_account"] *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 { if err := suite.db.UpdateFollow(ctx, follow); err != nil {
suite.FailNow(err.Error()) suite.FailNow(err.Error())
} }
@ -206,20 +207,20 @@ func (suite *FromClientAPITestSuite) TestProcessNewStatusWithNotification() {
EmojiIDs: []string{}, EmojiIDs: []string{},
CreatedAt: testrig.TimeMustParse("2021-10-20T11:36:45Z"), CreatedAt: testrig.TimeMustParse("2021-10-20T11:36:45Z"),
UpdatedAt: 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", AccountURI: "http://localhost:8080/users/admin",
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF", AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
InReplyToID: "", InReplyToID: "",
BoostOfID: "", BoostOfID: "",
ContentWarning: "", ContentWarning: "",
Visibility: gtsmodel.VisibilityFollowersOnly, Visibility: gtsmodel.VisibilityFollowersOnly,
Sensitive: testrig.FalseBool(), Sensitive: util.Ptr(false),
Language: "en", Language: "en",
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F", CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
Federated: testrig.FalseBool(), Federated: util.Ptr(false),
Boostable: testrig.TrueBool(), Boostable: util.Ptr(true),
Replyable: testrig.TrueBool(), Replyable: util.Ptr(true),
Likeable: testrig.TrueBool(), Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, 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. // processCreateStatusFromFederator handles Activity Create and Object Note.
func (p *Processor) processCreateStatusFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { 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 ( var (
status *gtsmodel.Status status *gtsmodel.Status
err error 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 { if forwarded {
// Model is set, use that. // Model was not set, deref with IRI.
status, err = p.statusFromGTSModel(ctx, federatorMsg) // This will also cause the status to be inserted into the db.
} else {
// Model is not set, use IRI.
status, err = p.statusFromAPIRI(ctx, federatorMsg) 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 { if err != nil {

View file

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

View file

@ -28,6 +28,7 @@ import (
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/testrig" "github.com/superseriousbusiness/gotosocial/testrig"
) )
@ -68,7 +69,7 @@ func (suite *GetFileTestSuite) TestGetRemoteFileUncached() {
// uncache the file from local // uncache the file from local
testAttachment := suite.testAttachments["remote_account_1_status_1_attachment_1"] 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") err := suite.db.UpdateByID(ctx, testAttachment, testAttachment.ID, "cached")
suite.NoError(err) suite.NoError(err)
err = suite.storage.Delete(ctx, testAttachment.File.Path) err = suite.storage.Delete(ctx, testAttachment.File.Path)
@ -120,7 +121,7 @@ func (suite *GetFileTestSuite) TestGetRemoteFileUncachedInterrupted() {
// uncache the file from local // uncache the file from local
testAttachment := suite.testAttachments["remote_account_1_status_1_attachment_1"] 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") err := suite.db.UpdateByID(ctx, testAttachment, testAttachment.ID, "cached")
suite.NoError(err) suite.NoError(err)
err = suite.storage.Delete(ctx, testAttachment.File.Path) err = suite.storage.Delete(ctx, testAttachment.File.Path)
@ -177,7 +178,7 @@ func (suite *GetFileTestSuite) TestGetRemoteFileThumbnailUncached() {
suite.NoError(err) suite.NoError(err)
// uncache the file from local // uncache the file from local
testAttachment.Cached = testrig.FalseBool() testAttachment.Cached = util.Ptr(false)
err = suite.db.UpdateByID(ctx, testAttachment, testAttachment.ID, "cached") err = suite.db.UpdateByID(ctx, testAttachment, testAttachment.ID, "cached")
suite.NoError(err) suite.NoError(err)
err = suite.storage.Delete(ctx, testAttachment.File.Path) 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. // 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) { 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) accountURIs := uris.GenerateURIsForAccount(account.Username)
thisStatusID := id.NewULID() thisStatusID := id.NewULID()
@ -55,7 +57,6 @@ func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, appli
ContentWarning: text.SanitizePlaintext(form.SpoilerText), ContentWarning: text.SanitizePlaintext(form.SpoilerText),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
Sensitive: &sensitive, Sensitive: &sensitive,
Language: form.Language,
CreatedWithApplicationID: application.ID, CreatedWithApplicationID: application.ID,
Text: form.Status, Text: form.Status,
} }

View file

@ -208,6 +208,40 @@ func (suite *StatusCreateTestSuite) TestProcessMediaDescriptionTooShort() {
suite.Nil(apiStatus) 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) { func TestStatusCreateTestSuite(t *testing.T) {
suite.Run(t, new(StatusCreateTestSuite)) suite.Run(t, new(StatusCreateTestSuite))
} }

View file

@ -21,8 +21,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"net/url"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
@ -31,6 +29,9 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/uris" "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) { 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. // Hashtags for later dereferencing.
if hashtags, err := ap.ExtractHashtags(statusable); err != nil { if hashtags, err := ap.ExtractHashtags(statusable); err != nil {
l.Infof("error extracting hashtags: %q", err) l.Warnf("error extracting hashtags: %v", err)
} else { } else {
status.Tags = hashtags status.Tags = hashtags
} }
@ -291,7 +292,7 @@ func (c *converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab
// //
// Custom emojis for later dereferencing. // Custom emojis for later dereferencing.
if emojis, err := ap.ExtractEmojis(statusable); err != nil { if emojis, err := ap.ExtractEmojis(statusable); err != nil {
l.Infof("error extracting emojis: %q", err) l.Warnf("error extracting emojis: %v", err)
} else { } else {
status.Emojis = emojis status.Emojis = emojis
} }
@ -300,7 +301,7 @@ func (c *converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab
// //
// Mentions of other accounts for later dereferencing. // Mentions of other accounts for later dereferencing.
if mentions, err := ap.ExtractMentions(statusable); err != nil { if mentions, err := ap.ExtractMentions(statusable); err != nil {
l.Infof("error extracting mentions: %q", err) l.Warnf("error extracting mentions: %v", err)
} else { } else {
status.Mentions = mentions 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. // db defaults, will fall back to now if not set.
published, err := ap.ExtractPublished(statusable) published, err := ap.ExtractPublished(statusable)
if err != nil { if err != nil {
l.Infof("error extracting published: %q", err) l.Warnf("error extracting published: %v", err)
} else { } else {
status.CreatedAt = published status.CreatedAt = published
status.UpdatedAt = 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 // TODO: a lot of work to be done here -- a new type
// needs to be created for this in go-fed/activity. // needs to be created for this in go-fed/activity.
// Until this is implemented, assume all true. // Until this is implemented, assume all true.
var trueBool = func() *bool { b := true; return &b } status.Federated = util.Ptr(true)
status.Federated = trueBool() status.Boostable = util.Ptr(true)
status.Boostable = trueBool() status.Replyable = util.Ptr(true)
status.Replyable = trueBool() status.Likeable = util.Ptr(true)
status.Likeable = trueBool()
// status.Sensitive // status.Sensitive
status.Sensitive = func() *bool { status.Sensitive = func() *bool {
@ -630,8 +630,8 @@ func (c *converter) ASAnnounceToStatus(ctx context.Context, announceable ap.Anno
// Extract published time for the boost. // Extract published time for the boost.
published, err := ap.ExtractPublished(announceable) published, err := ap.ExtractPublished(announceable)
if err != nil { if err != nil {
err = gtserror.Newf("error extracting published: %w", err) //err = gtserror.Newf("error extracting published: %w", err)
return nil, isNew, err published = time.Now().UTC()
} }
status.CreatedAt = published status.CreatedAt = published
status.UpdatedAt = 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 return err
} }
// Language checks that the given language string is a 2- or 3-letter ISO 639 code. // Language checks that the given language string is a valid, if not necessarily canonical, BCP 47 language tag.
// Returns an error if the language cannot be parsed. See: https://pkg.go.dev/golang.org/x/text/language // Returns a canonicalized version of the tag if the language can be parsed.
func Language(lang string) error { // 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 == "" { if lang == "" {
return errors.New("no language provided") return "", errors.New("no language provided")
} }
_, err := language.ParseBase(lang) parsed, err := language.Parse(lang)
return err if err != nil {
return "", err
}
return parsed.String(), err
} }
// SignUpReason checks that a sufficient reason is given for a server signup request // 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() { func (suite *ValidationTestSuite) TestValidateLanguage() {
empty := "" testCases := []struct {
notALanguage := "this isn't a language at all!" name, input, expected, err string
english := "en" }{
capitalEnglish := "EN" {name: "empty", err: "no language provided"},
arabic3Letters := "ara" {name: "notALanguage", input: "this isn't a language at all!", err: "language: tag is not well-formed"},
mixedCapsEnglish := "eN" {name: "english", input: "en", expected: "en"},
englishUS := "en-us" // Should be all lowercase
dutch := "nl" {name: "capitalEnglish", input: "EN", expected: "en"},
german := "de" // Overlong, should be in ISO 639-1 format
var err error {name: "arabic3Letters", input: "ara", expected: "ar"},
// Should be all lowercase
err = validate.Language(empty) {name: "mixedCapsEnglish", input: "eN", expected: "en"},
if suite.Error(err) { // Region should be capitalized
suite.Equal(errors.New("no language provided"), err) {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) for _, testCase := range testCases {
if suite.Error(err) { testCase := testCase
suite.Equal(errors.New("language: tag is not well-formed"), err) suite.Run(testCase.name, func() {
} actual, actualErr := validate.Language(testCase.input)
if testCase.err == "" {
err = validate.Language(english) suite.Equal(testCase.expected, actual)
if suite.NoError(err) { suite.NoError(actualErr)
suite.Equal(nil, err) } else {
} suite.Empty(actual)
suite.EqualError(actualErr, testCase.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)
} }
} }

View file

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

View file

@ -27,29 +27,43 @@ const (
robotsPath = "/robots.txt" robotsPath = "/robots.txt"
robotsMetaAllowSome = "nofollow, noarchive, nositelinkssearchbox, max-image-preview:standard" // https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag#robotsmeta 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 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: * User-agent: *
Crawl-delay: 500 Crawl-delay: 500
# api stuff
# API endpoints.
Disallow: /api/ Disallow: /api/
# auth/login stuff
# Auth/login endpoints.
Disallow: /auth/ Disallow: /auth/
Disallow: /oauth/ Disallow: /oauth/
Disallow: /check_your_email Disallow: /check_your_email
Disallow: /wait_for_approval Disallow: /wait_for_approval
Disallow: /account_disabled Disallow: /account_disabled
# well known stuff
# Well-known endpoints.
Disallow: /.well-known/ Disallow: /.well-known/
# files
# Fileserver/media.
Disallow: /fileserver/ Disallow: /fileserver/
# s2s AP stuff
# Fedi S2S API endpoints.
Disallow: /users/ Disallow: /users/
Disallow: /emoji/ Disallow: /emoji/
# panels
# Settings panels.
Disallow: /admin Disallow: /admin
Disallow: /user Disallow: /user
Disallow: /settings/ Disallow: /settings/
# domain blocklist
# Domain blocklist.
Disallow: /about/suspended` Disallow: /about/suspended`
) )

View file

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

View file

@ -2,6 +2,11 @@
set -e set -e
# Ensure test args are set.
ARGS=${@}; [ -z "$ARGS" ] && \
ARGS='./...'
# Run the SQLite tests.
GTS_DB_TYPE=sqlite \ GTS_DB_TYPE=sqlite \
GTS_DB_ADDRESS=':memory:' \ 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. 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 ## 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 mkdir Project
cd Project cd Project

View file

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

View file

@ -3,12 +3,12 @@ package html
type traits uint16 type traits uint16
const ( const (
normalTag traits = 1 << iota normalTag traits = 1 << iota
rawTag // raw tags need special processing for their content rawTag // raw tags need special processing for their content
nonPhrasingTag // non-phrasing elements are unaffected by whitespace, remove spaces around these tags blockTag // remove spaces around these tags
objectTag // content tags with a few exclusions, keep spaces after these open/close tags objectTag // keep spaces after these open/close tags
omitPTag // omit p end tag if it is followed by this start tag 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 keepPTag // keep p end tag if it is followed by this end tag
) )
const ( const (
@ -21,54 +21,54 @@ const (
var tagMap = map[Hash]traits{ var tagMap = map[Hash]traits{
A: keepPTag, A: keepPTag,
Abbr: normalTag, Abbr: normalTag,
Address: nonPhrasingTag | omitPTag, Address: blockTag | omitPTag,
Area: normalTag, Area: normalTag,
Article: nonPhrasingTag | omitPTag, Article: blockTag | omitPTag,
Aside: nonPhrasingTag | omitPTag, Aside: blockTag | omitPTag,
Audio: keepPTag, Audio: keepPTag,
B: normalTag, B: normalTag,
Base: normalTag, Base: normalTag,
Bb: normalTag, Bb: normalTag,
Bdi: normalTag, Bdi: normalTag,
Bdo: normalTag, Bdo: normalTag,
Blockquote: nonPhrasingTag | omitPTag, Blockquote: blockTag | omitPTag,
Body: nonPhrasingTag, Body: normalTag,
Br: nonPhrasingTag, Br: blockTag,
Button: objectTag, Button: objectTag,
Canvas: objectTag | keepPTag, Canvas: objectTag | keepPTag,
Caption: nonPhrasingTag, Caption: blockTag,
Cite: normalTag, Cite: normalTag,
Code: normalTag, Code: normalTag,
Col: nonPhrasingTag, Col: blockTag,
Colgroup: nonPhrasingTag, Colgroup: blockTag,
Data: normalTag, Data: normalTag,
Datalist: normalTag, Datalist: normalTag, // no text content
Dd: nonPhrasingTag, Dd: blockTag,
Del: keepPTag, Del: keepPTag,
Details: omitPTag, Details: blockTag | omitPTag,
Dfn: normalTag, Dfn: normalTag,
Dialog: normalTag, Dialog: normalTag,
Div: nonPhrasingTag | omitPTag, Div: blockTag | omitPTag,
Dl: nonPhrasingTag | omitPTag, Dl: blockTag | omitPTag,
Dt: nonPhrasingTag, Dt: blockTag,
Em: normalTag, Em: normalTag,
Embed: nonPhrasingTag, Embed: normalTag,
Fieldset: nonPhrasingTag | omitPTag, Fieldset: blockTag | omitPTag,
Figcaption: nonPhrasingTag | omitPTag, Figcaption: blockTag | omitPTag,
Figure: nonPhrasingTag | omitPTag, Figure: blockTag | omitPTag,
Footer: nonPhrasingTag | omitPTag, Footer: blockTag | omitPTag,
Form: nonPhrasingTag | omitPTag, Form: blockTag | omitPTag,
H1: nonPhrasingTag | omitPTag, H1: blockTag | omitPTag,
H2: nonPhrasingTag | omitPTag, H2: blockTag | omitPTag,
H3: nonPhrasingTag | omitPTag, H3: blockTag | omitPTag,
H4: nonPhrasingTag | omitPTag, H4: blockTag | omitPTag,
H5: nonPhrasingTag | omitPTag, H5: blockTag | omitPTag,
H6: nonPhrasingTag | omitPTag, H6: blockTag | omitPTag,
Head: nonPhrasingTag, Head: blockTag,
Header: nonPhrasingTag | omitPTag, Header: blockTag | omitPTag,
Hgroup: nonPhrasingTag, Hgroup: blockTag,
Hr: nonPhrasingTag | omitPTag, Hr: blockTag | omitPTag,
Html: nonPhrasingTag, Html: blockTag,
I: normalTag, I: normalTag,
Iframe: rawTag | objectTag, Iframe: rawTag | objectTag,
Img: objectTag, Img: objectTag,
@ -76,64 +76,90 @@ var tagMap = map[Hash]traits{
Ins: keepPTag, Ins: keepPTag,
Kbd: normalTag, Kbd: normalTag,
Label: normalTag, Label: normalTag,
Legend: normalTag, Legend: blockTag,
Li: nonPhrasingTag, Li: blockTag,
Link: normalTag, Link: normalTag,
Main: nonPhrasingTag | omitPTag, Main: blockTag | omitPTag,
Map: keepPTag, Map: keepPTag,
Mark: normalTag, Mark: normalTag,
Math: rawTag, Math: rawTag,
Menu: omitPTag, Menu: blockTag | omitPTag,
Meta: nonPhrasingTag, Meta: normalTag,
Meter: objectTag, Meter: objectTag,
Nav: nonPhrasingTag | omitPTag, Nav: blockTag | omitPTag,
Noscript: nonPhrasingTag | keepPTag, Noscript: blockTag | keepPTag,
Object: objectTag, Object: objectTag,
Ol: nonPhrasingTag | omitPTag, Ol: blockTag | omitPTag,
Optgroup: normalTag, Optgroup: normalTag, // no text content
Option: normalTag, Option: blockTag,
Output: nonPhrasingTag, Output: normalTag,
P: nonPhrasingTag | omitPTag, P: blockTag | omitPTag,
Param: normalTag, Param: normalTag,
Picture: normalTag, Picture: normalTag,
Pre: nonPhrasingTag | omitPTag, Pre: blockTag | omitPTag,
Progress: objectTag, Progress: objectTag,
Q: objectTag, Q: objectTag,
Rp: normalTag, Rp: normalTag,
Rt: normalTag, Rt: objectTag,
Ruby: normalTag, Ruby: normalTag,
S: normalTag, S: normalTag,
Samp: normalTag, Samp: normalTag,
Script: rawTag, Script: rawTag,
Section: nonPhrasingTag | omitPTag, Section: blockTag | omitPTag,
Select: objectTag, Select: objectTag,
Slot: normalTag, Slot: normalTag,
Small: normalTag, Small: normalTag,
Source: normalTag, Source: normalTag,
Span: normalTag, Span: normalTag,
Strong: normalTag, Strong: normalTag,
Style: rawTag | nonPhrasingTag, Style: rawTag | blockTag,
Sub: normalTag, Sub: normalTag,
Summary: normalTag, Summary: blockTag,
Sup: normalTag, Sup: normalTag,
Svg: rawTag | objectTag, Svg: rawTag | objectTag,
Table: nonPhrasingTag | omitPTag, Table: blockTag | omitPTag,
Tbody: nonPhrasingTag, Tbody: blockTag,
Td: nonPhrasingTag, Td: blockTag,
Template: normalTag, Template: normalTag,
Textarea: rawTag | objectTag, Textarea: rawTag | objectTag,
Tfoot: nonPhrasingTag, Tfoot: blockTag,
Th: nonPhrasingTag, Th: blockTag,
Thead: nonPhrasingTag, Thead: blockTag,
Time: normalTag, Time: normalTag,
Title: nonPhrasingTag, Title: normalTag,
Tr: nonPhrasingTag, Tr: blockTag,
Track: normalTag, Track: normalTag,
U: normalTag, U: normalTag,
Ul: nonPhrasingTag | omitPTag, Ul: blockTag | omitPTag,
Var: normalTag, Var: normalTag,
Video: objectTag | keepPTag, 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{ var attrMap = map[Hash]traits{
@ -574,7 +600,7 @@ var EntitiesMap = map[string][]byte{
"SupersetEqual": []byte("&supe;"), "SupersetEqual": []byte("&supe;"),
"Supset": []byte("&Sup;"), "Supset": []byte("&Sup;"),
"THORN": []byte("&#222;"), "THORN": []byte("&#222;"),
"Tab": []byte(" "), "Tab": []byte("\t"),
"Tcaron": []byte("&#356;"), "Tcaron": []byte("&#356;"),
"Tcedil": []byte("&#354;"), "Tcedil": []byte("&#354;"),
"Therefore": []byte("&#8756;"), "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) n := uint64(0)
for ; i < len(b); i++ { for ; i < len(b); i++ {
c := b[i] c := b[i]
if c >= '0' && c <= '9' { if '0' <= c && c <= '9' {
if trunk == -1 { if trunk == -1 {
if n > math.MaxUint64/10 { if math.MaxUint64/10 < n {
trunk = i trunk = i
} else { } else {
n *= 10 n *= 10
@ -62,7 +62,7 @@ func ParseFloat(b []byte) (float64, int) {
if i < len(b) && (b[i] == 'e' || b[i] == 'E') { if i < len(b) && (b[i] == 'e' || b[i] == 'E') {
startExp := i startExp := i
i++ i++
if e, expLen := ParseInt(b[i:]); expLen > 0 { if e, expLen := ParseInt(b[i:]); 0 < expLen {
expExp = e expExp = e
i += expLen i += expLen
} else { } else {
@ -74,17 +74,17 @@ func ParseFloat(b []byte) (float64, int) {
// copied from strconv/atof.go // copied from strconv/atof.go
if exp == 0 { if exp == 0 {
return f, i 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, // If exponent is big but number of digits is not,
// can move a few zeros into the integer part. // can move a few zeros into the integer part.
if exp > 22 { if 22 < exp {
f *= float64pow10[exp-22] f *= float64pow10[exp-22]
exp = 22 exp = 22
} }
if f <= 1e15 && f >= -1e15 { if -1e15 <= f && f <= 1e15 {
return f * float64pow10[exp], i 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 return f / float64pow10[-exp], i
} }
f *= math.Pow10(int(-mantExp)) 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 // expLen is zero for positive exponents, because positive exponents are determined later on in the big conversion loop
exp := 0 exp := 0
expLen := 0 expLen := 0
if mantExp > 0 { if 0 < mantExp {
// positive exponent is determined in the loop below // 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, // 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 // 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 { if neg {
maxLen++ maxLen++
} }
if i+maxLen > cap(b) { if cap(b) < i+maxLen {
b = append(b, make([]byte, maxLen)...) b = append(b, make([]byte, maxLen)...)
} else { } else {
b = b[:i+maxLen] 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 last := i + mantLen // right-most position of digit that is non-zero + dot
dot := last - prec - exp // position of dot dot := last - prec - exp // position of dot
j := last j := last
for mant > 0 { for 0 < mant {
if j == dot { if j == dot {
b[j] = '.' b[j] = '.'
j-- j--
} }
newMant := mant / 10 newMant := mant / 10
digit := mant - 10*newMant 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 // 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) // otherwise trim to the dot (including the dot)
if j > dot { if dot < j {
i = j + 1 i = j + 1
// decrease negative exponent further to get rid of dot // decrease negative exponent further to get rid of dot
if exp < 0 { if exp < 0 {
@ -209,9 +209,9 @@ func AppendFloat(b []byte, f float64, prec int) ([]byte, bool) {
mant = newMant mant = newMant
} }
if j > dot { if dot < j {
// extra zeros behind the dot // extra zeros behind the dot
for j > dot { for dot < j {
b[j] = '0' b[j] = '0'
j-- j--
} }
@ -244,7 +244,7 @@ func AppendFloat(b []byte, f float64, prec int) ([]byte, bool) {
} }
i += LenInt(int64(exp)) i += LenInt(int64(exp))
j := i j := i
for exp > 0 { for 0 < exp {
newExp := exp / 10 newExp := exp / 10
digit := exp - 10*newExp digit := exp - 10*newExp
j-- j--

View file

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

View file

@ -194,9 +194,8 @@ func render1(w writer, n *Node) error {
} }
} }
// Render any child nodes. // Render any child nodes
switch n.Data { if childTextNodesAreLiteral(n) {
case "iframe", "noembed", "noframes", "noscript", "plaintext", "script", "style", "xmp":
for c := n.FirstChild; c != nil; c = c.NextSibling { for c := n.FirstChild; c != nil; c = c.NextSibling {
if c.Type == TextNode { if c.Type == TextNode {
if _, err := w.WriteString(c.Data); err != nil { 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. // last element in the file, with no closing tag.
return plaintextAbort return plaintextAbort
} }
default: } else {
for c := n.FirstChild; c != nil; c = c.NextSibling { for c := n.FirstChild; c != nil; c = c.NextSibling {
if err := render1(w, c); err != nil { if err := render1(w, c); err != nil {
return err return err
@ -231,6 +230,27 @@ func render1(w writer, n *Node) error {
return w.WriteByte('>') 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 // 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. // quotes, but if s contains a double quote, it will use single quotes.
// It is used for writing the identifiers in a doctype declaration. // It is used for writing the identifiers in a doctype declaration.

View file

@ -19,6 +19,7 @@ import (
"io/fs" "io/fs"
"log" "log"
"math" "math"
"math/bits"
mathrand "math/rand" mathrand "math/rand"
"net" "net"
"net/http" "net/http"
@ -518,11 +519,14 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
func authorityAddr(scheme string, authority string) (addr string) { func authorityAddr(scheme string, authority string) (addr string) {
host, port, err := net.SplitHostPort(authority) host, port, err := net.SplitHostPort(authority)
if err != nil { // authority didn't have a port if err != nil { // authority didn't have a port
host = authority
port = ""
}
if port == "" { // authority's port was empty
port = "443" port = "443"
if scheme == "http" { if scheme == "http" {
port = "80" port = "80"
} }
host = authority
} }
if a, err := idna.ToASCII(host); err == nil { if a, err := idna.ToASCII(host); err == nil {
host = a host = a
@ -1677,7 +1681,27 @@ func (cs *clientStream) frameScratchBufferLen(maxFrameSize int) int {
return int(n) // doesn't truncate; max is 512K 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) { func (cs *clientStream) writeRequestBody(req *http.Request) (err error) {
cc := cs.cc cc := cs.cc
@ -1695,12 +1719,13 @@ func (cs *clientStream) writeRequestBody(req *http.Request) (err error) {
// Scratch buffer for reading into & writing from. // Scratch buffer for reading into & writing from.
scratchLen := cs.frameScratchBufferLen(maxFrameSize) scratchLen := cs.frameScratchBufferLen(maxFrameSize)
var buf []byte var buf []byte
if bp, ok := bufPool.Get().(*[]byte); ok && len(*bp) >= scratchLen { index := bufPoolIndex(scratchLen)
defer bufPool.Put(bp) if bp, ok := bufPools[index].Get().(*[]byte); ok && len(*bp) >= scratchLen {
defer bufPools[index].Put(bp)
buf = *bp buf = *bp
} else { } else {
buf = make([]byte, scratchLen) buf = make([]byte, scratchLen)
defer bufPool.Put(&buf) defer bufPools[index].Put(&buf)
} }
var sawEOF bool var sawEOF bool

View file

@ -3,7 +3,6 @@
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build appengine //go:build appengine
// +build appengine
package internal 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/manage
github.com/superseriousbusiness/oauth2/v4/models github.com/superseriousbusiness/oauth2/v4/models
github.com/superseriousbusiness/oauth2/v4/server github.com/superseriousbusiness/oauth2/v4/server
# github.com/tdewolff/minify/v2 v2.12.7 # github.com/tdewolff/minify/v2 v2.12.8
## explicit; go 1.13 ## explicit; go 1.18
github.com/tdewolff/minify/v2 github.com/tdewolff/minify/v2
github.com/tdewolff/minify/v2/html 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 ## explicit; go 1.13
github.com/tdewolff/parse/v2 github.com/tdewolff/parse/v2
github.com/tdewolff/parse/v2/buffer github.com/tdewolff/parse/v2/buffer
@ -852,7 +852,7 @@ golang.org/x/image/webp
# golang.org/x/mod v0.10.0 # golang.org/x/mod v0.10.0
## explicit; go 1.17 ## explicit; go 1.17
golang.org/x/mod/semver golang.org/x/mod/semver
# golang.org/x/net v0.12.0 # golang.org/x/net v0.14.0
## explicit; go 1.17 ## explicit; go 1.17
golang.org/x/net/bpf golang.org/x/net/bpf
golang.org/x/net/context golang.org/x/net/context
@ -870,8 +870,8 @@ golang.org/x/net/ipv4
golang.org/x/net/ipv6 golang.org/x/net/ipv6
golang.org/x/net/publicsuffix golang.org/x/net/publicsuffix
golang.org/x/net/trace golang.org/x/net/trace
# golang.org/x/oauth2 v0.10.0 # golang.org/x/oauth2 v0.11.0
## explicit; go 1.17 ## explicit; go 1.18
golang.org/x/oauth2 golang.org/x/oauth2
golang.org/x/oauth2/internal golang.org/x/oauth2/internal
# golang.org/x/sys v0.11.0 # 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> <a href="https://tusky.app" target="_blank" rel="noopener">Get Tusky</a>
</div> </div>
</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"> <div class="entry">
<img class="logo" src="/assets/mastodon.svg" alt="The Mastodon logo, the character M in a speech bubble"> <img class="logo" src="/assets/mastodon.svg" alt="The Mastodon logo, the character M in a speech bubble">
<div> <div>