Compare commits

..

1 Commits

Author SHA1 Message Date
Jake Mason 9037799222 Prepare URLs for the VisJam2023 feed 2023-04-02 14:05:24 -04:00
482 changed files with 23046 additions and 23023 deletions

1
.gitignore vendored
View File

@ -16,7 +16,6 @@ local/backups
/tmp
*.exe
.DS_Store
__debug_bin*
# vim session saves
Session.vim

View File

@ -10,7 +10,7 @@ We want the website to be a great example of Handmade software on the web. We en
You will need the following software installed:
- Go 1.21 or newer: https://go.dev/
- Go 1.18 or higher: https://go.dev/
You can download Go directly from the website, or install it through major package managers. If you already have Go installed, but are unsure of the version, you can check by running `go version`.

View File

@ -4,7 +4,6 @@ const RecvAddress = "admin@example.com"
const RecvName = "Admin"
const FromName = "From Name"
const FromAddress = "from@address.com"
const ServerUsername = "username"
const ServerPassword = "password"
const FromAddressPassword = "password"
const ServerAddress = "server.address"
const ServerPort = 587

View File

@ -46,7 +46,7 @@ func sendMail(toAddress, toName, subject, contentHtml string) error {
)
return smtp.SendMail(
fmt.Sprintf("%s:%d", ServerAddress, ServerPort),
smtp.PlainAuth("", ServerUsername, ServerPassword, ServerAddress),
smtp.PlainAuth("", FromAddress, FromAddressPassword, ServerAddress),
FromAddress,
[]string{toAddress},
contents,

View File

@ -1,4 +1,4 @@
- [ ] Export with [DiscordChatExporter](https://github.com/Tyrrrz/DiscordChatExporter) CLI 2.39.1
- [ ] Export with [DiscordChatExporter](https://github.com/Tyrrrz/DiscordChatExporter) CLI 2.34
```
DiscordChatExporter.Cli.exe export -c [thread-id] -t [token] -o [fishbowl].html --media
@ -49,7 +49,7 @@
go run twemoji.go [fishbowl]-dragged.html files [fishbowl]-twemojied.html
```
- [ ] Fix timestamps, validate they look correct
- [ ] Fix timestamps
```
go run timestamps.go [fishbowl]-twemojied.html [fishbowl]-timestamped.html
@ -59,7 +59,7 @@
- [ ] Create fishbowl folder under `hmn/src/templates/src/fishbowls/`
- [ ] Copy timestamped html and files, rename html
- [ ] Remove everything from html but chatlog
- [ ] Remove js, css and ggsans from files
- [ ] Remove js, css and whitney from files
- [ ] Add content path to `fishbowl.go`
- [ ] Test locally
- [ ] Submit a pull request

View File

@ -4,13 +4,11 @@ import (
"fmt"
"os"
"regexp"
"strconv"
"time"
)
func main() {
if len(os.Args) != 3 {
fmt.Println("Usage: go run timestamps.go <fishbowl>.html <fishbowl>-timestamped.html")
fmt.Println("Usage: go run timestamps.go [fishbowl].html [fishbowl]-timestamped.html")
os.Exit(1)
}
@ -24,19 +22,17 @@ func main() {
html := string(htmlBytes)
regex := regexp.MustCompile(
`(<span class="?chatlog__timestamp"?><a href=[^>]+>)(\d+)/(\d+)/(\d+)( [^<]+</a></span>)`,
regex, err := regexp.Compile(
"(<span class=\"chatlog__timestamp\">)(\\d+)-([A-Za-z]+)-(\\d+)( [^<]+</span>)",
)
htmlOut := regex.ReplaceAllStringFunc(html, func(s string) string {
match := regex.FindStringSubmatch(s)
month, err := strconv.ParseInt(match[2], 10, 64)
if err != nil {
panic(err)
}
monthStr := time.Month(month).String()
return fmt.Sprintf("%s%s %s, %s%s", match[1], monthStr, match[3], match[4], match[5])
})
htmlOut := regex.ReplaceAllString(
html,
"$1$3 $2, 20$4$5",
)
err = os.WriteFile(htmlOutPath, []byte(htmlOut), 0666)
if err != nil {

17
go.mod
View File

@ -1,6 +1,6 @@
module git.handmade.network/hmn/hmn
go 1.21
go 1.18
require (
github.com/HandmadeNetwork/golorem v0.0.0-20220507185207-414965a3a817
@ -11,7 +11,6 @@ require (
github.com/aws/aws-sdk-go-v2/credentials v1.3.3
github.com/aws/aws-sdk-go-v2/service/s3 v1.13.0
github.com/aws/smithy-go v1.7.0
github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f
github.com/frustra/bbcode v0.0.0-20201127003707-6ef347fbe1c8
github.com/go-stack/stack v1.8.0
github.com/google/uuid v1.2.0
@ -24,6 +23,7 @@ require (
github.com/stretchr/testify v1.8.1
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300
github.com/teacat/noire v1.1.0
github.com/wellington/go-libsass v0.9.2
github.com/yuin/goldmark v1.4.13
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01
golang.org/x/crypto v0.6.0
@ -44,12 +44,17 @@ require (
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/evanw/esbuild v0.21.4
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgtype v1.14.0 // indirect
github.com/jackc/pgx/v4 v4.18.1 // indirect
github.com/jackc/puddle/v2 v2.1.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
@ -57,12 +62,12 @@ require (
github.com/mitchellh/reflectwalk v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/teambition/rrule-go v1.7.2 // indirect
go.uber.org/atomic v1.10.0 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8
golang.org/x/sync v0.7.0 // indirect
golang.org/x/net v0.6.0 // indirect
golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

159
go.sum
View File

@ -15,12 +15,15 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/HandmadeNetwork/bbcode v0.0.0-20210623031351-ec0e2e2e39d9 h1:5WhEr56CD1uWDPcDIIa+UtNPPlJCoNJ3u38Rk+4XIks=
github.com/HandmadeNetwork/bbcode v0.0.0-20210623031351-ec0e2e2e39d9/go.mod h1:vMiNHD8absjmnO60Do5KCaJBwdbaiI/AzhMmSipMme4=
github.com/HandmadeNetwork/goldmark v1.4.1-0.20210707024600-f7e596e26b5e h1:z0GlF2OMmy852mrcMVpjZIzEHYCbUweS8RaWRCPfL1g=
github.com/HandmadeNetwork/goldmark v1.4.1-0.20210707024600-f7e596e26b5e/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/HandmadeNetwork/golorem v0.0.0-20220507185207-414965a3a817 h1:cBqVP/sLiK7DPay7Aac1PRUwu3fCVyL5Wc+xLXzqwkE=
github.com/HandmadeNetwork/golorem v0.0.0-20220507185207-414965a3a817/go.mod h1:doKbGBIdiM1nkEfvAeP5hvUmERah9H6StTVfCverqdE=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
@ -70,13 +73,16 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -88,20 +94,19 @@ github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f h1:feGUUxxvOtWVOhTko8Cbmp33a+tU0IMZxMEmnkoAISQ=
github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f/go.mod h1:2MKFUgfNMULRxqZkadG1Vh44we3y5gJAtTBlVsx1BKQ=
github.com/evanw/esbuild v0.21.4 h1:pe4SEQMoR1maEjhgWPEPWmUy11Jp6nidxd1mOvMrFFU=
github.com/evanw/esbuild v0.21.4/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@ -117,9 +122,8 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@ -160,14 +164,60 @@ github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.14.0 h1:vrbA9Ud87g6JdFWkHTJXppVce58qPIdP7N8y0Ml/A7Q=
github.com/jackc/pgconn v1.14.0/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0=
github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw=
github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgx-zerolog v0.0.0-20220923130014-7856b90a65ae h1:s/r/bBI1EUCZvGtoJ/Ow6WPUYe08V9VpQY3fv6eq99s=
github.com/jackc/pgx-zerolog v0.0.0-20220923130014-7856b90a65ae/go.mod h1:CRUuPsmIajLt3dZIlJ5+O8IDSib6y8yrst8DkCthTa4=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0=
github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE=
github.com/jackc/pgx/v5 v5.2.0 h1:NdPpngX0Y6z6XDFKqmFQaE+bCtkqzvQIOt1wvBlAqs8=
github.com/jackc/pgx/v5 v5.2.0/go.mod h1:Ptn7zmohNsWEsdxRawMzk3gaKma2obW+NWTnKa0S4nk=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle/v2 v2.1.2 h1:0f7vaaXINONKTsxYDn4otOAiJanX/BMeAtY//BXqzlg=
github.com/jackc/puddle/v2 v2.1.2/go.mod h1:2lpufsF5mRHO6SuZkm0fNYxM6SWHfvyFj62KwNzgels=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
@ -182,21 +232,29 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
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.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
@ -241,16 +299,24 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
@ -266,13 +332,17 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
@ -281,28 +351,47 @@ github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300 h1:XQdibLKagjdevRB6vA
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300/go.mod h1:FNa/dfN95vAYCNFrIKRrlRo+MBLbwmR9Asa5f2ljmBI=
github.com/teacat/noire v1.1.0 h1:5IgJ1H8jodiSSYnrVadV2JjbAnEgCCjYUQxSUuaQ7Sg=
github.com/teacat/noire v1.1.0/go.mod h1:cetGlnqr+9yKJcFgRgYXOWJY66XIrrjUsGBwNlNNtAk=
github.com/teambition/rrule-go v1.7.2 h1:goEajFWYydfCgavn2m/3w5U+1b3PGqPUHx/fFSVfTy0=
github.com/teambition/rrule-go v1.7.2/go.mod h1:mBJ1Ht5uboJ6jexKdNUJg2NcwP8uUMNvStWXlJD3MvU=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/wellington/go-libsass v0.9.2 h1:6Ims04UDdBs6/CGSVK5JC8FNikR5ssrsMMKE/uaO5Q8=
github.com/wellington/go-libsass v0.9.2/go.mod h1:mxgxgam0N0E+NAUMHLcu20Ccfc3mVpDkyrLDayqfiTs=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.3.6/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01 h1:0SJnXjE4jDClMW6grE0xpNhwpqbPwkBTn8zpVw5C0SI=
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01/go.mod h1:TwKQPa5XkCCRC2GRZ5wtfNUTQ2+9/i19mGRijFeJ4BE=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 h1:a5Yg6ylndHHYJqIPrdq0AhvR6KTvDTAvgBtaidhEevY=
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -310,8 +399,6 @@ golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
@ -327,6 +414,8 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -341,6 +430,13 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -350,8 +446,9 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 h1:ZrnxWX62AgTKOSagEqxvb3ffipvEDX2pl7E1TdqLqIc=
golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -360,25 +457,44 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RAc5XyaeamM+0VHRd4lc=
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -392,16 +508,28 @@ golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@ -427,12 +555,13 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -1,20 +0,0 @@
#!/usr/bin/env python3
import boto3
# You must already have configured your "AWS" (DigitalOcean) credentials via the AWS CLI.
s3 = boto3.resource("s3")
bucket = s3.Bucket("hmn-backup")
for obj in bucket.objects.filter(Prefix="db"):
print(obj.key)
print()
print("Above is a list of all the available database backups.")
print("Enter the name of the one you would like to download (e.g. \"hmn_pg_dump_live_2023-09-24\"):")
filename = input()
s3 = boto3.client("s3")
s3.download_file("hmn-backup", f"db/{filename}", f"local/backups/{filename}")
print(f"Downloaded {filename} to local/backups.")

15
local/download_database.sh Executable file
View File

@ -0,0 +1,15 @@
#!/bin/bash
set -euo pipefail
s3cmd ls s3://hmn-backup/db/
echo ""
echo "Above is a list of all the available database backups."
echo "Enter the name of the one you would like to download (e.g. \"hmn_pg_dump_live_2021-09-01\"):"
read filename
s3cmd get --force s3://hmn-backup/db/$filename ./local/backups/$filename
echo ""
echo "Downloaded $filename to local/backups."

View File

@ -1 +0,0 @@
boto3

View File

@ -3,7 +3,7 @@ package main
import (
_ "git.handmade.network/hmn/hmn/src/admintools"
_ "git.handmade.network/hmn/hmn/src/assets"
_ "git.handmade.network/hmn/hmn/src/buildcss/cmd"
_ "git.handmade.network/hmn/hmn/src/buildscss"
_ "git.handmade.network/hmn/hmn/src/discord/cmd"
_ "git.handmade.network/hmn/hmn/src/initimage"
_ "git.handmade.network/hmn/hmn/src/migration"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 256 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.402516,0,0,0.410256,60.3774,109.538)">
<rect x="-150" y="-267" width="636" height="117" style="fill:rgb(88,101,242);"/>
</g>
<g transform="matrix(0.266745,0,0,0.266745,60.1574,11.1482)">
<g id="图层_2">
<g id="Discord_Logos">
<g id="Discord_Logo_-_Large_-_White">
<path d="M170.85,20.2L198.15,20.2C204.73,20.2 210.297,21.227 214.85,23.28C219.042,25.042 222.602,28.034 225.06,31.86C227.361,35.639 228.54,39.996 228.46,44.42C228.511,48.864 227.285,53.231 224.93,57C222.303,60.982 218.545,64.088 214.14,65.92C209.313,68.12 203.33,69.217 196.19,69.21L170.85,69.21L170.85,20.2ZM195.91,56.74C200.343,56.74 203.75,55.633 206.13,53.42C208.57,51.054 209.873,47.745 209.7,44.35C209.85,41.202 208.699,38.127 206.52,35.85C204.387,33.73 201.177,32.667 196.89,32.66L188.35,32.66L188.35,56.74L195.91,56.74Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M269.34,69.13C265.738,68.228 262.293,66.788 259.12,64.86L259.12,53.24C261.89,55.255 265.009,56.74 268.32,57.62C271.942,58.725 275.704,59.301 279.49,59.33C280.797,59.4 282.102,59.174 283.31,58.67C284.17,58.23 284.6,57.67 284.6,57.09C284.619,56.435 284.365,55.801 283.9,55.34C283.101,54.726 282.164,54.317 281.17,54.15L272.77,52.26C267.957,51.14 264.54,49.59 262.52,47.61C260.471,45.543 259.381,42.707 259.52,39.8C259.486,37.194 260.437,34.668 262.18,32.73C264.248,30.543 266.855,28.939 269.74,28.08C273.452,26.913 277.329,26.356 281.22,26.43C284.852,26.396 288.474,26.819 292,27.69C294.848,28.365 297.583,29.449 300.12,30.91L300.12,41.91C297.741,40.53 295.184,39.483 292.52,38.8C289.634,38.026 286.658,37.636 283.67,37.64C279.283,37.64 277.09,38.387 277.09,39.88C277.075,40.559 277.47,41.183 278.09,41.46C279.289,41.97 280.544,42.332 281.83,42.54L288.83,43.8C293.377,44.6 296.767,46 299,48C301.233,50 302.353,52.927 302.36,56.78C302.432,60.898 300.308,64.758 296.79,66.9C293.103,69.373 287.84,70.607 281,70.6C277.067,70.606 273.149,70.112 269.34,69.13Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M318.9,67.66C315.158,65.941 312.003,63.158 309.83,59.66C307.803,56.23 306.764,52.304 306.83,48.32C306.77,44.338 307.864,40.423 309.98,37.05C312.235,33.607 315.447,30.896 319.22,29.25C323.807,27.251 328.778,26.282 333.78,26.41C340.78,26.41 346.59,27.88 351.21,30.82L351.21,43.65C349.452,42.468 347.532,41.549 345.51,40.92C343.246,40.205 340.884,39.851 338.51,39.87C334.17,39.87 330.773,40.663 328.32,42.25C326.06,43.511 324.655,45.905 324.655,48.493C324.655,51.041 326.016,53.403 328.22,54.68C330.6,56.287 334.053,57.09 338.58,57.09C340.916,57.096 343.241,56.759 345.48,56.09C347.522,55.504 349.484,54.668 351.32,53.6L351.32,66C345.91,69.154 339.731,70.754 333.47,70.62C328.451,70.757 323.467,69.744 318.9,67.66Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M368.64,67.66C364.856,65.932 361.645,63.155 359.39,59.66C357.265,56.238 356.161,52.278 356.21,48.25C356.142,44.27 357.248,40.356 359.39,37C361.657,33.594 364.854,30.907 368.6,29.26C377.799,25.528 388.101,25.528 397.3,29.26C401.031,30.894 404.215,33.568 406.47,36.96C408.597,40.328 409.693,44.247 409.62,48.23C409.67,52.254 408.577,56.211 406.47,59.64C404.239,63.138 401.044,65.917 397.27,67.64C388.133,71.558 377.777,71.558 368.64,67.64L368.64,67.66ZM389.91,55.24C391.657,53.436 392.584,50.989 392.47,48.48C392.592,45.994 391.662,43.568 389.91,41.8C388.01,40.058 385.483,39.159 382.91,39.31C380.339,39.175 377.818,40.072 375.91,41.8C374.164,43.571 373.239,45.996 373.36,48.48C373.247,50.987 374.169,53.433 375.91,55.24C377.8,57.004 380.328,57.925 382.91,57.79C385.494,57.94 388.028,57.017 389.91,55.24Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M451.69,29L451.69,44.14C449.6,42.893 447.191,42.285 444.76,42.39C441.03,42.39 438.15,43.53 436.15,45.79C434.15,48.05 433.15,51.56 433.15,56.32L433.15,69.2L416,69.2L416,28.25L432.8,28.25L432.8,41.25C433.733,36.49 435.24,32.98 437.32,30.72C439.36,28.472 442.286,27.228 445.32,27.32C447.56,27.259 449.771,27.842 451.69,29Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M508.67,18.8L508.67,69.2L491.52,69.2L491.52,60C490.227,63.275 487.903,66.041 484.9,67.88C481.585,69.769 477.814,70.71 474,70.6C470.411,70.684 466.878,69.698 463.85,67.77C460.918,65.865 458.581,63.172 457.11,60C455.504,56.499 454.705,52.681 454.77,48.83C454.657,44.837 455.508,40.875 457.25,37.28C458.851,33.988 461.351,31.214 464.46,29.28C467.656,27.344 471.334,26.349 475.07,26.41C483.23,26.41 488.713,29.957 491.52,37.05L491.52,18.8L508.67,18.8ZM489,55C490.766,53.261 491.722,50.857 491.63,48.38C491.705,45.976 490.747,43.652 489,42C484.971,38.719 479.139,38.719 475.11,42C473.368,43.69 472.425,46.045 472.52,48.47C472.431,50.918 473.384,53.292 475.14,55C476.995,56.725 479.47,57.63 482,57.51C484.575,57.652 487.102,56.746 489,55Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M107.7,8.07C99.347,4.246 90.528,1.533 81.47,0C80.23,2.216 79.109,4.496 78.11,6.83C68.461,5.376 58.649,5.376 49,6.83C48.001,4.496 46.879,2.216 45.64,-0C36.576,1.546 27.752,4.265 19.39,8.09C2.79,32.65 -1.71,56.6 0.54,80.21C10.261,87.392 21.143,92.855 32.71,96.36C35.315,92.857 37.62,89.14 39.6,85.25C35.838,83.845 32.208,82.112 28.75,80.07C29.66,79.41 30.55,78.73 31.41,78.07C51.768,87.644 75.372,87.644 95.73,78.07C96.6,78.78 97.49,79.46 98.39,80.07C94.926,82.115 91.288,83.852 87.52,85.26C89.498,89.149 91.803,92.862 94.41,96.36C105.987,92.869 116.877,87.409 126.6,80.22C129.24,52.84 122.09,29.11 107.7,8.07ZM42.45,65.69C36.18,65.69 31,60 31,53C31,46 36,40.26 42.43,40.26C48.86,40.26 54,46 53.89,53C53.78,60 48.84,65.69 42.45,65.69ZM84.69,65.69C78.41,65.69 73.25,60 73.25,53C73.25,46 78.25,40.26 84.69,40.26C91.13,40.26 96.23,46 96.12,53C96.01,60 91.08,65.69 84.69,65.69Z" style="fill:white;fill-rule:nonzero;"/>
<ellipse cx="242.92" cy="24.93" rx="8.55" ry="7.68" style="fill:white;"/>
<path d="M234.36,37.9C239.829,40.199 246.001,40.199 251.47,37.9L251.47,69.42L234.36,69.42L234.36,37.9Z" style="fill:white;fill-rule:nonzero;"/>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -94,7 +94,7 @@
text-align: center;
}
.fishbowl .chatlog__reference-symbol, .fishbowl .chatlog__reply-symbol {
.fishbowl .chatlog__reference-symbol {
height: 10px;
margin: 6px 4px 4px 36px;
border-left: 2px solid #4f545c;
@ -122,7 +122,7 @@
min-width: 0;
}
.fishbowl .chatlog__reference, .fishbowl .chatlog__reply {
.fishbowl .chatlog__reference {
display: flex;
margin-bottom: 0.15rem;
align-items: center;
@ -133,53 +133,53 @@
text-overflow: ellipsis;
}
.fishbowl .chatlog__reference-avatar, .fishbowl .chatlog__reply-avatar {
.fishbowl .chatlog__reference-avatar {
width: 16px;
height: 16px;
margin-right: 0.25rem;
border-radius: 50%;
}
.fishbowl .chatlog__reference-author, .fishbowl .chatlog__reply-author {
.fishbowl .chatlog__reference-author {
margin-right: 0.3rem;
font-weight: 600;
}
.fishbowl .chatlog__reference-content, .fishbowl .chatlog__reply-content {
.fishbowl .chatlog__reference-content {
overflow: hidden;
text-overflow: ellipsis;
}
.fishbowl .chatlog__reference-link, .fishbowl .chatlog__reply-link {
.fishbowl .chatlog__reference-link {
cursor: pointer;
}
.fishbowl .chatlog__reference-link *, .fishbowl .chatlog__reply-link * {
.fishbowl .chatlog__reference-link * {
display: inline;
pointer-events: none;
}
.fishbowl .chatlog__reference-link .hljs, .fishbowl .chatlog__reply-link .hljs {
.fishbowl .chatlog__reference-link .hljs {
display: inline;
}
.fishbowl .chatlog__reference-link .chatlog__markdown-quote, .fishbowl .chatlog__reply-link .chatlog__markdown-quote {
.fishbowl .chatlog__reference-link .chatlog__markdown-quote {
display: inline
}
.fishbowl .chatlog__reference-link .chatlog__markdown-pre, .fishbowl .chatlog__reply-link .chatlog__markdown-pre {
.fishbowl .chatlog__reference-link .chatlog__markdown-pre {
display: inline
}
.fishbowl .chatlog__reference-link:hover, .fishbowl .chatlog__reply-link:hover {
.fishbowl .chatlog__reference-link:hover {
color: #ffffff;
}
.fishbowl .chatlog__reference-link:hover *:not(.chatlog__markdown-spoiler), .fishbowl .chatlog__reply-link:hover *:not(.chatlog__markdown-spoiler) {
.fishbowl .chatlog__reference-link:hover *:not(.chatlog__markdown-spoiler) {
color: inherit;
}
.fishbowl .chatlog__reference-edited-timestamp, .fishbowl .chatlog__reply-edited-timestamp {
.fishbowl .chatlog__reference-edited-timestamp {
margin-left: 0.25rem;
color: #a3a6aa;
font-size: 0.75rem;

View File

@ -95,7 +95,7 @@
text-align: center;
}
.fishbowl .chatlog__reference-symbol, .fishbowl .chatlog__reply-symbol {
.fishbowl .chatlog__reference-symbol {
height: 10px;
margin: 6px 4px 4px 36px;
border-left: 2px solid #c7ccd1;
@ -123,7 +123,7 @@
min-width: 0;
}
.fishbowl .chatlog__reference, .fishbowl .chatlog__reply {
.fishbowl .chatlog__reference {
display: flex;
margin-bottom: 0.15rem;
align-items: center;
@ -134,53 +134,53 @@
text-overflow: ellipsis;
}
.fishbowl .chatlog__reference-avatar, .fishbowl .chatlog__reply-avatar {
.fishbowl .chatlog__reference-avatar {
width: 16px;
height: 16px;
margin-right: 0.25rem;
border-radius: 50%;
}
.fishbowl .chatlog__reference-author, .fishbowl .chatlog__reply-author {
.fishbowl .chatlog__reference-author {
margin-right: 0.3rem;
font-weight: 600;
}
.fishbowl .chatlog__reference-content, .fishbowl .chatlog__reply-content {
.fishbowl .chatlog__reference-content {
overflow: hidden;
text-overflow: ellipsis;
}
.fishbowl .chatlog__reference-link, .fishbowl .chatlog__reply-link {
.fishbowl .chatlog__reference-link {
cursor: pointer;
}
.fishbowl .chatlog__reference-link *, .fishbowl .chatlog__reply-link * {
.fishbowl .chatlog__reference-link * {
display: inline;
pointer-events: none;
}
.fishbowl .chatlog__reference-link .hljs, .fishbowl .chatlog__reply-link .hljs {
.fishbowl .chatlog__reference-link .hljs {
display: inline;
}
.fishbowl .chatlog__reference-link .chatlog__markdown-quote, .fishbowl .chatlog__reply-link .chatlog__markdown-quote {
.fishbowl .chatlog__reference-link .chatlog__markdown-quote {
display: inline
}
.fishbowl .chatlog__reference-link .chatlog__markdown-pre, .fishbowl .chatlog__reply-link .chatlog__markdown-pre {
.fishbowl .chatlog__reference-link .chatlog__markdown-pre {
display: inline
}
.fishbowl .chatlog__reference-link:hover, .fishbowl .chatlog__reply-link:hover {
.fishbowl .chatlog__reference-link:hover {
color: #2f3136;
}
.fishbowl .chatlog__reference-link:hover *:not(.chatlog__markdown-spoiler), .fishbowl .chatlog__reply-link:hover *:not(.chatlog__markdown-spoiler) {
.fishbowl .chatlog__reference-link:hover *:not(.chatlog__markdown-spoiler) {
color: inherit;
}
.fishbowl .chatlog__reference-edited-timestamp, .fishbowl .chatlog__reply-edited-timestamp {
.fishbowl .chatlog__reference-edited-timestamp {
margin-left: 0.25rem;
color: #5e6772;
font-size: 0.75rem;

0
public/icons.ttf Normal file → Executable file
View File

View File

@ -1,33 +0,0 @@
document.addEventListener("DOMContentLoaded", () => {
for (const countdown of document.querySelectorAll(".countdown")) {
const deadline = countdown.getAttribute("data-deadline");
const deadlineDate = new Date(parseInt(deadline, 10) * 1000);
function updateCountdown() {
const remainingMs = deadlineDate.getTime() - new Date().getTime();
const remainingMinutes = remainingMs / 1000 / 60;
const remainingHours = remainingMinutes / 60;
const remainingDays = remainingHours / 24; // no daylight savings transitions during the jam mmkay
let str = "imminently";
if (remainingMinutes < 60) {
str = `in ${Math.ceil(remainingMinutes)} ${
remainingMinutes === 1 ? "minute" : "minutes"
}`;
} else if (remainingHours < 24) {
str = `in ${Math.ceil(remainingHours)} ${
remainingHours === 1 ? "hour" : "hours"
}`;
} else {
str = `in ${Math.ceil(remainingDays)} ${
remainingDays === 1 ? "day" : "days"
}`;
}
countdown.innerText = str;
}
updateCountdown();
setInterval(updateCountdown, 1000 * 60);
}
});

View File

@ -1,28 +1,20 @@
function ImageSelector(form, maxFileSize, container, {
defaultImageUrl = "",
onUpdate = (url) => {},
} = {}) {
function ImageSelector(form, maxFileSize, container, defaultImageUrl) {
this.form = form;
this.maxFileSize = maxFileSize;
this.fileInput = container.querySelector(".imginput");
this.removeImageInput = container.querySelector(".imginput-remove");
this.fileInput = container.querySelector(".image_input");
this.removeImageInput = container.querySelector(".remove_input");
this.imageEl = container.querySelector("img");
this.container = container.querySelector(".imginput-container");
this.resetLink = container.querySelector(".imginput-reset-link");
this.removeLink = container.querySelector(".imginput-remove-link");
this.filenameText = container.querySelector(".imginput-filename");
this.errorEl = container.querySelector(".error");
this.originalImageUrl = this.imageEl.getAttribute("data-imginput-original");
this.originalImageFilename = this.imageEl.getAttribute("data-imginput-original-filename");
this.resetLink = container.querySelector(".reset");
this.removeLink = container.querySelector(".remove");
this.originalImageUrl = this.imageEl.getAttribute("data-original");
this.currentImageUrl = this.originalImageUrl;
this.defaultImageUrl = defaultImageUrl;
this.onUpdate = onUpdate;
this.defaultImageUrl = defaultImageUrl || "";
this.fileInput.value = "";
this.removeImageInput.value = "";
this.setImageUrl(this.originalImageUrl, true);
this.updatePreview();
this.setImageUrl(this.originalImageUrl);
this.updateButtons();
this.fileInput.addEventListener("change", function(ev) {
if (this.fileInput.files.length > 0) {
@ -41,16 +33,12 @@ function ImageSelector(form, maxFileSize, container, {
}
}
ImageSelector.prototype.openFileInput = function() {
this.fileInput.click();
}
ImageSelector.prototype.handleNewImageFile = function(file) {
if (file) {
this.updateSizeLimit(file.size);
this.removeImageInput.value = "";
this.setImageUrl(URL.createObjectURL(file));
this.updatePreview(file);
this.updateButtons();
}
};
@ -59,7 +47,7 @@ ImageSelector.prototype.removeImage = function() {
this.fileInput.value = "";
this.removeImageInput.value = "true";
this.setImageUrl(this.defaultImageUrl);
this.updatePreview(null);
this.updateButtons();
};
ImageSelector.prototype.resetImage = function() {
@ -67,7 +55,7 @@ ImageSelector.prototype.resetImage = function() {
this.fileInput.value = "";
this.removeImageInput.value = "";
this.setImageUrl(this.originalImageUrl);
this.updatePreview(null);
this.updateButtons();
};
ImageSelector.prototype.updateSizeLimit = function(size) {
@ -80,13 +68,11 @@ ImageSelector.prototype.updateSizeLimit = function(size) {
};
ImageSelector.prototype.setError = function(error) {
this.errorEl.textContent = error;
this.errorEl.hidden = !error;
this.fileInput.setCustomValidity(error);
this.fileInput.reportValidity();
}
ImageSelector.prototype.setImageUrl = function(url, initial = false) {
ImageSelector.prototype.setImageUrl = function(url) {
this.currentImageUrl = url;
this.imageEl.src = url;
if (url.length > 0) {
@ -94,30 +80,21 @@ ImageSelector.prototype.setImageUrl = function(url, initial = false) {
} else {
this.imageEl.style.display = "none";
}
this.url = url;
if (!initial) {
this.onUpdate(url);
}
};
ImageSelector.prototype.updatePreview = function(file) {
const showReset = (
this.originalImageUrl
&& this.originalImageUrl != this.defaultImageUrl
&& this.originalImageUrl != this.currentImageUrl
);
const showRemove = (
!this.fileInput.required
&& this.currentImageUrl != this.defaultImageUrl
);
this.resetLink.hidden = !showReset;
this.removeLink.hidden = !showRemove;
ImageSelector.prototype.updateButtons = function() {
if ((this.originalImageUrl.length > 0 && this.originalImageUrl != this.defaultImageUrl)
&& this.currentImageUrl != this.originalImageUrl) {
if (this.currentImageUrl == this.originalImageUrl) {
this.filenameText.innerText = this.originalImageFilename;
this.resetLink.style.display = "inline-block";
} else {
this.filenameText.innerText = file ? file.name : "";
this.resetLink.style.display = "none";
}
this.container.hidden = !this.currentImageUrl;
if (!this.fileInput.required && this.currentImageUrl != this.defaultImageUrl) {
this.removeLink.style.display = "inline-block";
} else {
this.removeLink.style.display = "none";
}
};

View File

@ -1,3 +0,0 @@
function rem2px(rem) {
return rem * parseFloat(getComputedStyle(document.documentElement).fontSize);
}

View File

@ -25,7 +25,6 @@ function doOnce(f) {
}
}
// TODO(redesign): Fix snippet editing, given that snippets are now presented very differently.
function makeShowcaseItem(timelineItem) {
const timestamp = showcaseTimestamp(timelineItem.date);
@ -46,38 +45,27 @@ function makeShowcaseItem(timelineItem) {
createModalContentFunc = () => {
const modalImage = document.createElement('img');
modalImage.src = timelineItem.asset_url;
modalImage.classList.add('mw-100', 'maxh-60vh');
modalImage.classList.add('mw-100', 'mh-60vh');
return modalImage;
};
break;
case TimelineMediaTypes.VIDEO:
addThumbnailFunc = () => {
let thumbEl;
if (timelineItem.thumbnail_url) {
thumbEl = document.createElement('img');
thumbEl.src = timelineItem.thumbnail_url;
} else {
thumbEl = document.createElement('video');
thumbEl.src = timelineItem.asset_url;
thumbEl.controls = false;
thumbEl.preload = 'metadata';
}
thumbEl.classList.add('h-100');
itemEl.thumbnail.appendChild(thumbEl);
const video = document.createElement('video');
video.src = timelineItem.asset_url; // TODO: Use image thumbnails
video.controls = false;
video.classList.add('h-100');
video.preload = 'metadata';
itemEl.thumbnail.appendChild(video);
};
createModalContentFunc = () => {
const modalVideo = document.createElement('video');
modalVideo.src = timelineItem.asset_url;
if (timelineItem.thumbnail_url) {
modalVideo.poster = timelineItem.thumbnail_url;
modalVideo.preload = 'none';
} else {
modalVideo.preload = 'metadata';
}
modalVideo.controls = true;
modalVideo.classList.add('mw-100', 'maxh-60vh');
modalVideo.preload = 'metadata';
modalVideo.classList.add('mw-100', 'mh-60vh');
return modalVideo;
};

View File

@ -24,13 +24,8 @@ function makeSnippetEdit(ownerName, ownerAvatar, ownerUrl, date, text, attachmen
let attachmentChanged = false;
let hasAttachment = false;
snippetEdit.redirect.value = location.href;
if (ownerAvatar) {
snippetEdit.avatarImg.src = ownerAvatar;
snippetEdit.avatarLink.href = ownerUrl;
snippetEdit.avatarImg.hidden = false;
} else {
snippetEdit.avatarImg.hidden = true;
}
snippetEdit.username.textContent = ownerName;
snippetEdit.username.href = ownerUrl;
snippetEdit.date.textContent = new Intl.DateTimeFormat([], {month: "2-digit", day: "2-digit", year: "numeric"}).format(date);
@ -372,10 +367,10 @@ function makeSnippetEdit(ownerName, ownerAvatar, ownerUrl, date, text, attachmen
function editTimelineSnippet(timelineItemEl, stickyProjectId) {
let ownerName = timelineItemEl.querySelector(".user")?.textContent;
let ownerUrl = timelineItemEl.querySelector(".user")?.href;
let ownerAvatar = timelineItemEl.querySelector(".avatar")?.src;
let ownerAvatar = timelineItemEl.querySelector(".avatar-icon")?.src;
let creationDate = new Date(timelineItemEl.querySelector("time").dateTime);
let rawDesc = timelineItemEl.querySelector(".rawdesc").textContent;
let attachment = timelineItemEl.querySelector(".timeline-media")?.children?.[0];
let attachment = timelineItemEl.querySelector(".timeline-content-box")?.children?.[0];
let projectIds = [];
let projectEls = timelineItemEl.querySelectorAll(".projects > a");
for (let i = 0; i < projectEls.length; ++i) {

View File

@ -1,38 +1,106 @@
function initTabs(container, {
initialTab = null,
onSelect = (name) => {},
}) {
const buttons = Array.from(container.querySelectorAll("[data-tab-button]"));
const tabs = Array.from(container.querySelectorAll("[data-tab]"));
function TabState(tabbed) {
this.container = tabbed;
this.tabs = tabbed.querySelector(".tab");
const firstTab = tabs[0].getAttribute("data-tab");
this.tabbar = document.createElement("div");
this.tabbar.classList.add("tab-bar");
this.container.insertBefore(this.tabbar, this.container.firstChild);
function selectTab(name, { sendEvent = true } = {}) {
if (!document.querySelector(`[data-tab="${name}"]`)) {
console.warn("no tab found with name", name);
return selectTab(firstTab, initial);
this.current_i = -1;
this.tab_buttons = [];
}
for (const tab of tabs) {
tab.hidden = tab.getAttribute("data-tab") !== name;
}
for (const button of buttons) {
button.classList.toggle("tab-button-active", button.getAttribute("data-tab-button") === name);
function switch_tab_old(state, tab_i) {
return function() {
if (state.current_i >= 0) {
state.tabs[state.current_i].classList.add("hidden");
state.tab_buttons[state.current_i].classList.remove("current");
}
if (sendEvent) {
onSelect(name);
}
}
selectTab(initialTab || firstTab, { sendEvent: false });
state.tabs[tab_i].classList.remove("hidden");
state.tab_buttons[tab_i].classList.add("current");
for (const button of buttons) {
button.addEventListener("click", () => {
selectTab(button.getAttribute("data-tab-button"));
});
var hash = "";
if (state.tabs[tab_i].hasAttribute("data-url-hash")) {
hash = state.tabs[tab_i].getAttribute("data-url-hash");
}
window.location.hash = hash;
return {
selectTab,
state.current_i = tab_i;
};
}
document.addEventListener("DOMContentLoaded", function() {
const tabContainers = document.getElementsByClassName("tabbed");
for (const container of tabContainers) {
const tabBar = document.createElement("div");
tabBar.classList.add("tab-bar");
container.insertAdjacentElement('afterbegin', tabBar);
const tabs = container.querySelectorAll(".tab");
for (let i = 0; i < tabs.length; i++) {
const tab = tabs[i];
tab.classList.toggle('dn', i > 0);
const slug = tab.getAttribute("data-slug");
// TODO: Should this element be a link?
const tabButton = document.createElement("div");
tabButton.classList.add("tab-button");
tabButton.classList.toggle("current", i === 0);
tabButton.innerText = tab.getAttribute("data-name");
tabButton.setAttribute("data-slug", slug);
tabButton.addEventListener("click", () => {
switchTab(container, slug);
});
tabBar.appendChild(tabButton);
}
const initialSlug = window.location.hash;
if (initialSlug) {
switchTab(container, initialSlug.substring(1));
}
}
});
function switchTab(container, slug) {
const tabs = container.querySelectorAll('.tab');
let didMatch = false;
for (const tab of tabs) {
const slugMatches = tab.getAttribute("data-slug") === slug;
tab.classList.toggle('dn', !slugMatches);
if (slugMatches) {
didMatch = true;
}
}
const tabButtons = document.querySelectorAll(".tab-button");
for (const tabButton of tabButtons) {
const buttonSlug = tabButton.getAttribute("data-slug");
tabButton.classList.toggle('current', slug === buttonSlug);
}
if (!didMatch) {
// switch to first tab as a fallback
tabs[0].classList.remove('dn');
tabButtons[0].classList.add('current');
}
window.location.hash = slug;
}
function switchToTabOfElement(container, el) {
const tabs = Array.from(container.querySelectorAll('.tab'));
let target = el.parentElement;
while (target) {
if (tabs.includes(target)) {
switchTab(container, target.getAttribute("data-slug"));
return;
}
target = target.parentElement;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 267 KiB

View File

@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 236 253" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="books" x="0" y="0" width="235.74" height="252.97" style="fill:none;"/><path d="M77.61,252.704l0,-230.569l80.52,-0l-0,230.569l-80.52,0Zm59.686,-188.853l0,-20.883l-38.853,-0l0,20.883l38.853,0Zm0,128.926l0,-108.092l-38.853,-0l0,108.092l38.853,-0Zm-38.853,20.833l0,18.261l38.853,-0l0,-18.261l-38.853,0Zm-98.032,39.094l-0,-252.438l70.429,-0l-0,252.438l-70.429,0Zm49.596,-206.372l-0,-25.233l-28.763,-0l0,25.233l28.763,0Zm-0,142.369l-0,-121.536l-28.763,0l0,121.536l28.763,-0Zm-28.763,20.833l0,22.337c0,-0 28.763,-0 28.763,-0l-0,-22.337l-28.763,-0Zm143.656,43.17l-0,-252.438l70.429,-0l-0,252.438l-70.429,0Zm49.596,-206.372l-0,-25.233l-28.763,-0l0,25.233l28.763,0Zm-0,142.369l-0,-121.536l-28.763,0l0,121.536l28.763,-0Zm-28.763,20.833l0,22.337l28.763,-0l-0,-22.337l-28.763,-0Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 336 233" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="lightbulb" x="0" y="0" width="335.159" height="232.656" style="fill:none;"/><path d="M197.613,202.015c0,16.576 -13.457,30.034 -30.034,30.034c-16.576,-0 -30.034,-13.458 -30.034,-30.034l60.068,-0Zm0.06,-9.97l-60.206,-0c-3.155,-16.04 -5.152,-23.793 -32.39,-57.373c-10.513,-12.96 -16.178,-29.812 -16.178,-47.773c0,-43.425 35.256,-78.68 78.68,-78.68c43.425,-0 78.68,34.8 78.68,78.224c0,17.961 -5.665,34.814 -16.177,47.774c-27.238,33.58 -29.254,41.788 -32.409,57.828Zm-16.343,-20.834c3.88,-11.276 11.569,-24.225 32.572,-50.118c7.611,-9.383 11.524,-21.647 11.524,-34.65c-0,-31.893 -25.954,-57.391 -57.847,-57.391c-31.926,0 -57.846,25.92 -57.846,57.847c-0,13.003 3.913,25.266 11.524,34.649c20.946,25.823 28.639,38.556 32.521,49.663l27.552,0Zm-160.921,-151.684c-4.694,-3.321 -5.808,-9.827 -2.488,-14.52c3.321,-4.693 9.827,-5.808 14.52,-2.488l31.239,22.101c4.694,3.321 5.808,9.827 2.488,14.52c-3.32,4.693 -9.827,5.808 -14.52,2.488l-31.239,-22.101Zm12.032,153.97c-4.693,3.32 -11.199,2.205 -14.52,-2.488c-3.32,-4.693 -2.206,-11.199 2.488,-14.52l31.239,-22.101c4.693,-3.32 11.2,-2.205 14.52,2.488c3.32,4.693 2.206,11.199 -2.488,14.52l-31.239,22.101Zm-21.856,-74.77c-5.749,-0 -10.416,-4.668 -10.416,-10.417c-0,-5.749 4.667,-10.417 10.416,-10.417l38.267,0c5.749,0 10.417,4.668 10.417,10.417c-0,5.749 -4.668,10.417 -10.417,10.417l-38.267,-0Zm292.133,-95.906c4.693,-3.32 11.199,-2.205 14.519,2.488c3.321,4.693 2.206,11.199 -2.487,14.52l-31.239,22.101c-4.694,3.32 -11.2,2.206 -14.52,-2.488c-3.321,-4.693 -2.206,-11.199 2.487,-14.52l31.24,-22.101Zm12.032,153.97c4.693,3.321 5.808,9.827 2.487,14.52c-3.32,4.694 -9.826,5.808 -14.519,2.488l-31.24,-22.101c-4.693,-3.321 -5.808,-9.827 -2.487,-14.52c3.32,-4.693 9.826,-5.808 14.52,-2.488l31.239,22.101Zm9.823,-78.595c5.75,-0 10.417,4.667 10.417,10.416c0,5.749 -4.667,10.417 -10.417,10.417l-38.266,-0c-5.75,-0 -10.417,-4.668 -10.417,-10.417c-0,-5.749 4.667,-10.416 10.417,-10.416l38.266,-0Z"/></svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 301 229" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="presentation" x="-0" y="0" width="300.716" height="228.578" style="fill:none;"/><path d="M124.979,14.95l175.159,0l0,152.429l-137.896,0c-1.49,-7.262 -3.898,-14.19 -7.097,-20.659l124.334,-0l-0,-111.11l-143.514,-0c-2.302,-7.632 -6.074,-14.628 -10.986,-20.66Zm-119.77,196.191c-2.997,-8.252 -4.631,-17.155 -4.631,-26.437c0,-42.797 34.745,-77.542 77.541,-77.542c42.796,0 77.541,34.745 77.541,77.542c0,9.282 -1.634,18.185 -4.631,26.437c-22.091,10.548 -46.816,16.455 -72.91,16.455c-26.094,0 -50.819,-5.907 -72.91,-16.455Zm15.522,-13.612c17.693,7.296 37.075,11.317 57.388,11.317c20.314,0 39.695,-4.021 57.389,-11.317c0.919,-4.129 1.402,-8.421 1.402,-12.825c0,-32.448 -26.343,-58.792 -58.791,-58.792c-32.448,0 -58.791,26.344 -58.791,58.792c0,4.404 0.484,8.696 1.403,12.825Zm57.388,-196.547c28.741,-0 52.075,23.334 52.075,52.074c-0,28.741 -23.334,52.075 -52.075,52.075c-28.74,-0 -52.074,-23.334 -52.074,-52.075c-0,-28.74 23.334,-52.074 52.074,-52.074Zm0,18.75c-18.392,-0 -33.324,14.932 -33.324,33.324c-0,18.392 14.932,33.325 33.324,33.325c18.392,-0 33.325,-14.933 33.325,-33.325c-0,-18.392 -14.933,-33.324 -33.325,-33.324Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,309 @@
/*
Inserts a CSS expression with one or more custom variables.
You can provide an arbitrary number of strings in the second
argument, separated by spaces. Any strings corresponding to
variable names will be replaced by the correct values, while
other strings are left untouched.
Example usage:
@include usevar(border-color, dimmer-color);
@include usevar(background, "linear-gradient(" dim-background-transparent "," dim-background ")");
For clarity and to avoid syntax issues, you are encouraged to
use unquoted strings for variables and quoted strings for
everything else.
For convenience in common cases, if only a single argument
is provided and it does not match an existing variable, this
will throw an error.
*/
pre, code, .codeblock {
/* Comment */
/* Error */
/* Keyword */
/* Literal */
/* Name */
/* Operator */
/* Punctuation */
/* Comment.Multiline */
/* Comment.Preproc */
/* Comment.Single */
/* Comment.Special */
/* Generic.Emph */
/* Generic.Strong */
/* Keyword.Constant */
/* Keyword.Declaration */
/* Keyword.Namespace */
/* Keyword.Pseudo */
/* Keyword.Reserved */
/* Keyword.Type */
/* Literal.Date */
/* Literal.Number */
/* Literal.String */
/* Name.Attribute */
/* Name.Builtin */
/* Name.Class */
/* Name.Constant */
/* Name.Decorator */
/* Name.Entity */
/* Name.Exception */
/* Name.Function */
/* Name.Label */
/* Name.Namespace */
/* Name.Other */
/* Name.Property */
/* Name.Tag */
/* Name.Variable */
/* Operator.Word */
/* Text.Whitespace */
/* Literal.Number.Float */
/* Literal.Number.Hex */
/* Literal.Number.Integer */
/* Literal.Number.Oct */
/* Literal.String.Backtick */
/* Literal.String.Char */
/* Literal.String.Doc */
/* Literal.String.Double */
/* Literal.String.Escape */
/* Literal.String.Heredoc */
/* Literal.String.Interpol */
/* Literal.String.Other */
/* Literal.String.Regex */
/* Literal.String.Single */
/* Literal.String.Symbol */
/* Name.Builtin.Pseudo */
/* Name.Variable.Class */
/* Name.Variable.Global */
/* Name.Variable.Instance */
/* Literal.Number.Integer.Long */
/* Generic Heading & Diff Header */
/* Generic.Subheading & Diff Unified/Comment? */
/* Generic.Deleted & Diff Deleted */
/* Generic.Inserted & Diff Inserted */ }
pre .hll, code .hll, .codeblock .hll {
background-color: #49483e; }
pre .c, code .c, .codeblock .c {
color: #75715e; }
pre .err, code .err, .codeblock .err {
color: #ff0000; }
pre .k, code .k, .codeblock .k {
color: #66d9ef; }
pre .l, code .l, .codeblock .l {
color: #ae81ff; }
pre .n, code .n, .codeblock .n {
color: #f8f8f2; }
pre .o, code .o, .codeblock .o {
color: #f92672; }
pre .p, code .p, .codeblock .p {
color: #f8f8f2; }
pre .cm, code .cm, .codeblock .cm {
color: #75715e; }
pre .cp, code .cp, .codeblock .cp {
color: #75715e; }
pre .c1, code .c1, .codeblock .c1 {
color: #75715e; }
pre .cs, code .cs, .codeblock .cs {
color: #75715e; }
pre .ge, code .ge, .codeblock .ge {
font-style: italic; }
pre .gs, code .gs, .codeblock .gs {
font-weight: bold; }
pre .kc, code .kc, .codeblock .kc {
color: #66d9ef; }
pre .kd, code .kd, .codeblock .kd {
color: #66d9ef; }
pre .kn, code .kn, .codeblock .kn {
color: #f92672; }
pre .kp, code .kp, .codeblock .kp {
color: #66d9ef; }
pre .kr, code .kr, .codeblock .kr {
color: #66d9ef; }
pre .kt, code .kt, .codeblock .kt {
color: #66d9ef; }
pre .ld, code .ld, .codeblock .ld {
color: #e6db74; }
pre .m, code .m, .codeblock .m {
color: #ae81ff; }
pre .s, code .s, .codeblock .s {
color: #e6db74; }
pre .na, code .na, .codeblock .na {
color: #a6e22e; }
pre .nb, code .nb, .codeblock .nb {
color: #f8f8f2; }
pre .nc, code .nc, .codeblock .nc {
color: #a6e22e; }
pre .no, code .no, .codeblock .no {
color: #66d9ef; }
pre .nd, code .nd, .codeblock .nd {
color: #a6e22e; }
pre .ni, code .ni, .codeblock .ni {
color: #f8f8f2; }
pre .ne, code .ne, .codeblock .ne {
color: #a6e22e; }
pre .nf, code .nf, .codeblock .nf {
color: #a6e22e; }
pre .nl, code .nl, .codeblock .nl {
color: #f8f8f2; }
pre .nn, code .nn, .codeblock .nn {
color: #f8f8f2; }
pre .nx, code .nx, .codeblock .nx {
color: #a6e22e; }
pre .py, code .py, .codeblock .py {
color: #f8f8f2; }
pre .nt, code .nt, .codeblock .nt {
color: #f92672; }
pre .nv, code .nv, .codeblock .nv {
color: #f8f8f2; }
pre .ow, code .ow, .codeblock .ow {
color: #f92672; }
pre .w, code .w, .codeblock .w {
color: #f8f8f2; }
pre .mf, code .mf, .codeblock .mf {
color: #ae81ff; }
pre .mh, code .mh, .codeblock .mh {
color: #ae81ff; }
pre .mi, code .mi, .codeblock .mi {
color: #ae81ff; }
pre .mo, code .mo, .codeblock .mo {
color: #ae81ff; }
pre .sb, code .sb, .codeblock .sb {
color: #e6db74; }
pre .sc, code .sc, .codeblock .sc {
color: #e6db74; }
pre .sd, code .sd, .codeblock .sd {
color: #e6db74; }
pre .s2, code .s2, .codeblock .s2 {
color: #e6db74; }
pre .se, code .se, .codeblock .se {
color: #ae81ff; }
pre .sh, code .sh, .codeblock .sh {
color: #e6db74; }
pre .si, code .si, .codeblock .si {
color: #e6db74; }
pre .sx, code .sx, .codeblock .sx {
color: #e6db74; }
pre .sr, code .sr, .codeblock .sr {
color: #e6db74; }
pre .s1, code .s1, .codeblock .s1 {
color: #e6db74; }
pre .ss, code .ss, .codeblock .ss {
color: #e6db74; }
pre .bp, code .bp, .codeblock .bp {
color: #f8f8f2; }
pre .vc, code .vc, .codeblock .vc {
color: #f8f8f2; }
pre .vg, code .vg, .codeblock .vg {
color: #f8f8f2; }
pre .vi, code .vi, .codeblock .vi {
color: #f8f8f2; }
pre .il, code .il, .codeblock .il {
color: #ae81ff; }
pre .gu, code .gu, .codeblock .gu {
color: #75715e; }
pre .gd, code .gd, .codeblock .gd {
color: #f92672; }
pre .gi, code .gi, .codeblock .gi {
color: #a6e22e; }
.light {
background-color: #fff;
color: #000; }
:root {
--fg-font-color: #eee;
--theme-color: #666;
--theme-color-dim: #444;
--theme-color-dimmer: #383838;
--theme-color-dimmest: #333;
--theme-color-dark: #666;
--theme-color-light: #666;
--link-color: #aaa;
--link-border-color: #aaa;
--hr-color: #aaa;
--main-background-color: #202020;
--main-color: #eee;
--dim-color: #bbb;
--dimmer-color: #999;
--dimmest-color: #777;
--menu-bottom-border-color: #444;
--login-popup-background: #181818;
--content-background: #202020;
--content-background-transparent: rgba(32, 32, 32, 0);
--dim-background: #252525;
--dim-background-transparent: rgba(37, 37, 37, 0);
--text-background: #181818;
--spoiler-border: #777;
--background-even-background: #242424;
--project-card-border-color: #333;
--project-user-suggestions-background: #222;
--project-user-suggestions-border-color: #444;
--notice-text-color: #eee;
--notice-unapproved-color: #7a2020;
--notice-hidden-color: #494949;
--notice-hiatus-color: #876327;
--notice-dead-color: #7a2020;
--notice-lts-color: #2a681d;
--notice-lts-reqd-color: #876327;
--notice-success-color: #2a681d;
--notice-warn-color: #876327;
--notice-failure-color: #7a2020;
--optionbar-border-color: #333;
--tab-background: #181818;
--tab-border-color: #3f3f3f;
--tab-button-background: #303030;
--tab-button-background-hover: #383838;
--tab-button-background-current: #181818;
--form-check-background: #252527;
--form-check-border-color: #666;
--form-check-border-color-hover: #084068;
--form-text-background: #181818;
--form-text-background-active: #252527;
--form-text-border-color: #444;
--form-text-border-color-active: #084068;
--form-button-color: #999;
--form-button-color-active: #4c9ed9;
--form-button-background: #383838;
--form-button-background-active: #303840;
--form-button-border-color: transparent;
--form-button-inline-border-color: transparent;
--form-error-color: #c61d24;
--landing-search-background: #282828;
--landing-search-background-hover: #181818;
--editor-toolbar-background: #282828;
--editor-toolbar-border-color: #333;
--editor-toolbar-button-background: #282828;
--editor-toolbar-button-background-hover: #333;
--editor-toolbar-button-border-color: #333;
--post-blockquote-border-color: #555;
--forum-even-background: #242424;
--forum-thread-read-color: #777;
--forum-thread-read-link-color: #999;
--forum-post-author-color: #999;
--forum-diff-source-background: #181818;
--forum-diff-source-border-color: #444;
--forum-diff-replace-background: #18283a;
--forum-diff-replace-border-color: #223d5b;
--forum-diff-delete-background: #3a1818;
--forum-diff-delete-border-color: #6b1e1c;
--forum-diff-insert-background: #233a18;
--forum-diff-insert-border-color: #30591b;
--card-background: #282828;
--card-background-hover: #333;
--timeline-content-background: rgba(255, 255, 255, 0.06);
--irc-border-color: #333;
--irc-tab-current-shadow: 0px 0px 5px #000 inset;
--irc-tab-close-button-color: #bbb;
--irc-tab-close-button-background: #444;
--irc-nick-border-color: #444;
--irc-users-color: #aaa;
--irc-users-background: #181818;
--irc-users-border-color: transparent;
--irc-users-popout-background: #181818;
--irc-users-popout-border-color-left: #444;
--irc-users-popout-border-color-right: #333;
--code-line-number-color: #444;
--library-star-btn-background: #252525;
--library-star-btn-border-color: #bbb;
--library-star-btn-a-border-color: #999;
--library-star-btn-a-hover-background: #333; }

View File

@ -0,0 +1,327 @@
/*
Inserts a CSS expression with one or more custom variables.
You can provide an arbitrary number of strings in the second
argument, separated by spaces. Any strings corresponding to
variable names will be replaced by the correct values, while
other strings are left untouched.
Example usage:
@include usevar(border-color, dimmer-color);
@include usevar(background, "linear-gradient(" dim-background-transparent "," dim-background ")");
For clarity and to avoid syntax issues, you are encouraged to
use unquoted strings for variables and quoted strings for
everything else.
For convenience in common cases, if only a single argument
is provided and it does not match an existing variable, this
will throw an error.
*/
pre, code, .codeblock {
/* Comment */
/* Error */
/* Keyword */
/* Operator */
/* Comment.Multiline */
/* Comment.Preproc */
/* Comment.Single */
/* Comment.Special */
/* Generic.Deleted */
/* Generic.Emph */
/* Generic.Error */
/* Generic.Heading */
/* Generic.Inserted */
/* Generic.Output */
/* Generic.Prompt */
/* Generic.Strong */
/* Generic.Subheading */
/* Generic.Traceback */
/* Keyword.Constant */
/* Keyword.Declaration */
/* Keyword.Namespace */
/* Keyword.Pseudo */
/* Keyword.Reserved */
/* Keyword.Type */
/* Literal.Number */
/* Literal.String */
/* Name.Attribute */
/* Name.Builtin */
/* Name.Class */
/* Name.Constant */
/* Name.Decorator */
/* Name.Entity */
/* Name.Exception */
/* Name.Function */
/* Name.Label */
/* Name.Namespace */
/* Name.Tag */
/* Name.Variable */
/* Operator.Word */
/* Text.Whitespace */
/* Literal.Number.Float */
/* Literal.Number.Hex */
/* Literal.Number.Integer */
/* Literal.Number.Oct */
/* Literal.String.Backtick */
/* Literal.String.Char */
/* Literal.String.Doc */
/* Literal.String.Double */
/* Literal.String.Escape */
/* Literal.String.Heredoc */
/* Literal.String.Interpol */
/* Literal.String.Other */
/* Literal.String.Regex */
/* Literal.String.Single */
/* Literal.String.Symbol */
/* Name.Builtin.Pseudo */
/* Name.Variable.Class */
/* Name.Variable.Global */
/* Name.Variable.Instance */
/* Literal.Number.Integer.Long */ }
pre .hll, code .hll, .codeblock .hll {
background-color: #ffffcc; }
pre .c, code .c, .codeblock .c {
color: #60a0b0;
font-style: italic; }
pre .err, code .err, .codeblock .err {
color: #FF0000; }
pre .k, code .k, .codeblock .k {
color: #007020;
font-weight: bold; }
pre .o, code .o, .codeblock .o {
color: #666666; }
pre .cm, code .cm, .codeblock .cm {
color: #60a0b0;
font-style: italic; }
pre .cp, code .cp, .codeblock .cp {
color: #007020; }
pre .c1, code .c1, .codeblock .c1 {
color: #60a0b0;
font-style: italic; }
pre .cs, code .cs, .codeblock .cs {
color: #60a0b0;
background-color: #fff0f0; }
pre .gd, code .gd, .codeblock .gd {
color: #A00000; }
pre .ge, code .ge, .codeblock .ge {
font-style: italic; }
pre .gr, code .gr, .codeblock .gr {
color: #FF0000; }
pre .gh, code .gh, .codeblock .gh {
color: #000080;
font-weight: bold; }
pre .gi, code .gi, .codeblock .gi {
color: #00A000; }
pre .go, code .go, .codeblock .go {
color: #808080; }
pre .gp, code .gp, .codeblock .gp {
color: #c65d09;
font-weight: bold; }
pre .gs, code .gs, .codeblock .gs {
font-weight: bold; }
pre .gu, code .gu, .codeblock .gu {
color: #800080;
font-weight: bold; }
pre .gt, code .gt, .codeblock .gt {
color: #0040D0; }
pre .kc, code .kc, .codeblock .kc {
color: #007020;
font-weight: bold; }
pre .kd, code .kd, .codeblock .kd {
color: #007020;
font-weight: bold; }
pre .kn, code .kn, .codeblock .kn {
color: #007020;
font-weight: bold; }
pre .kp, code .kp, .codeblock .kp {
color: #007020; }
pre .kr, code .kr, .codeblock .kr {
color: #007020;
font-weight: bold; }
pre .kt, code .kt, .codeblock .kt {
color: #902000; }
pre .m, code .m, .codeblock .m {
color: #40a070; }
pre .s, code .s, .codeblock .s {
color: #4070a0; }
pre .na, code .na, .codeblock .na {
color: #4070a0; }
pre .nb, code .nb, .codeblock .nb {
color: #007020; }
pre .nc, code .nc, .codeblock .nc {
color: #0e84b5;
font-weight: bold; }
pre .no, code .no, .codeblock .no {
color: #60add5; }
pre .nd, code .nd, .codeblock .nd {
color: #555555;
font-weight: bold; }
pre .ni, code .ni, .codeblock .ni {
color: #d55537;
font-weight: bold; }
pre .ne, code .ne, .codeblock .ne {
color: #007020; }
pre .nf, code .nf, .codeblock .nf {
color: #06287e; }
pre .nl, code .nl, .codeblock .nl {
color: #002070;
font-weight: bold; }
pre .nn, code .nn, .codeblock .nn {
color: #0e84b5;
font-weight: bold; }
pre .nt, code .nt, .codeblock .nt {
color: #062873;
font-weight: bold; }
pre .nv, code .nv, .codeblock .nv {
color: #bb60d5; }
pre .ow, code .ow, .codeblock .ow {
color: #007020;
font-weight: bold; }
pre .w, code .w, .codeblock .w {
color: #bbbbbb; }
pre .mf, code .mf, .codeblock .mf {
color: #40a070; }
pre .mh, code .mh, .codeblock .mh {
color: #40a070; }
pre .mi, code .mi, .codeblock .mi {
color: #40a070; }
pre .mo, code .mo, .codeblock .mo {
color: #40a070; }
pre .sb, code .sb, .codeblock .sb {
color: #4070a0; }
pre .sc, code .sc, .codeblock .sc {
color: #4070a0; }
pre .sd, code .sd, .codeblock .sd {
color: #4070a0;
font-style: italic; }
pre .s2, code .s2, .codeblock .s2 {
color: #4070a0; }
pre .se, code .se, .codeblock .se {
color: #4070a0;
font-weight: bold; }
pre .sh, code .sh, .codeblock .sh {
color: #4070a0; }
pre .si, code .si, .codeblock .si {
color: #70a0d0;
font-style: italic; }
pre .sx, code .sx, .codeblock .sx {
color: #c65d09; }
pre .sr, code .sr, .codeblock .sr {
color: #235388; }
pre .s1, code .s1, .codeblock .s1 {
color: #4070a0; }
pre .ss, code .ss, .codeblock .ss {
color: #517918; }
pre .bp, code .bp, .codeblock .bp {
color: #007020; }
pre .vc, code .vc, .codeblock .vc {
color: #bb60d5; }
pre .vg, code .vg, .codeblock .vg {
color: #bb60d5; }
pre .vi, code .vi, .codeblock .vi {
color: #bb60d5; }
pre .il, code .il, .codeblock .il {
color: #40a070; }
.dark {
background-color: #222;
color: #bbb; }
:root {
--fg-font-color: black;
--theme-color: #666;
--theme-color-dim: #aaa;
--theme-color-dimmer: #bbb;
--theme-color-dimmest: #ccc;
--theme-color-dark: #666;
--theme-color-light: #666;
--link-color: #666;
--link-border-color: #666;
--hr-color: #444;
--main-background-color: #fff;
--main-color: black;
--dim-color: #333;
--dimmer-color: #999;
--dimmest-color: #bbb;
--menu-bottom-border-color: black;
--login-popup-background: #fbfbfb;
--content-background: #f8f8f8;
--content-background-transparent: rgba(248, 248, 248, 0);
--dim-background: #f0f0f0;
--dim-background-transparent: rgba(240, 240, 240, 0);
--text-background: #f9f9f9;
--spoiler-border: #aaa;
--background-even-background: #f8f8f8;
--project-card-border-color: #aaa;
--project-user-suggestions-background: #fff;
--project-user-suggestions-border-color: #ddd;
--notice-text-color: #fff;
--notice-unapproved-color: #b42222;
--notice-hidden-color: #b6b6b6;
--notice-hiatus-color: #aa7d30;
--notice-dead-color: #b42222;
--notice-lts-color: #43a52f;
--notice-lts-reqd-color: #aa7d30;
--notice-success-color: #43a52f;
--notice-warn-color: #aa7d30;
--notice-failure-color: #b42222;
--optionbar-border-color: #ccc;
--tab-background: #fff;
--tab-border-color: #d8d8d8;
--tab-button-background: #dfdfdf;
--tab-button-background-hover: #efefef;
--tab-button-background-current: #fff;
--form-check-background: #fafafc;
--form-check-border-color: #999;
--form-check-border-color-hover: #4c9ed9;
--form-text-background: #fff;
--form-text-background-active: #fafafc;
--form-text-border-color: #999;
--form-text-border-color-active: #4c9ed9;
--form-button-color: black;
--form-button-color-active: #4c9ed9;
--form-button-background: #fff;
--form-button-background-active: #f2f2f2;
--form-button-border-color: #ccc;
--form-button-inline-border-color: #999;
--form-error-color: #c61d24;
--landing-search-background: #f8f8f8;
--landing-search-background-hover: #fefeff;
--editor-toolbar-background: #fff;
--editor-toolbar-border-color: transparent;
--editor-toolbar-button-background: transparent;
--editor-toolbar-button-background-hover: #ddd;
--editor-toolbar-button-border-color: #ccc;
--post-blockquote-border-color: #ddd;
--forum-even-background: #f0f0f0;
--forum-thread-read-color: #555;
--forum-thread-read-link-color: #888;
--forum-post-author-color: #333;
--forum-diff-source-background: #fff;
--forum-diff-source-border-color: #999;
--forum-diff-replace-background: #adcef4;
--forum-diff-replace-border-color: #4787d1;
--forum-diff-delete-background: #e57979;
--forum-diff-delete-border-color: #c12626;
--forum-diff-insert-background: #96e579;
--forum-diff-insert-border-color: #5baa3f;
--card-background: #e8e8e8;
--card-background-hover: #f0f0f0;
--timeline-content-background: rgba(0, 0, 0, 0.2);
--irc-border-color: #ddd;
--irc-tab-current-shadow: 0px 0px 5px #bbb inset;
--irc-tab-close-button-color: #fff;
--irc-tab-close-button-background: #aaa;
--irc-nick-border-color: #ccc;
--irc-users-color: black;
--irc-users-background: #fff;
--irc-users-border-color: #ccc;
--irc-users-popout-background: #fff;
--irc-users-popout-border-color-left: #bbb;
--irc-users-popout-border-color-right: #ccc;
--code-line-number-color: #777;
--library-star-btn-background: #fff;
--library-star-btn-border-color: #999;
--library-star-btn-a-border-color: #aaa;
--library-star-btn-a-hover-background: #fafafa; }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 814 KiB

View File

@ -1,163 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="739.32581"
height="447.97736"
viewBox="0 0 195.61328 118.52734"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><g
id="layer2"
transform="translate(-6.5429683,-7.9628906)"><g
id="g88"><path
style="color:#000000;fill:#ffffff;stroke-linecap:round;stroke-dashoffset:8.8;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 13.785156,7.9628906 c -3.9914079,0 -7.2421873,3.2507794 -7.2421873,7.2421874 V 119.24805 c 0,3.99141 3.2507794,7.24218 7.2421873,7.24218 H 194.91406 c 3.99141,0 7.24219,-3.25077 7.24219,-7.24218 V 15.205078 c 0,-3.991408 -3.25078,-7.2421874 -7.24219,-7.2421874 z m 0,2.3808594 H 194.91406 c 2.71339,0 4.86133,2.147942 4.86133,4.861328 V 119.24805 c 0,2.71338 -2.14794,4.86132 -4.86133,4.86133 H 13.785156 c -2.713386,0 -4.8613279,-2.14795 -4.8613279,-4.86133 V 15.205078 c 0,-2.713386 2.1479419,-4.861328 4.8613279,-4.861328 z"
id="rect1" /><path
style="color:#000000;fill:#ffffff;stroke-linecap:round;stroke-dashoffset:8.8;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 8.1660156,112.96094 a 1.190625,1.190625 0 0 0 -1.1914062,1.1914 1.190625,1.190625 0 0 0 1.1914062,1.18946 H 200.73633 a 1.190625,1.190625 0 0 0 1.1914,-1.18946 1.190625,1.190625 0 0 0 -1.1914,-1.1914 z"
id="path36" /><path
style="color:#000000;fill:#ffffff;stroke-linecap:round;stroke-dashoffset:8.8;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 180.14453,118.25195 a 1.190625,1.190625 0 0 0 -1.18945,1.19141 1.190625,1.190625 0 0 0 1.18945,1.19141 h 8.94922 a 1.190625,1.190625 0 0 0 1.19141,-1.19141 1.190625,1.190625 0 0 0 -1.19141,-1.19141 z"
id="path37" /></g><g
id="g89"><path
style="color:#000000;fill:#ffffff;stroke-dashoffset:8.8;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 39.162109,38.353516 -2.21875,0.867187 6.207032,15.861328 2.216796,-0.867187 z"
id="path3" /><path
style="color:#000000;fill:#ffffff;stroke-dashoffset:8.8;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 38.902344,80.289062 -4.63086,5.419922 1.808594,1.546875 4.632813,-5.419922 z"
id="path4" /><path
style="color:#000000;fill:#ffffff;stroke-dashoffset:8.8;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 64.087891,75.970703 -1.189454,2.0625 12.404297,7.154297 1.189453,-2.064453 z"
id="path5" /><path
style="color:#000000;fill:#ffffff;stroke-dashoffset:8.8;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 74.726562,52.677734 -12.070312,7.365235 1.240234,2.033203 12.070313,-7.365235 z"
id="path2" /><path
style="color:#000000;fill:#ffffff;stroke-linecap:round;stroke-dashoffset:8.8;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 83.150391,38.503906 c -5.744916,0 -10.427735,4.682819 -10.427735,10.427735 0,5.744915 4.682819,10.427734 10.427735,10.427734 5.744915,0 10.427734,-4.682819 10.427734,-10.427734 0,-5.744916 -4.682819,-10.427735 -10.427734,-10.427735 z m 0,2.38086 c 4.457993,0 8.046875,3.588881 8.046875,8.046875 0,4.457993 -3.588882,8.046875 -8.046875,8.046875 -4.457994,0 -8.044922,-3.588882 -8.044922,-8.046875 0,-4.457994 3.586928,-8.046875 8.044922,-8.046875 z"
id="use16" /><path
style="color:#000000;fill:#ffffff;stroke-linecap:round;stroke-dashoffset:8.8;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 83.757812,78.259766 c -5.744915,0 -10.427734,4.682819 -10.427734,10.427734 0,5.744915 4.682819,10.427734 10.427734,10.427734 5.744916,0 10.427735,-4.682819 10.427735,-10.427734 0,-5.744915 -4.682819,-10.427734 -10.427735,-10.427734 z m 0,2.380859 c 4.457994,0 8.046875,3.588881 8.046875,8.046875 0,4.457994 -3.588881,8.044922 -8.046875,8.044922 -4.457993,0 -8.046875,-3.586928 -8.046875,-8.044922 0,-4.457994 3.588882,-8.046875 8.046875,-8.046875 z"
id="use17" /><path
style="color:#000000;fill:#ffffff;stroke-linecap:round;stroke-dashoffset:8.8;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 29.087891,83.179687 c -5.744916,0 -10.427735,4.682819 -10.427735,10.427735 0,5.744915 4.682819,10.427738 10.427735,10.427738 5.744915,0 10.427734,-4.682823 10.427734,-10.427738 0,-5.744916 -4.682819,-10.427735 -10.427734,-10.427735 z m 0,2.38086 c 4.457993,0 8.044921,3.588881 8.044921,8.046875 0,4.457994 -3.586928,8.044918 -8.044921,8.044918 -4.457994,0 -8.046875,-3.586924 -8.046875,-8.044918 0,-4.457994 3.588881,-8.046875 8.046875,-8.046875 z"
id="use18" /><path
style="color:#000000;fill:#ffffff;stroke-linecap:round;stroke-dashoffset:8.8;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 34.621094,19.585937 c -5.744916,0 -10.427735,4.682819 -10.427735,10.427735 0,5.744915 4.682819,10.427734 10.427735,10.427734 5.744915,0 10.425781,-4.682819 10.425781,-10.427734 0,-5.744916 -4.680866,-10.427735 -10.425781,-10.427735 z m 0,2.382813 c 4.457994,0 8.044922,3.586928 8.044922,8.044922 0,4.457994 -3.586928,8.046875 -8.044922,8.046875 -4.457994,0 -8.046875,-3.588881 -8.046875,-8.046875 0,-4.457994 3.588881,-8.044922 8.046875,-8.044922 z"
id="use83" /><path
style="color:#000000;fill:#ffffff;stroke-linecap:round;stroke-dashoffset:8.8;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 49.949219,55.927734 c -7.313646,0 -13.265625,5.951979 -13.265625,13.265625 0,7.313646 5.951979,13.265625 13.265625,13.265625 7.313646,0 13.265625,-5.951979 13.265625,-13.265625 0,-7.313646 -5.951979,-13.265625 -13.265625,-13.265625 z m 0,2.117188 c 6.169714,0 11.148437,4.978724 11.148437,11.148437 0,6.169714 -4.978723,11.148438 -11.148437,11.148438 -6.169714,0 -11.148438,-4.978724 -11.148438,-11.148438 0,-6.169713 4.978724,-11.148437 11.148438,-11.148437 z"
id="use84" /></g><g
id="g90"><path
style="color:#000000;fill:#ffffff;stroke-dashoffset:8.8;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 127.79297,52.058594 v 2.380859 H 158.75 v -2.380859 z"
id="path61" /><path
style="color:#000000;fill:#ffffff;stroke-dashoffset:8.8;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 171.97852,52.058594 v 2.380859 h 13.23046 v -2.380859 z"
id="path62" /><path
style="color:#000000;fill:#ffffff;stroke-dashoffset:8.8;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 167.48047,40.416016 v 2.380859 h 17.72851 v -2.380859 z"
id="path63" /><path
style="color:#000000;fill:#ffffff;stroke-dashoffset:8.8;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 138.90625,64.757812 v 2.38086 h 44.18555 v -2.38086 z"
id="path64" /><path
style="color:#000000;fill:#ffffff;stroke-dashoffset:8.8;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 119.85547,76.929687 v 2.38086 h 19.05078 v -2.38086 z"
id="path65" /><path
style="color:#000000;fill:#ffffff;stroke-dashoffset:8.8;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 174.0957,28.246094 v 2.380859 h 11.11328 v -2.380859 z"
id="path66" /><path
style="color:#000000;fill:#ffffff;stroke-dashoffset:8.8;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 158.75,89.628906 v 2.38086 h 11.11328 v -2.38086 z"
id="path67" /><path
style="color:#000000;fill:#ffffff;stroke-linecap:round;stroke-dashoffset:33.2598;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 171.97852,25.765625 c -2.01296,0 -3.66993,1.656968 -3.66993,3.669922 0,2.012954 1.65697,3.671875 3.66993,3.671875 2.01295,0 3.67187,-1.658921 3.67187,-3.671875 0,-2.012954 -1.65892,-3.669922 -3.67187,-3.669922 z m 0,2.380859 c 0.72603,0 1.28906,0.56303 1.28906,1.289063 0,0.726032 -0.56303,1.289062 -1.28906,1.289062 -0.72604,0 -1.28907,-0.56303 -1.28907,-1.289062 0,-0.726033 0.56303,-1.289063 1.28907,-1.289063 z"
id="use67" /><path
style="color:#000000;fill:#ffffff;stroke-linecap:round;stroke-dashoffset:33.2598;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 165.36523,37.9375 c -2.01295,0 -3.67187,1.656968 -3.67187,3.669922 0,2.012954 1.65892,3.669922 3.67187,3.669922 2.01296,0 3.66993,-1.656968 3.66993,-3.669922 0,-2.012954 -1.65697,-3.669922 -3.66993,-3.669922 z m 0,2.380859 c 0.72604,0 1.28907,0.56303 1.28907,1.289063 0,0.726032 -0.56303,1.289062 -1.28907,1.289062 -0.72603,0 -1.28906,-0.56303 -1.28906,-1.289062 0,-0.726033 0.56303,-1.289063 1.28906,-1.289063 z"
id="use68" /><path
style="color:#000000;fill:#ffffff;stroke-linecap:round;stroke-dashoffset:33.2598;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 185.20898,62.279297 c -2.01295,0 -3.67187,1.656968 -3.67187,3.669922 0,2.012954 1.65892,3.669922 3.67187,3.669922 2.01296,0 3.66993,-1.656968 3.66993,-3.669922 0,-2.012954 -1.65697,-3.669922 -3.66993,-3.669922 z m 0,2.380859 c 0.72604,0 1.28907,0.56303 1.28907,1.289063 0,0.726032 -0.56303,1.289062 -1.28907,1.289062 -0.72603,0 -1.28906,-0.56303 -1.28906,-1.289062 0,-0.726033 0.56303,-1.289063 1.28906,-1.289063 z"
id="use70" /><path
style="color:#000000;fill:#ffffff;stroke-linecap:round;stroke-dashoffset:33.2598;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 117.74023,74.449219 c -2.01295,0 -3.67187,1.656968 -3.67187,3.669922 0,2.012954 1.65892,3.669921 3.67187,3.669921 2.01296,0 3.66993,-1.656967 3.66993,-3.669921 0,-2.012954 -1.65697,-3.669922 -3.66993,-3.669922 z m 0,2.380859 c 0.72604,0 1.28907,0.56303 1.28907,1.289063 0,0.726032 -0.56303,1.289062 -1.28907,1.289062 -0.72603,0 -1.28906,-0.56303 -1.28906,-1.289062 0,-0.726033 0.56303,-1.289063 1.28906,-1.289063 z"
id="use71" /><path
style="color:#000000;fill:#ffffff;stroke-linecap:round;stroke-dashoffset:33.2598;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 171.97852,87.173828 c -2.01296,0 -3.66993,1.656968 -3.66993,3.669922 0,2.012954 1.65697,3.669922 3.66993,3.669922 2.01295,0 3.67187,-1.656968 3.67187,-3.669922 0,-2.012954 -1.65892,-3.669922 -3.67187,-3.669922 z m 0,2.380859 c 0.72603,0 1.28906,0.563031 1.28906,1.289063 0,0.726032 -0.56303,1.289062 -1.28906,1.289062 -0.72604,0 -1.28907,-0.56303 -1.28907,-1.289062 0,-0.726032 0.56303,-1.289062 1.28907,-1.289063 z"
id="use72" /><rect
style="fill:#ffffff;stroke-width:1.89844;stroke-linecap:round;stroke-dashoffset:33.2598;-inkscape-stroke:none;paint-order:fill markers stroke"
id="use73"
width="5.3566022"
height="5.3566022"
x="149.09836"
y="-112.82577"
transform="rotate(45)" /><rect
style="fill:#ffffff;stroke-width:1.89844;stroke-linecap:round;stroke-dashoffset:33.2598;-inkscape-stroke:none;paint-order:fill markers stroke"
id="use74"
width="5.3566022"
height="5.3566022"
x="157.70444"
y="-104.21969"
transform="rotate(45)" /><rect
style="fill:#ffffff;stroke-width:1.89844;stroke-linecap:round;stroke-dashoffset:33.2598;-inkscape-stroke:none;paint-order:fill markers stroke"
id="use75"
width="5.3566022"
height="5.3566022"
x="166.31052"
y="-95.613609"
transform="rotate(45)" /><rect
style="fill:#ffffff;stroke-width:1.89844;stroke-linecap:round;stroke-dashoffset:33.2598;-inkscape-stroke:none;paint-order:fill markers stroke"
id="use76"
width="5.3566022"
height="5.3566022"
x="147.22748"
y="-77.278923"
transform="rotate(45)" /><rect
style="fill:#ffffff;stroke-width:1.89844;stroke-linecap:round;stroke-dashoffset:33.2598;-inkscape-stroke:none;paint-order:fill markers stroke"
id="use77"
width="5.3566022"
height="5.3566022"
x="131.384"
y="-77.899246"
transform="rotate(45)" /><rect
style="fill:#ffffff;stroke-width:1.89844;stroke-linecap:round;stroke-dashoffset:33.2598;-inkscape-stroke:none;paint-order:fill markers stroke"
id="use78"
width="5.3566022"
height="5.3566022"
x="150.78217"
y="-45.660938"
transform="rotate(45)" /><rect
style="fill:#ffffff;stroke-width:1.89844;stroke-linecap:round;stroke-dashoffset:33.2598;-inkscape-stroke:none;paint-order:fill markers stroke"
id="use79"
width="5.3566022"
height="5.3566022"
x="142.17609"
y="-54.267017"
transform="rotate(45)" /><rect
style="fill:#ffffff;stroke-width:1.89844;stroke-linecap:round;stroke-dashoffset:33.2598;-inkscape-stroke:none;paint-order:fill markers stroke"
id="use80"
width="5.3566022"
height="5.3566022"
x="173.79407"
y="-50.71233"
transform="rotate(45)" /><rect
style="fill:#ffffff;stroke-width:1.89844;stroke-linecap:round;stroke-dashoffset:33.2598;-inkscape-stroke:none;paint-order:fill markers stroke"
id="use81"
width="5.3566022"
height="5.3566022"
x="156.58191"
y="-86.633354"
transform="rotate(45)" /><rect
style="fill:#ffffff;stroke-width:1.89844;stroke-linecap:round;stroke-dashoffset:33.2598;-inkscape-stroke:none;paint-order:fill markers stroke"
id="use82"
width="5.3566022"
height="5.3566022"
x="178.84547"
y="-73.724243"
transform="rotate(45)" /><path
style="color:#000000;fill:#ffffff;stroke-linecap:round;stroke-dashoffset:33.2598;-inkscape-stroke:none;paint-order:fill markers stroke"
d="m 125.67773,49.578125 c -2.01295,0 -3.67187,1.656968 -3.67187,3.669922 0,2.012954 1.65892,3.671875 3.67187,3.671875 2.01296,0 3.66993,-1.658921 3.66993,-3.671875 0,-2.012954 -1.65697,-3.669922 -3.66993,-3.669922 z m 0,2.380859 c 0.72604,0 1.28907,0.56303 1.28907,1.289063 0,0.726032 -0.56303,1.289062 -1.28907,1.289062 -0.72603,0 -1.28906,-0.56303 -1.28906,-1.289062 0,-0.726033 0.56303,-1.289063 1.28906,-1.289063 z"
id="use69" /></g></g></svg>

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 475 283" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<g id="Handlebars" transform="matrix(1,0,0,1,-100,-167)">
<path d="M404,196C407.778,166.333 384.333,169 364,184" style="fill:none;stroke:white;stroke-width:9px;"/>
</g>
<g id="Seat">
</g>
<path d="M120,42L160,42C177.006,42.052 186.77,48.289 191,58" style="fill:none;stroke:white;stroke-width:9px;"/>
<g id="Frame" transform="matrix(1,0,0,1,-100,-167)">
<path d="M314,390L445,274" style="fill:none;stroke:white;stroke-width:9px;"/>
<path d="M500,375L404,196" style="fill:none;stroke:white;stroke-width:9px;stroke-linecap:butt;"/>
<path d="M266,259L437,259" style="fill:none;stroke:white;stroke-width:9px;"/>
<path d="M314,390L248,209" style="fill:none;stroke:white;stroke-width:9px;stroke-linecap:butt;"/>
<g transform="matrix(0.815396,-0.0212494,-0.0212494,0.997554,66.2531,7.62625)">
<path d="M143.044,371.322L314,390" style="fill:none;stroke:white;stroke-width:10.11px;"/>
</g>
<path d="M175,375L266,259" style="fill:none;stroke:white;stroke-width:9px;"/>
</g>
<g id="Wheel" transform="matrix(1,0,0,1,-100,-167)">
<g transform="matrix(0.980392,0,0,0.980392,-46.0784,31.3725)">
<path d="M302,293.019C302,282.522 293.478,274 282.981,274L168.019,274C157.522,274 149,282.522 149,293.019L149,407.981C149,418.478 157.522,427 168.019,427L282.981,427C293.478,427 302,418.478 302,407.981L302,293.019ZM292.82,293.019L292.82,407.981C292.82,413.411 288.411,417.82 282.981,417.82C282.981,417.82 168.019,417.82 168.019,417.82C162.589,417.82 158.18,413.411 158.18,407.981L158.18,293.019C158.18,287.589 162.589,283.18 168.019,283.18L282.981,283.18C288.411,283.18 292.82,287.589 292.82,293.019Z" style="fill:white;"/>
</g>
<g transform="matrix(1,0,0,1,-50,26)">
<path d="M237,343C237,339.689 234.311,337 231,337L219,337C215.689,337 213,339.689 213,343L213,355C213,358.311 215.689,361 219,361L231,361C234.311,361 237,358.311 237,355L237,343Z" style="fill:white;"/>
</g>
</g>
<g id="Wheel1" serif:id="Wheel" transform="matrix(1,0,0,1,225,-167)">
<g transform="matrix(0.980392,0,0,0.980392,-46.0784,31.3725)">
<path d="M302,293.019C302,282.522 293.478,274 282.981,274L168.019,274C157.522,274 149,282.522 149,293.019L149,407.981C149,418.478 157.522,427 168.019,427L282.981,427C293.478,427 302,418.478 302,407.981L302,293.019ZM292.82,293.019L292.82,407.981C292.82,413.411 288.411,417.82 282.981,417.82C282.981,417.82 168.019,417.82 168.019,417.82C162.589,417.82 158.18,413.411 158.18,407.981L158.18,293.019C158.18,287.589 162.589,283.18 168.019,283.18L282.981,283.18C288.411,283.18 292.82,287.589 292.82,293.019Z" style="fill:white;"/>
</g>
<g transform="matrix(1,0,0,1,-50,26)">
<path d="M237,343C237,339.689 234.311,337 231,337L219,337C215.689,337 213,339.689 213,343L213,355C213,358.311 215.689,361 219,361L231,361C234.311,361 237,358.311 237,355L237,343Z" style="fill:white;"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

View File

@ -298,16 +298,6 @@ SCRIPT
savecheckpoint 110
fi
# Install ffmpeg and cpulimit
if [ $checkpoint -lt 120 ]; then
apt update
apt install -y \
ffmpeg \
cpulimit
savecheckpoint 120
fi
cat <<HELP
Everything has been successfully installed!

View File

@ -1,33 +1,22 @@
package admintools
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"git.handmade.network/hmn/hmn/src/assets"
"git.handmade.network/hmn/hmn/src/auth"
"git.handmade.network/hmn/hmn/src/config"
"git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/email"
"git.handmade.network/hmn/hmn/src/hmndata"
"git.handmade.network/hmn/hmn/src/hmnurl"
"git.handmade.network/hmn/hmn/src/logging"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/perf"
"git.handmade.network/hmn/hmn/src/templates"
"git.handmade.network/hmn/hmn/src/utils"
"git.handmade.network/hmn/hmn/src/website"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
@ -515,125 +504,10 @@ func init() {
}
}
hmndata.UpdateSnippetLastPostedForAllProjects(ctx, conn)
fmt.Printf("Done!\n")
},
}
adminCommand.AddCommand(fixupSnippetAssociation)
extractImage := &cobra.Command{
Use: "extractimage [source] [dest]",
Short: "Tests ffmpeg for extracting image from video",
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()
if len(args) < 2 {
fmt.Printf("You must provide input and output files.\n")
cmd.Usage()
os.Exit(1)
}
inFile := args[0]
outFile := args[1]
inBytes, err := ioutil.ReadFile(inFile)
if err != nil {
fmt.Printf("Error while reading input: %v\n", err)
os.Exit(1)
}
fmt.Printf("%v\n", len(inBytes))
file, err := os.CreateTemp("", "hmnasset")
if err != nil {
fmt.Printf("%v", err)
}
defer os.Remove(file.Name())
_, err = file.Write(inBytes)
if err != nil {
fmt.Printf("%v", err)
}
err = file.Close()
if err != nil {
fmt.Printf("%v", err)
}
inputArg := fmt.Sprintf("-i %s -filter_complex [0]select=gte(n\\,1)[s0] -map [s0] -f image2 -vcodec png -vframes 1 pipe:1", file.Name())
ffmpegCmd := exec.CommandContext(ctx, config.Config.PreviewGeneration.FFMpegPath, strings.Split(inputArg, " ")...)
fmt.Printf("\n%s\n", ffmpegCmd.String())
var output bytes.Buffer
var errorOut bytes.Buffer
ffmpegCmd.Stdout = &output
ffmpegCmd.Stderr = &errorOut
err = ffmpegCmd.Run()
if err != nil {
fmt.Printf("%v", err)
}
if len(errorOut.Bytes()) > 0 {
fmt.Printf("FFMpeg error:\n%s\n", string(errorOut.Bytes()))
}
out, err := os.Create(outFile)
if err != nil {
fmt.Printf("Error opening output file: %v\n", err)
os.Exit(1)
}
_, err = out.Write(output.Bytes())
if err != nil {
fmt.Printf("Error writing output: %v\n", err)
os.Exit(1)
}
fmt.Printf("%v", len(output.Bytes()))
out.Close()
fmt.Printf("Done!\n")
},
}
adminCommand.AddCommand(extractImage)
uploadAsset := &cobra.Command{
Use: "uploadasset <file> <content type>",
Short: "Upload a file to our asset CDN",
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 2 {
cmd.Usage()
os.Exit(1)
}
fname := args[0]
contentType := args[1]
ctx := context.Background()
conn := db.NewConn()
defer conn.Close(ctx)
assetContents := utils.Must1(io.ReadAll(utils.Must1(os.Open(fname))))
assetFilename := filepath.Base(fname)
fmt.Printf("Uploading %s with content type %s...\n", assetFilename, contentType)
asset := utils.Must1(assets.Create(ctx, conn, assets.CreateInput{
Content: assetContents,
Filename: assetFilename,
ContentType: contentType,
}))
fmt.Printf("Uploaded and accessible at %s\n", hmnurl.BuildS3Asset(asset.S3Key))
},
}
adminCommand.AddCommand(uploadAsset)
adminCommand.AddCommand(&cobra.Command{
Use: "newsletteremails",
Short: "Print a list of all newsletter email receipients",
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()
conn := db.NewConn()
defer conn.Close(ctx)
recipients := utils.Must1(db.Query[models.NewsletterEmail](ctx, conn, `SELECT $columns FROM newsletter_emails`))
for _, r := range recipients {
fmt.Println(r.Email)
}
},
})
addProjectCommands(adminCommand)
}

View File

@ -6,19 +6,10 @@ import (
"crypto/sha1"
"errors"
"fmt"
"image"
"io"
"net/http"
"os"
"os/exec"
"regexp"
"strings"
"git.handmade.network/hmn/hmn/src/config"
"git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/hmnurl"
"git.handmade.network/hmn/hmn/src/jobs"
"git.handmade.network/hmn/hmn/src/logging"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
"github.com/aws/aws-sdk-go-v2/aws"
@ -28,7 +19,6 @@ import (
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/aws/smithy-go"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
var client *s3.Client
@ -128,50 +118,21 @@ func Create(ctx context.Context, dbConn db.ConnOrTx, in CreateInput) (*models.As
}
}
var thumbnailKey *string
width := in.Width
height := in.Height
if previewBytes, thumbWidth, thumbHeight, err := ExtractPreview(ctx, in.ContentType, in.Content); err != nil {
logging.Error().Err(err).Msg("Failed to generate preview for asset")
} else if len(previewBytes) > 0 {
keyStr := AssetKey(id.String(), fmt.Sprintf("%s_thumb.jpg", id.String()))
thumbnailType := "image/jpeg"
_, err = client.PutObject(ctx, &s3.PutObjectInput{
Bucket: &config.Config.DigitalOcean.AssetsSpacesBucket,
Key: &keyStr,
Body: bytes.NewReader(previewBytes),
ACL: types.ObjectCannedACLPublicRead,
ContentType: &thumbnailType,
})
if err != nil {
logging.Error().Err(err).Msg("Failed to upload thumbnail for video")
} else {
thumbnailKey = &keyStr
}
if width == 0 || height == 0 {
width = thumbWidth
height = thumbHeight
}
}
// Save a record in our database
// TODO(db): Would be convient to use RETURNING here...
_, err = dbConn.Exec(ctx,
`
INSERT INTO asset (id, s3_key, thumbnail_s3_key, filename, size, mime_type, sha1sum, width, height, uploader_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
INSERT INTO asset (id, s3_key, filename, size, mime_type, sha1sum, width, height, uploader_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
`,
id,
key,
thumbnailKey,
filename,
len(in.Content),
in.ContentType,
checksum,
width,
height,
in.Width,
in.Height,
in.UploaderID,
)
if err != nil {
@ -193,175 +154,3 @@ func Create(ctx context.Context, dbConn db.ConnOrTx, in CreateInput) (*models.As
return asset, nil
}
func getFFMpegPath() string {
path := config.Config.PreviewGeneration.FFMpegPath
if path != "" {
return path
}
var err error
path, err = exec.LookPath("ffmpeg")
if err == nil {
return path
}
return ""
}
func ExtractPreview(ctx context.Context, mimeType string, inBytes []byte) ([]byte, int, int, error) {
log := logging.ExtractLogger(ctx)
execPath := getFFMpegPath()
if execPath == "" {
return nil, 0, 0, nil
}
if !strings.HasPrefix(mimeType, "video") {
return nil, 0, 0, nil
}
file, err := os.CreateTemp("", "hmnasset")
if err != nil {
return nil, 0, 0, oops.New(err, "Failed to create temp file for preview generation")
}
defer os.Remove(file.Name())
_, err = file.Write(inBytes)
if err != nil {
return nil, 0, 0, oops.New(err, "Failed to write to temp file for preview generation")
}
err = file.Close()
if err != nil {
return nil, 0, 0, oops.New(err, "Failed to close temp file for preview generation")
}
args := fmt.Sprintf("-i %s -filter_complex [0]select=gte(n\\,1)[s0] -map [s0] -c:v mjpeg -f mjpeg -vframes 1 pipe:1", file.Name())
if config.Config.PreviewGeneration.CPULimitPath != "" {
args = fmt.Sprintf("-l 10 -- %s %s", execPath, args)
execPath = config.Config.PreviewGeneration.CPULimitPath
}
ffmpegCmd := exec.CommandContext(ctx, execPath, strings.Split(args, " ")...)
var output bytes.Buffer
var errorOut bytes.Buffer
ffmpegCmd.Stdout = &output
ffmpegCmd.Stderr = &errorOut
err = ffmpegCmd.Run()
if err != nil {
log.Error().Str("ffmpeg output", errorOut.String()).Msg("FFMpeg returned error while generating preview thumbnail")
return nil, 0, 0, oops.New(err, "FFMpeg failed for preview generation")
}
imageBytes := output.Bytes()
cfg, _, err := image.DecodeConfig(bytes.NewBuffer(imageBytes))
if err != nil {
log.Error().Err(err).Msg("failed to get width/height from video thumbnail")
return nil, 0, 0, oops.New(err, "FFMpeg failed for preview generation")
}
return imageBytes, cfg.Width, cfg.Height, nil
}
func BackgroundPreviewGeneration(ctx context.Context, conn *pgxpool.Pool) jobs.Job {
log := logging.ExtractLogger(ctx).With().Str("module", "preview_gen").Logger()
job := jobs.New()
go func() {
defer job.Done()
log.Debug().Msg("Starting preview gen job")
if getFFMpegPath() == "" {
log.Warn().Msg("Couldn't find ffmpeg! No thumbnails will be generated.")
return
}
assets, err := db.Query[models.Asset](ctx, conn,
`
SELECT $columns
FROM asset
WHERE
mime_type LIKE 'video%'
AND (
thumbnail_s3_key IS NULL
OR thumbnail_s3_key = ''
OR thumbnail_s3_key LIKE '%.png'
OR width = 0
OR height = 0
)
`,
)
if err != nil {
log.Error().Err(oops.New(err, "Failed to fetch assets for preview generation")).Msg("Asset preview generation job failed")
return
}
log.Debug().Int("Num assets", len(assets)).Msg("Processing...")
for _, asset := range assets {
select {
case <-ctx.Done():
return
default:
}
log := log.With().Str("AssetID", asset.ID.String()).Logger()
ctx := logging.AttachLoggerToContext(&log, ctx)
log.Debug().Msg("Generating preview")
assetUrl := hmnurl.BuildS3Asset(asset.S3Key)
resp, err := http.Get(assetUrl)
if err != nil || resp.StatusCode != 200 {
log.Error().Err(err).Msg("Failed to fetch asset file for preview generation")
continue
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Error().Err(err).Msg("Failed to read asset body for preview generation")
continue
}
thumbBytes, width, height, err := ExtractPreview(ctx, asset.MimeType, body)
if err != nil {
log.Error().Err(err).Msg("Failed to run extraction for preview generation")
continue
} else if len(thumbBytes) > 0 {
keyStr := AssetKey(asset.ID.String(), fmt.Sprintf("%s_thumb.jpg", asset.ID.String()))
thumbnailType := "image/jpeg"
_, err = client.PutObject(ctx, &s3.PutObjectInput{
Bucket: &config.Config.DigitalOcean.AssetsSpacesBucket,
Key: &keyStr,
Body: bytes.NewReader(thumbBytes),
ACL: types.ObjectCannedACLPublicRead,
ContentType: &thumbnailType,
})
if err != nil {
log.Error().Err(err).Msg("Failed to upload thumbnail for video")
continue
}
_, err = conn.Exec(ctx,
`
UPDATE asset
SET
thumbnail_s3_key = $1,
width = $2,
height = $3
WHERE asset.id = $4
`,
keyStr,
width,
height,
asset.ID,
)
if err != nil {
log.Error().Err(err).Msg("Failed to update asset for preview generation")
continue
}
log.Debug().Msg("Generated preview successfully!")
} else {
log.Debug().Msg("No error, but no thumbnail was generated, skipping")
}
}
log.Debug().Msg("No more previews to generate")
}()
return job
}

View File

@ -24,7 +24,7 @@ const CSRFFieldName = "csrf_token"
const sessionDuration = time.Hour * 24 * 14
func MakeSessionId() string {
func makeSessionId() string {
idBytes := make([]byte, 40)
_, err := io.ReadFull(rand.Reader, idBytes)
if err != nil {
@ -47,16 +47,7 @@ func makeCSRFToken() string {
var ErrNoSession = errors.New("no session found")
func GetSession(ctx context.Context, conn *pgxpool.Pool, id string) (*models.Session, error) {
sess, err := db.QueryOne[models.Session](ctx, conn,
`
SELECT $columns
FROM session
WHERE
id = $1
AND expires_at > CURRENT_TIMESTAMP
`,
id,
)
sess, err := db.QueryOne[models.Session](ctx, conn, "SELECT $columns FROM session WHERE id = $1", id)
if err != nil {
if errors.Is(err, db.NotFound) {
return nil, ErrNoSession
@ -70,7 +61,7 @@ func GetSession(ctx context.Context, conn *pgxpool.Pool, id string) (*models.Ses
func CreateSession(ctx context.Context, conn *pgxpool.Pool, username string) (*models.Session, error) {
session := models.Session{
ID: MakeSessionId(),
ID: makeSessionId(),
Username: username,
ExpiresAt: time.Now().Add(sessionDuration),
CSRFToken: makeCSRFToken(),
@ -143,16 +134,7 @@ func DeleteExpiredSessions(ctx context.Context, conn *pgxpool.Pool) (int64, erro
return tag.RowsAffected(), nil
}
func DeleteExpiredPendingLogins(ctx context.Context, conn *pgxpool.Pool) (int64, error) {
tag, err := conn.Exec(ctx, "DELETE FROM pending_login WHERE expires_at <= CURRENT_TIMESTAMP")
if err != nil {
return 0, oops.New(err, "failed to delete expired pending logins")
}
return tag.RowsAffected(), nil
}
func PeriodicallyDeleteExpiredStuff(ctx context.Context, conn *pgxpool.Pool) jobs.Job {
func PeriodicallyDeleteExpiredSessions(ctx context.Context, conn *pgxpool.Pool) jobs.Job {
job := jobs.New()
go func() {
defer job.Done()
@ -163,7 +145,6 @@ func PeriodicallyDeleteExpiredStuff(ctx context.Context, conn *pgxpool.Pool) job
case <-t.C:
err := func() (err error) {
defer utils.RecoverPanicAsError(&err)
n, err := DeleteExpiredSessions(ctx, conn)
if err == nil {
if n > 0 {
@ -172,20 +153,10 @@ func PeriodicallyDeleteExpiredStuff(ctx context.Context, conn *pgxpool.Pool) job
} else {
logging.Error().Err(err).Msg("Failed to delete expired sessions")
}
n, err = DeleteExpiredPendingLogins(ctx, conn)
if err == nil {
if n > 0 {
logging.Info().Int64("num deleted pending logins", n).Msg("Deleted expired pending logins")
}
} else {
logging.Error().Err(err).Msg("Failed to delete expired pending logins")
}
return nil
}()
if err != nil {
logging.Error().Err(err).Msg("Panicked in PeriodicallyDeleteExpiredStuff")
logging.Error().Err(err).Msg("Panicked in PeriodicallyDeleteExpiredSessions")
}
case <-ctx.Done():
return

View File

@ -1,66 +0,0 @@
package buildcss
import (
"context"
"git.handmade.network/hmn/hmn/src/config"
"git.handmade.network/hmn/hmn/src/jobs"
"git.handmade.network/hmn/hmn/src/logging"
"github.com/evanw/esbuild/pkg/api"
)
var ActiveServerPort uint16
func RunServer(ctx context.Context) jobs.Job {
job := jobs.New()
if config.Config.Env != config.Dev {
job.Done()
return job
}
logger := logging.ExtractLogger(ctx).With().Str("module", "EsBuild").Logger()
esCtx, ctxErr := BuildContext()
if ctxErr != nil {
panic(ctxErr)
}
logger.Info().Msg("Starting esbuild server and watcher")
err := esCtx.Watch(api.WatchOptions{})
serverResult, err := esCtx.Serve(api.ServeOptions{
Port: config.Config.EsBuild.Port,
Servedir: "./",
OnRequest: func(args api.ServeOnRequestArgs) {
if args.Status != 200 {
logger.Warn().Interface("args", args).Msg("Response from esbuild server")
}
},
})
if err != nil {
panic(err)
}
ActiveServerPort = serverResult.Port
go func() {
<-ctx.Done()
logger.Info().Msg("Shutting down esbuild server and watcher")
esCtx.Dispose()
job.Done()
}()
return job
}
func BuildContext() (api.BuildContext, *api.ContextError) {
return api.Context(api.BuildOptions{
EntryPoints: []string{
"src/rawdata/css/style.css",
},
Outbase: "src/rawdata/css",
Outdir: "public",
External: []string{"/public/*"},
Bundle: true,
Write: true,
Engines: []api.Engine{
{Name: api.EngineChrome, Version: "109"},
{Name: api.EngineFirefox, Version: "109"},
{Name: api.EngineSafari, Version: "12"},
},
})
}

View File

@ -1,39 +0,0 @@
package cmd
import (
"fmt"
"os"
"git.handmade.network/hmn/hmn/src/buildcss"
"git.handmade.network/hmn/hmn/src/logging"
"git.handmade.network/hmn/hmn/src/website"
"github.com/spf13/cobra"
)
func init() {
buildCommand := &cobra.Command{
Use: "buildcss",
Short: "Build the website CSS",
Run: func(cmd *cobra.Command, args []string) {
ctx, err := buildcss.BuildContext()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
res := ctx.Rebuild()
outputFilenames := make([]string, 0)
for _, o := range res.OutputFiles {
outputFilenames = append(outputFilenames, o.Path)
}
logging.Info().
Interface("Errors", res.Errors).
Interface("Warnings", res.Warnings).
Msg("Ran esbuild")
if len(outputFilenames) > 0 {
logging.Info().Interface("Files", outputFilenames).Msg("Wrote files")
}
},
}
website.WebsiteCommand.AddCommand(buildCommand)
}

View File

@ -0,0 +1,95 @@
package buildscss
import (
"context"
"encoding/base64"
"fmt"
"os"
"path/filepath"
color "git.handmade.network/hmn/hmn/src/ansicolor"
"git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/website"
"github.com/spf13/cobra"
"github.com/wellington/go-libsass"
)
var compressed bool
func init() {
libsass.RegisterSassFunc("base64($filename)", func(ctx context.Context, in libsass.SassValue) (*libsass.SassValue, error) {
var filename string
err := libsass.Unmarshal(in, &filename)
if err != nil {
return nil, err
}
fileBytes, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
encoded, _ := libsass.Marshal(base64.StdEncoding.EncodeToString(fileBytes))
return &encoded, nil
})
buildCommand := &cobra.Command{
Use: "buildscss",
Short: "Build the website CSS",
Run: func(cmd *cobra.Command, args []string) {
style := libsass.NESTED_STYLE
if compressed {
style = libsass.COMPRESSED_STYLE
}
err := compile("src/rawdata/scss/style.scss", "public/style.css", "light", style)
if err != nil {
fmt.Println(color.Bold + color.Red + "Failed to compile main SCSS." + color.Reset)
fmt.Println(err)
os.Exit(1)
}
for _, theme := range []string{"light", "dark"} {
err := compile("src/rawdata/scss/theme.scss", fmt.Sprintf("public/themes/%s/theme.css", theme), theme, style)
if err != nil {
fmt.Println(color.Bold + color.Red + "Failed to compile theme SCSS." + color.Reset)
fmt.Println(err)
os.Exit(1)
}
}
},
}
buildCommand.Flags().BoolVar(&compressed, "compressed", false, "Minify the output CSS")
website.WebsiteCommand.AddCommand(buildCommand)
}
func compile(inpath, outpath string, theme string, style int) error {
err := os.MkdirAll(filepath.Dir(outpath), 0755)
if err != nil {
panic(oops.New(err, "failed to create directory for CSS file"))
}
outfile, err := os.OpenFile(outpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
panic(oops.New(err, "failed to open CSS file for writing"))
}
defer outfile.Close()
infile, err := os.Open(inpath)
if err != nil {
panic(oops.New(err, "failed to open SCSS file"))
}
compiler, err := libsass.New(outfile, infile,
libsass.IncludePaths([]string{
"src/rawdata/scss",
fmt.Sprintf("src/rawdata/scss/themes/%s", theme),
}),
libsass.OutputStyle(style),
)
if err != nil {
panic(oops.New(err, "failed to create SCSS compiler"))
}
return compiler.Run()
}

View File

@ -1,383 +0,0 @@
package calendar
import (
"bytes"
"context"
"crypto/sha1"
"io"
"net/http"
"sort"
"strings"
"sync"
"time"
"git.handmade.network/hmn/hmn/src/config"
"git.handmade.network/hmn/hmn/src/jobs"
"git.handmade.network/hmn/hmn/src/logging"
"git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/utils"
"github.com/emersion/go-ical"
)
type RawCalendarData struct {
Name string
Url string
Data []byte
Hash [sha1.Size]byte
}
type CalendarEvent struct {
ID string
Name string
Desc string
StartTime time.Time
EndTime time.Time
Duration time.Duration
CalName string
}
var unifiedCalendar *ical.Calendar
var rawCalendarData = make([]*RawCalendarData, 0)
var cachedICals = make(map[string][]byte)
var httpClient = http.Client{}
// NOTE(asaf): Passing an empty array for selectedCals returns all cals
func GetICal(selectedCals []string) ([]byte, error) {
if unifiedCalendar == nil {
return nil, oops.New(nil, "No calendar")
}
sort.Strings(selectedCals)
cacheKey := strings.Join(selectedCals, "##")
cachedICal, ok := cachedICals[cacheKey]
if ok {
return cachedICal, nil
}
var cal *ical.Calendar
if len(selectedCals) == 0 {
cal = unifiedCalendar
} else {
cal = newHMNCalendar()
for _, child := range unifiedCalendar.Children {
include := true
if child.Name == ical.CompEvent {
calName, _ := child.Props.Text(ical.PropComment)
if calName != "" {
found := false
for _, s := range selectedCals {
if calName == s {
found = true
}
}
if !found {
include = false
}
}
}
if include {
cal.Children = append(cal.Children, child)
}
}
}
var calBytes []byte
if len(cal.Children) > 0 {
var buffer bytes.Buffer
err := ical.NewEncoder(&buffer).Encode(cal)
if err != nil {
return nil, oops.New(err, "Failed to encode calendar to iCal")
}
calBytes = buffer.Bytes()
} else {
calBytes = emptyCalendarString()
}
cachedICals[cacheKey] = calBytes
return calBytes, nil
}
func GetFutureEvents() []CalendarEvent {
if unifiedCalendar == nil {
return nil
}
futureEvents := make([]CalendarEvent, 0)
eventObjects := unifiedCalendar.Events()
now := time.Now()
lastTime := now.Add(time.Hour * 24 * 365)
for _, ev := range eventObjects {
summary, err := ev.Props.Text(ical.PropSummary)
if err != nil {
logging.Error().Err(err).Msg("Failed to get summary for calendar event")
continue
}
startTime, err := ev.DateTimeStart(nil)
if err != nil {
logging.Error().Err(err).Str("Event name", summary).Msg("Failed to get start time for calendar event")
continue
}
var evTimes []time.Time
set, err := ev.RecurrenceSet(nil)
if err != nil {
logging.Error().Err(err).Str("Event name", summary).Msg("Failed to get recurrence set for calendar event")
continue
}
if set != nil {
evTimes = set.Between(now, lastTime, true)
} else if startTime.After(now) {
evTimes = []time.Time{startTime}
}
if len(evTimes) == 0 {
continue
}
desc, err := ev.Props.Text(ical.PropDescription)
if err != nil {
logging.Error().Err(err).Str("Event name", summary).Msg("Failed to get description for calendar event")
continue
}
calName, _ := ev.Props.Text(ical.PropComment)
uid, err := ev.Props.Text(ical.PropUID)
if err != nil {
logging.Error().Err(err).Str("Event name", summary).Msg("Failed to get uid for calendar event")
continue
}
endTime, err := ev.DateTimeStart(nil)
if err != nil {
logging.Error().Err(err).Str("Event name", summary).Msg("Failed to get end time for calendar event")
continue
}
evDuration := endTime.Sub(startTime)
for _, t := range evTimes {
futureEvents = append(futureEvents, CalendarEvent{
ID: uid,
Name: summary,
Desc: desc,
StartTime: t,
EndTime: t.Add(evDuration),
Duration: evDuration,
CalName: calName,
})
}
}
sort.Slice(futureEvents, func(i, j int) bool {
return futureEvents[i].StartTime.Before(futureEvents[j].StartTime)
})
return futureEvents
}
func MonitorCalendars(ctx context.Context) jobs.Job {
log := logging.ExtractLogger(ctx).With().Str("calendar goroutine", "calendar monitor").Logger()
if len(config.Config.Calendars) == 0 {
log.Info().Msg("No calendars specified in config")
return jobs.Noop()
}
ctx = logging.AttachLoggerToContext(&log, ctx)
job := jobs.New()
go func() {
defer func() {
log.Info().Msg("Shutting down calendar monitor")
job.Done()
}()
log.Info().Msg("Running calendar monitor")
monitorTimer := time.NewTimer(time.Second)
for {
select {
case <-monitorTimer.C:
err := func() (err error) {
defer utils.RecoverPanicAsError(&err)
ReloadCalendars(ctx)
return nil
}()
if err != nil {
logging.Error().Err(err).Msg("Panicked in MonitorCalendars")
}
monitorTimer.Reset(60 * time.Minute)
case <-ctx.Done():
return
}
}
}()
return job
}
func ReloadCalendars(ctx context.Context) {
log := logging.ExtractLogger(ctx)
// Download calendars
calChan := make(chan RawCalendarData, len(config.Config.Calendars))
var wg sync.WaitGroup
wg.Add(len(config.Config.Calendars))
for _, c := range config.Config.Calendars {
go func(cal config.CalendarSource) {
defer func() {
wg.Done()
logging.LogPanics(log)
}()
calUrl := cal.Url
req, err := http.NewRequestWithContext(ctx, "GET", calUrl, nil)
if err != nil {
log.Error().Err(err).Msg("Failed to create request for calendar fetch")
return
}
res, err := httpClient.Do(req)
if err != nil {
log.Error().Err(err).Str("Url", calUrl).Msg("Failed to fetch calendar")
return
}
if res.StatusCode > 299 || !strings.HasPrefix(res.Header.Get("Content-Type"), "text/calendar") {
log.Error().Str("Url", calUrl).Str("Status", res.Status).Msg("Failed to fetch calendar")
io.ReadAll(res.Body)
res.Body.Close()
return
}
data, err := io.ReadAll(res.Body)
res.Body.Close()
if err != nil {
log.Error().Err(err).Str("Url", calUrl).Msg("Failed to fetch calendar")
return
}
calChan <- RawCalendarData{Name: cal.Name, Url: calUrl, Data: data}
}(c)
}
wg.Wait()
newRawCalendarData := make([]*RawCalendarData, 0, len(config.Config.Calendars))
Collect:
for {
select {
case d := <-calChan:
newRawCalendarData = append(newRawCalendarData, &d)
default:
break Collect
}
}
// Diff calendars
same := true
for _, n := range newRawCalendarData {
n.Hash = sha1.Sum(n.Data)
}
sort.Slice(newRawCalendarData, func(i, j int) bool {
return newRawCalendarData[i].Name < newRawCalendarData[j].Name
})
if len(newRawCalendarData) != len(rawCalendarData) {
same = false
} else {
for i := range newRawCalendarData {
newData := newRawCalendarData[i]
oldData := rawCalendarData[i]
if newData.Name != oldData.Name {
same = false
break
}
if newData.Hash != oldData.Hash {
same = false
break
}
}
}
if same {
return
}
// Unify calendars and clear cache
rawCalendarData = newRawCalendarData
cachedICals = make(map[string][]byte)
unified := newHMNCalendar()
var timezones []string
for _, calData := range rawCalendarData {
decoder := ical.NewDecoder(bytes.NewReader(calData.Data))
calNameProp := ical.NewProp(ical.PropComment)
calNameProp.SetText(calData.Name)
for {
cal, err := decoder.Decode()
if err == io.EOF {
break
} else if err != nil {
log.Error().Err(err).Str("Url", calData.Url).Msg("Failed to parse calendar")
break
}
for _, child := range cal.Children {
if child.Name == ical.CompTimezone {
tzid, err := child.Props.Text(ical.PropTimezoneID)
if err != nil {
found := false
for _, s := range timezones {
if s == tzid {
found = true
}
}
if found {
continue
} else {
timezones = append(timezones, tzid)
}
} else {
continue
}
}
if child.Name == ical.CompEvent {
child.Props.Set(calNameProp)
}
unified.Children = append(unified.Children, child)
}
}
}
unifiedCalendar = unified
}
func newHMNCalendar() *ical.Calendar {
cal := ical.NewCalendar()
prodID := ical.NewProp(ical.PropProductID)
prodID.SetText("Handmade Network")
cal.Props.Set(prodID)
version := ical.NewProp(ical.PropVersion)
version.SetText("1.0")
cal.Props.Set(version)
name := ical.NewProp("X-WR-CALNAME")
name.SetText("Handmade Network")
cal.Props.Set(name)
return cal
}
// NOTE(asaf): The ical library we're using doesn't like encoding empty calendars, so we have to do this manually.
func emptyCalendarString() []byte {
empty := `BEGIN:VCALENDAR
VERSION:1.0
PRODID:Handmade Network
X-WR-CALNAME:Handmade Network
END:VCALENDAR
`
return []byte(empty)
}

View File

@ -23,7 +23,6 @@ var Config = HMNConfig{
LogLevel: tracelog.LogLevelError, // LogLevelWarn is recommended for production
MinConn: 2, // Keep these low for dev, high for production
MaxConn: 10,
SlowQueryThresholdMs: 200,
},
Auth: AuthConfig{
CookieDomain: ".handmade.local",
@ -79,20 +78,8 @@ var Config = HMNConfig{
BaseUrl: "https://api.twitch.tv/helix",
BaseIDUrl: "https://id.twitch.tv/oauth2",
},
Calendars: []CalendarSource{
},
EpisodeGuide: EpisodeGuide{
CineraOutputPath: "./annotations/",
Projects: map[string]string{"hero": "code", "riscy": "riscy", "bitwise": "bitwise"},
},
DevConfig: DevConfig{
LiveTemplates: true,
},
PreviewGeneration: PreviewGenerationConfig{
FFMpegPath: "", // Will not generate asset video thumbnails if ffmpeg is not specified
CPULimitPath: "", // Not mandatory. FFMpeg will not limited if this is not provided
},
EsBuild: EsBuildConfig{
Port: 9004,
},
}

View File

@ -28,12 +28,7 @@ type HMNConfig struct {
DigitalOcean DigitalOceanConfig
Discord DiscordConfig
Twitch TwitchConfig
Matrix MatrixConfig
EpisodeGuide EpisodeGuide
DevConfig DevConfig
PreviewGeneration PreviewGenerationConfig
Calendars []CalendarSource
EsBuild EsBuildConfig
}
type PostgresConfig struct {
@ -45,7 +40,6 @@ type PostgresConfig struct {
LogLevel tracelog.LogLevel
MinConn int32
MaxConn int32
SlowQueryThresholdMs int
}
type AuthConfig struct {
@ -97,18 +91,6 @@ type TwitchConfig struct {
BaseIDUrl string
}
type MatrixConfig struct {
Username string
Password string
BaseUrl string
AnnouncementsRoomID string
}
type CalendarSource struct {
Name string
Url string
}
type EpisodeGuide struct {
CineraOutputPath string
Projects map[string]string // NOTE(asaf): Maps from slugs to default episode guide topic
@ -119,19 +101,6 @@ type AdminConfig struct {
AtomPassword string
}
type DevConfig struct {
LiveTemplates bool // load templates live from the filesystem instead of embedding them
}
type PreviewGenerationConfig struct {
FFMpegPath string
CPULimitPath string
}
type EsBuildConfig struct {
Port uint16
}
func init() {
if Config.EpisodeGuide.Projects == nil {
Config.EpisodeGuide.Projects = make(map[string]string)

View File

@ -7,7 +7,6 @@ import (
"reflect"
"regexp"
"strings"
"time"
"git.handmade.network/hmn/hmn/src/config"
"git.handmade.network/hmn/hmn/src/logging"
@ -63,8 +62,8 @@ func NewConnWithConfig(cfg config.PostgresConfig) *pgx.Conn {
pgcfg, err := pgx.ParseConfig(cfg.DSN())
pgcfg.Tracer = &tracelog.TraceLog{
Logger: zerologadapter.NewLogger(log.Logger),
LogLevel: cfg.LogLevel,
zerologadapter.NewLogger(log.Logger),
cfg.LogLevel,
}
conn, err := pgx.ConnectConfig(context.Background(), pgcfg)
@ -89,8 +88,8 @@ func NewConnPoolWithConfig(cfg config.PostgresConfig) *pgxpool.Pool {
pgcfg.MinConns = cfg.MinConn
pgcfg.MaxConns = cfg.MaxConn
pgcfg.ConnConfig.Tracer = &tracelog.TraceLog{
Logger: zerologadapter.NewLogger(log.Logger),
LogLevel: cfg.LogLevel,
zerologadapter.NewLogger(log.Logger),
cfg.LogLevel,
}
conn, err := pgxpool.NewWithConfig(context.Background(), pgcfg)
@ -299,7 +298,6 @@ func QueryIterator[T any](
compiled := compileQuery(query, destType)
queryStart := time.Now()
rows, err := conn.Query(ctx, compiled.query, args...)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
@ -307,14 +305,6 @@ func QueryIterator[T any](
}
return nil, err
}
duration := time.Now().Sub(queryStart)
if config.Config.Postgres.SlowQueryThresholdMs > 0 && duration > time.Duration(config.Config.Postgres.SlowQueryThresholdMs)*time.Millisecond {
logging.Warn().
Interface("Duration", duration.String()).
Interface("Query", strings.ReplaceAll(strings.ReplaceAll(compiled.query, "\n", " "), "\t", " ")).
Interface("Args", args).
Msg("Slow query")
}
it := &Iterator[T]{
fieldPaths: compiled.fieldPaths,
@ -489,7 +479,6 @@ This is common for custom types like:
*/
var queryableKinds = []reflect.Kind{
reflect.Int,
reflect.String,
}
/*

View File

@ -17,6 +17,7 @@ import (
const SlashCommandProfile = "profile"
const ProfileOptionUser = "user"
const SlashCommandWishlist = "wishlist"
const SlashCommandManifesto = "manifesto"
// User command names
@ -49,6 +50,12 @@ func (bot *botInstance) createApplicationCommands(ctx context.Context) {
Name: UserCommandProfile,
}))
doOrWarn(CreateGuildApplicationCommand(ctx, CreateGuildApplicationCommandRequest{
Type: ApplicationCommandTypeChatInput,
Name: SlashCommandWishlist,
Description: "Check out the Handmade Network wishlist",
}))
doOrWarn(CreateGuildApplicationCommand(ctx, CreateGuildApplicationCommandRequest{
Type: ApplicationCommandTypeChatInput,
Name: SlashCommandManifesto,
@ -76,6 +83,17 @@ func (bot *botInstance) doInteraction(ctx context.Context, i *Interaction) {
bot.handleProfileCommand(ctx, i, userID)
case UserCommandProfile:
bot.handleProfileCommand(ctx, i, i.Data.TargetID)
case SlashCommandWishlist:
err := CreateInteractionResponse(ctx, i.ID, i.Token, InteractionResponse{
Type: InteractionCallbackTypeChannelMessageWithSource,
Data: &InteractionCallbackData{
Content: "Check out the Handmade Network wishlist at https://github.com/HandmadeNetwork/wishlist/discussions/",
Flags: FlagEphemeral,
},
})
if err != nil {
logging.ExtractLogger(ctx).Error().Err(err).Msg("failed to send wishlist response")
}
case SlashCommandManifesto:
err := CreateInteractionResponse(ctx, i.ID, i.Token, InteractionResponse{
Type: InteractionCallbackTypeChannelMessageWithSource,

View File

@ -23,34 +23,6 @@ import (
"github.com/jpillora/backoff"
)
type BotEvent struct {
Timestamp time.Time
Name string
Extra string
}
var botEvents = make([]BotEvent, 0, 1000)
var botEventsMutex = sync.Mutex{}
func RecordBotEvent(name, extra string) {
botEventsMutex.Lock()
defer botEventsMutex.Unlock()
if len(botEvents) > 1000 {
botEvents = botEvents[len(botEvents)-500:]
}
botEvents = append(botEvents, BotEvent{
Timestamp: time.Now(),
Name: name,
Extra: extra,
})
}
func GetBotEvents() []BotEvent {
botEventsMutex.Lock()
defer botEventsMutex.Unlock()
return botEvents[:]
}
func RunDiscordBot(ctx context.Context, dbConn *pgxpool.Pool) jobs.Job {
log := logging.ExtractLogger(ctx).With().Str("module", "discord").Logger()
ctx = logging.AttachLoggerToContext(&log, ctx)
@ -84,11 +56,6 @@ func RunDiscordBot(ctx context.Context, dbConn *pgxpool.Pool) jobs.Job {
log.Info().Msg("Connecting to the Discord gateway")
bot := newBotInstance(dbConn)
err := bot.Run(ctx)
disconnectMessage := ""
if err != nil {
disconnectMessage = err.Error()
}
RecordBotEvent("Disconnected", disconnectMessage)
if err != nil {
dur := boff.Duration()
log.Error().
@ -134,8 +101,6 @@ type botInstance struct {
conn *websocket.Conn
dbConn *pgxpool.Pool
resuming bool
heartbeatIntervalMs int
forceHeartbeat chan struct{}
@ -228,7 +193,6 @@ func (bot *botInstance) Run(ctx context.Context) (err error) {
logging.ExtractLogger(ctx).Info().Msg("Discord asked us to reconnect to the gateway")
return nil
case OpcodeInvalidSession:
RecordBotEvent("Failed to resume - invalid session", "")
// We tried to resume but the session was invalid.
// Delete the session and reconnect from scratch again.
_, err := bot.dbConn.Exec(ctx, `DELETE FROM discord_session`)
@ -300,11 +264,8 @@ func (bot *botInstance) connect(ctx context.Context) error {
}
}
RecordBotEvent("Connected", "")
if shouldResume {
RecordBotEvent("Resuming with session ID", session.ID)
// Reconnect to the previous session
bot.resuming = true
err := bot.sendGatewayMessage(ctx, GatewayMessage{
Opcode: OpcodeResume,
Data: Resume{
@ -579,20 +540,11 @@ func (bot *botInstance) processEventMsg(ctx context.Context, msg *GatewayMessage
panic(fmt.Sprintf("processEventMsg must only be used on Dispatch messages (opcode %d). Validate this before you call this function.", OpcodeDispatch))
}
if bot.resuming {
name := ""
if msg.EventName != nil {
name = *msg.EventName
}
RecordBotEvent("Got event while resuming", name)
}
switch *msg.EventName {
case "RESUMED":
// Nothing to do, but at least we can log something
logging.ExtractLogger(ctx).Info().Msg("Finished resuming gateway session")
bot.resuming = false
RecordBotEvent("Done resuming", "")
bot.createApplicationCommands(ctx)
case "MESSAGE_CREATE":
newMessage := *MessageFromMap(msg.Data, "")

View File

@ -185,7 +185,6 @@ func Scrape(ctx context.Context, dbConn *pgxpool.Pool, channelID string, earlies
return true
}
msg.Backfilled = true
err := HandleIncomingMessage(ctx, dbConn, &msg, createSnippets)
if err != nil {

View File

@ -1,17 +1,13 @@
package discord
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"math/rand"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"
@ -23,15 +19,9 @@ import (
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/parsing"
"git.handmade.network/hmn/hmn/src/utils"
"github.com/google/uuid"
)
var autostoreChannels = []string{
config.Config.Discord.ShowcaseChannelID,
// TODO(asaf): Add jam channel
}
func HandleIncomingMessage(ctx context.Context, dbConn db.ConnOrTx, msg *Message, createSnippets bool) error {
deleted := false
var err error
@ -46,10 +36,6 @@ func HandleIncomingMessage(ctx context.Context, dbConn db.ConnOrTx, msg *Message
deleted, err = CleanUpShowcase(ctx, dbConn, msg)
}
if !deleted && err == nil {
err = ShareToMatrix(ctx, msg)
}
if !deleted && err == nil {
err = MaybeInternMessage(ctx, dbConn, msg)
}
@ -58,11 +44,6 @@ func HandleIncomingMessage(ctx context.Context, dbConn db.ConnOrTx, msg *Message
err = HandleInternedMessage(ctx, dbConn, msg, deleted, createSnippets)
}
// when we needed her most...she vanished
// if !deleted && err == nil {
// err = FreyaMode(ctx, dbConn, msg)
// }
return err
}
@ -75,7 +56,17 @@ func CleanUpShowcase(ctx context.Context, dbConn db.ConnOrTx, msg *Message) (boo
return deleted, nil
}
if !messageShouldBeStored(msg) {
hasGoodContent := true
if msg.OriginalHasFields("content") && !messageHasLinks(msg.Content) {
hasGoodContent = false
}
hasGoodAttachments := true
if msg.OriginalHasFields("attachments") && len(msg.Attachments) == 0 {
hasGoodAttachments = false
}
if !hasGoodContent && !hasGoodAttachments {
err := DeleteMessage(ctx, msg.ChannelID, msg.ID)
if err != nil {
return deleted, oops.New(err, "failed to delete message")
@ -91,7 +82,7 @@ func CleanUpShowcase(ctx context.Context, dbConn db.ConnOrTx, msg *Message) (boo
err = SendMessages(ctx, dbConn, MessageToSend{
ChannelID: channel.ID,
Req: CreateMessageRequest{
Content: "Posts in #project-showcase are required to have either an image/video or a link, or start with `!til`. Discuss showcase content in #projects.",
Content: "Posts in #project-showcase are required to have either an image/video or a link. Discuss showcase content in #projects.",
},
})
if err != nil {
@ -146,163 +137,8 @@ func CleanUpLibrary(ctx context.Context, dbConn db.ConnOrTx, msg *Message) (bool
return deleted, nil
}
func FreyaMode(ctx context.Context, dbConn db.ConnOrTx, msg *Message) error {
if msg.Author.IsBot {
return nil
}
if msg.ChannelID == config.Config.Discord.ShowcaseChannelID || msg.ChannelID == config.Config.Discord.LibraryChannelID {
return nil
}
twitteryUrls := []string{
"https://twitter.com",
"https://x.com",
"https://vxtwitter.com",
"https://fxtwitter.com",
}
isTwittery := false
for _, url := range twitteryUrls {
if strings.Contains(msg.Content, url) {
isTwittery = true
}
}
if !isTwittery {
return nil
}
// FREYA MODE ENGAGED
approvedTweets := []string{
"https://vxtwitter.com/FreyaHolmer/status/1757836988495847568",
"https://vxtwitter.com/FreyaHolmer/status/1752441092501361103",
"https://vxtwitter.com/FreyaHolmer/status/1753813557966217268",
"https://vxtwitter.com/FreyaHolmer/status/1746228932188295579",
"https://vxtwitter.com/FreyaHolmer/status/1732687685850894799",
"https://vxtwitter.com/FreyaHolmer/status/1761487879178736048",
"https://vxtwitter.com/FreyaHolmer/status/1733820461492863442",
"https://vxtwitter.com/FreyaHolmer/status/1732845451701871101",
"https://vxtwitter.com/FreyaHolmer/status/1765680355657359585",
"https://vxtwitter.com/FreyaHolmer/status/1784678195997852129",
"https://vxtwitter.com/FreyaHolmer/status/1741468609044508831",
"https://vxtwitter.com/FreyaHolmer/status/1759306434053870012",
"https://vxtwitter.com/FreyaHolmer/status/1754929898492162178",
"https://vxtwitter.com/FreyaHolmer/status/1782498313511534822",
"https://vxtwitter.com/FreyaHolmer/status/1623737764041695232",
"https://vxtwitter.com/FreyaHolmer/status/1718979996125925494",
"https://vxtwitter.com/FreyaHolmer/status/1675945798448607248",
"https://vxtwitter.com/FreyaHolmer/status/1662229911375953922",
"https://vxtwitter.com/FreyaHolmer/status/1652235944752185345",
"https://vxtwitter.com/FreyaHolmer/status/1386408507218427905",
"https://vxtwitter.com/FreyaHolmer/status/1436696408506212353",
"https://vxtwitter.com/FreyaHolmer/status/1444755552777670657",
"https://vxtwitter.com/FreyaHolmer/status/1232826293902888960",
}
tweet := approvedTweets[rand.Intn(len(approvedTweets))]
err := SendMessages(ctx, dbConn, MessageToSend{
ChannelID: msg.ChannelID,
Req: CreateMessageRequest{
Content: fmt.Sprintf("No. Only Freya is allowed to tweet. %s", tweet),
},
})
if err != nil {
return oops.New(err, "failed to send Freya tweet")
}
return nil
}
func ShareToMatrix(ctx context.Context, msg *Message) error {
if msg.Flags&MessageFlagCrossposted == 0 {
return nil
}
if config.Config.Matrix.Username == "" {
logging.ExtractLogger(ctx).Warn().Msg("No Matrix user provided; Discord announcement will not be shared")
}
fullMsg, err := GetChannelMessage(ctx, msg.ChannelID, msg.ID)
if err != nil {
return oops.New(err, "failed to get published message contents")
}
bodyMarkdown := CleanUpMarkdown(ctx, fullMsg.Content)
bodyHTML := parsing.ParseMarkdown(bodyMarkdown, parsing.DiscordMarkdown)
// Log in to Matrix (we don't bother to keep access tokens around)
var accessToken string
{
type MatrixLogin struct {
Type string `json:"type"`
User string `json:"user"`
Password string `json:"password"`
}
type MatrixLoginResponse struct {
AccessToken string `json:"access_token"`
}
body := MatrixLogin{
Type: "m.login.password",
User: config.Config.Matrix.Username,
Password: config.Config.Matrix.Password,
}
bodyBytes := utils.Must1(json.Marshal(body))
res, err := http.Post(
"https://matrix.handmadecities.com/_matrix/client/r0/login",
"application/json",
bytes.NewReader(bodyBytes),
)
if err != nil || res.StatusCode >= 300 {
return oops.New(err, "failed to log into Matrix")
}
defer res.Body.Close()
resBodyBytes := utils.Must1(io.ReadAll(res.Body))
var resBody MatrixLoginResponse
utils.Must(json.Unmarshal(resBodyBytes, &resBody))
accessToken = resBody.AccessToken
}
// Create message
{
type MessageEvent struct {
MsgType string `json:"msgtype"`
Body string `json:"body"`
Format string `json:"format,omitempty"`
FormattedBody string `json:"formatted_body,omitempty"`
}
tid := "hmn" + strconv.Itoa(rand.Int())
body := MessageEvent{
MsgType: "m.text",
Body: bodyMarkdown,
Format: "org.matrix.custom.html",
FormattedBody: bodyHTML,
}
bodyBytes := utils.Must1(json.Marshal(body))
req := utils.Must1(http.NewRequestWithContext(
ctx,
http.MethodPut,
fmt.Sprintf(
"%s/_matrix/client/v3/rooms/%s/send/m.room.message/%s",
config.Config.Matrix.BaseUrl,
config.Config.Matrix.AnnouncementsRoomID,
tid,
),
bytes.NewReader(bodyBytes),
))
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken))
req.Header.Add("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil || res.StatusCode >= 300 {
return oops.New(err, "failed to send Matrix message")
}
}
logging.ExtractLogger(ctx).Info().
Str("contents", bodyMarkdown).
Msg("Published Discord announcement to Matrix")
return nil
}
func MaybeInternMessage(ctx context.Context, dbConn db.ConnOrTx, msg *Message) error {
if messageShouldBeStored(msg) {
if msg.ChannelID == config.Config.Discord.ShowcaseChannelID {
err := InternMessage(ctx, dbConn, msg)
if errors.Is(err, errNotEnoughInfo) {
logging.ExtractLogger(ctx).Warn().
@ -354,8 +190,8 @@ func InternMessage(
_, err = dbConn.Exec(ctx,
`
INSERT INTO discord_message (id, channel_id, guild_id, url, user_id, sent_at, snippet_created, backfilled)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
INSERT INTO discord_message (id, channel_id, guild_id, url, user_id, sent_at, snippet_created)
VALUES ($1, $2, $3, $4, $5, $6, $7)
`,
msg.ID,
msg.ChannelID,
@ -364,7 +200,6 @@ func InternMessage(
msg.Author.ID,
msg.Time(),
false,
msg.Backfilled,
)
if err != nil {
return oops.New(err, "failed to save new discord message")
@ -406,7 +241,7 @@ func FetchInternedMessage(ctx context.Context, dbConn db.ConnOrTx, msgId string)
// 1. Saves/updates content
// 2. Saves/updates snippet
// 3. Deletes content/snippet
func HandleInternedMessage(ctx context.Context, dbConn db.ConnOrTx, msg *Message, removeInternedMessage bool, createSnippet bool) error {
func HandleInternedMessage(ctx context.Context, dbConn db.ConnOrTx, msg *Message, deleted bool, createSnippet bool) error {
tx, err := dbConn.Begin(ctx)
if err != nil {
return oops.New(err, "failed to start transaction")
@ -417,11 +252,7 @@ func HandleInternedMessage(ctx context.Context, dbConn db.ConnOrTx, msg *Message
if err != nil && !errors.Is(err, db.NotFound) {
return err
} else if err == nil {
if !removeInternedMessage {
removeInternedMessage = !messageShouldBeStored(msg)
}
if !removeInternedMessage {
if !deleted {
err = SaveMessageContents(ctx, tx, interned, msg)
if err != nil {
return err
@ -600,7 +431,7 @@ var discordDownloadClient = &http.Client{
type DiscordResourceBadStatusCode error
func DownloadDiscordResource(ctx context.Context, url string) ([]byte, string, error) {
func downloadDiscordResource(ctx context.Context, url string) ([]byte, string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, "", oops.New(err, "failed to make Discord download request")
@ -660,7 +491,7 @@ func saveAttachment(
height = *attachment.Height
}
content, _, err := DownloadDiscordResource(ctx, attachment.Url)
content, _, err := downloadDiscordResource(ctx, attachment.Url)
if err != nil {
return nil, oops.New(err, "failed to download Discord attachment")
}
@ -730,7 +561,7 @@ func saveEmbed(
}
maybeSaveImageish := func(i EmbedImageish, contentTypeCheck func(string) bool) (*uuid.UUID, error) {
content, contentType, err := DownloadDiscordResource(ctx, *i.Url)
content, contentType, err := downloadDiscordResource(ctx, *i.Url)
if err != nil {
var statusError DiscordResourceBadStatusCode
if errors.As(err, &statusError) {
@ -854,7 +685,7 @@ func HandleSnippetForInternedMessage(ctx context.Context, dbConn db.ConnOrTx, in
if interned.MessageContent == nil {
// NOTE(asaf): Can't have a snippet without content
// NOTE(asaf): Messages that only have an attachment also have a content struct with an empty content string
// NOTE(asaf): Messages that only have an attachment also have blank content
// TODO(asaf): Do we need to delete existing snippets in this case??? Not entirely sure how to trigger this through discord
return nil
}
@ -904,7 +735,7 @@ func HandleSnippetForInternedMessage(ctx context.Context, dbConn db.ConnOrTx, in
if shouldCreate {
// Get an asset ID or URL to make a snippet from
assetId, url, err := getSnippetAssetOrUrl(ctx, tx, &interned.Message)
if assetId != nil || url != nil {
contentMarkdown := interned.MessageContent.LastContent
contentHTML := parsing.ParseMarkdown(contentMarkdown, parsing.DiscordMarkdown)
@ -943,6 +774,7 @@ func HandleSnippetForInternedMessage(ctx context.Context, dbConn db.ConnOrTx, in
}
}
}
}
if existingSnippet != nil {
// Update tags
@ -995,8 +827,6 @@ func HandleSnippetForInternedMessage(ctx context.Context, dbConn db.ConnOrTx, in
}
}
}
hmndata.UpdateSnippetLastPostedForAllProjects(ctx, tx)
}
err = tx.Commit(ctx)
@ -1008,7 +838,6 @@ func HandleSnippetForInternedMessage(ctx context.Context, dbConn db.ConnOrTx, in
}
// TODO(asaf): I believe this will also match https://example.com?hello=1&whatever=5
//
// Probably need to add word boundaries.
var REDiscordTag = regexp.MustCompile(`&([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)`)
@ -1083,36 +912,3 @@ func messageHasLinks(content string) bool {
return false
}
func messageShouldBeStored(msg *Message) bool {
if msg == nil {
return false
}
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(msg.Content)), "!til") {
return true
}
autostore := false
for _, cid := range autostoreChannels {
if msg.ChannelID == cid {
autostore = true
break
}
}
if autostore {
hasGoodContent := true
if msg.OriginalHasFields("content") && !messageHasLinks(msg.Content) {
hasGoodContent = false
}
hasGoodAttachments := true
if msg.OriginalHasFields("attachments") && len(msg.Attachments) == 0 {
hasGoodAttachments = false
}
return hasGoodContent || hasGoodAttachments
}
return false
}

View File

@ -244,22 +244,6 @@ const (
MessageTypeGuildInviteReminder MessageType = 22
)
type MessageFlags int
const (
MessageFlagCrossposted MessageFlags = 1 << iota
MessageFlagIsCrosspost
MessageFlagSuppressEmbeds
MessageFlagSourceMessageDeleted
MessageFlagUrgent
MessageFlagHasThread
MessageFlagEphemeral
MessageFlagLoading
MessageFlagFailedToMentionSomeRolesInThread
MessageFlagSuppressNotifications
MessageFlagIsVoiceMessage
)
// https://discord.com/developers/docs/resources/channel#message-object
type Message struct {
ID string `json:"id"`
@ -269,13 +253,11 @@ type Message struct {
Author *User `json:"author"` // note that this may not be an actual valid user (see the docs)
Timestamp string `json:"timestamp"`
Type MessageType `json:"type"`
Flags MessageFlags `json:"flags"`
Attachments []Attachment `json:"attachments"`
Embeds []Embed `json:"embeds"`
originalMap map[string]interface{}
Backfilled bool
}
func (m *Message) JumpURL() string {
@ -335,7 +317,6 @@ func MessageFromMap(m interface{}, k string) *Message {
Author: UserFromMap(m, "author"),
Timestamp: maybeString(mmap, "timestamp"),
Type: MessageType(maybeInt(mmap, "type")),
Flags: MessageFlags(maybeInt(mmap, "flags")),
originalMap: mmap,
}
@ -365,7 +346,6 @@ type User struct {
Avatar *string `json:"avatar"`
IsBot bool `json:"bot"`
Locale string `json:"locale"`
Email string `json:"email"`
}
func UserFromMap(m interface{}, k string) *User {
@ -409,7 +389,6 @@ func GuildFromMap(m interface{}, k string) *Guild {
type GuildMember struct {
User *User `json:"user"`
Nick *string `json:"nick"`
Avatar *string `json:"avatar"`
// more fields not yet handled here
}
@ -432,7 +411,6 @@ func GuildMemberFromMap(m interface{}, k string) *GuildMember {
gm := &GuildMember{
User: UserFromMap(m, "user"),
Nick: maybeStringP(mmap, "nick"),
Avatar: maybeStringP(mmap, "avatar"),
}
return gm
@ -1022,11 +1000,3 @@ func maybeBoolP(m map[string]interface{}, k string) *bool {
boolval := val.(bool)
return &boolval
}
func maybeArray(m map[string]any, k string) []any {
val, ok := m[k]
if !ok || val == nil {
return nil
}
return val.([]any)
}

View File

@ -110,7 +110,7 @@ func createLimiter(headers rateLimitHeaders, routeName string) {
buckets.Store(routeName, headers.Bucket)
ilimiter, loaded := rateLimiters.LoadOrStore(headers.Bucket, &restRateLimiter{
requests: make(chan struct{}, 200), // presumably this is big enough to handle bursts
requests: make(chan struct{}, 100), // presumably this is big enough to handle bursts
refills: make(chan rateLimiterRefill),
})
if !loaded {
@ -124,9 +124,7 @@ func createLimiter(headers rateLimitHeaders, routeName string) {
select {
case limiter.requests <- struct{}{}:
default:
log.Warn().
Int("remaining", headers.Remaining).
Msg("rate limiting channel was too small; you should increase the default capacity")
log.Warn().Msg("rate limiting channel was too small; you should increase the default capacity")
break prefillloop
}
}
@ -160,9 +158,7 @@ func createLimiter(headers rateLimitHeaders, routeName string) {
select {
case limiter.requests <- struct{}{}:
default:
log.Warn().
Int("maxRequests", refill.maxRequests).
Msg("rate limiting channel was too small; you should increase the default capacity")
log.Warn().Msg("rate limiting channel was too small; you should increase the default capacity")
break refillloop
}
}

View File

@ -652,17 +652,11 @@ func EditOriginalInteractionResponse(ctx context.Context, interactionToken strin
return &msg, nil
}
func GetAuthorizeUrl(state string, includeEmail bool) string {
scope := "identify"
if includeEmail {
scope = "identify email"
}
func GetAuthorizeUrl(state string) string {
params := make(url.Values)
params.Set("response_type", "code")
params.Set("client_id", config.Config.Discord.OAuthClientID)
params.Set("scope", scope)
params.Set("prompt", "none") // immediately redirect back to HMN if already authorized
params.Set("scope", "identify")
params.Set("state", state)
params.Set("redirect_uri", hmnurl.BuildDiscordOAuthCallback())
return fmt.Sprintf("%s?%s", buildUrl("/oauth2/authorize"), params.Encode())

View File

@ -48,47 +48,7 @@ func SendRegistrationEmail(
perf.EndBlock()
perf.StartBlock("EMAIL", "Sending email")
err = sendMail(toAddress, toName, "[Handmade Network] Registration confirmation", contents)
if err != nil {
return oops.New(err, "Failed to send email")
}
perf.EndBlock()
perf.EndBlock()
return nil
}
type ExistingAccountEmailData struct {
Name string
Username string
HomepageUrl string
LoginUrl string
}
func SendExistingAccountEmail(
toAddress string,
toName string,
username string,
destination string,
perf *perf.RequestPerf,
) error {
perf.StartBlock("EMAIL", "Existing account email")
perf.StartBlock("EMAIL", "Rendering template")
contents, err := renderTemplate("email_account_existing.html", ExistingAccountEmailData{
Name: toName,
Username: username,
HomepageUrl: hmnurl.BuildHomepage(),
LoginUrl: hmnurl.BuildLoginPage(destination),
})
if err != nil {
return err
}
perf.EndBlock()
perf.StartBlock("EMAIL", "Sending email")
err = sendMail(toAddress, toName, "[Handmade Network] You already have an account!", contents)
err = sendMail(toAddress, toName, "[handmade.network] Registration confirmation", contents)
if err != nil {
return oops.New(err, "Failed to send email")
}
@ -120,7 +80,7 @@ func SendPasswordReset(toAddress string, toName string, username string, resetTo
perf.EndBlock()
perf.StartBlock("EMAIL", "Sending email")
err = sendMail(toAddress, toName, "[Handmade Network] Your password reset request", contents)
err = sendMail(toAddress, toName, "[handmade.network] Your password reset request", contents)
if err != nil {
return oops.New(err, "Failed to send email")
}
@ -131,41 +91,6 @@ func SendPasswordReset(toAddress string, toName string, username string, resetTo
return nil
}
type TimeMachineEmailData struct {
ProfileUrl string
Username string
UserEmail string
DiscordUsername string
MediaUrls []string
DeviceInfo string
Description string
}
func SendTimeMachineEmail(profileUrl, username, userEmail, discordUsername string, mediaUrls []string, deviceInfo, description string, perf *perf.RequestPerf) error {
perf.StartBlock("EMAIL", "Time machine email")
defer perf.EndBlock()
contents, err := renderTemplate("email_time_machine.html", TimeMachineEmailData{
ProfileUrl: profileUrl,
Username: username,
UserEmail: userEmail,
DiscordUsername: discordUsername,
MediaUrls: mediaUrls,
DeviceInfo: deviceInfo,
Description: description,
})
if err != nil {
return err
}
err = sendMail("team@handmade.network", "HMN Team", "[Time Machine] New submission", contents)
if err != nil {
return oops.New(err, "Failed to send email")
}
return nil
}
var EmailRegex = regexp.MustCompile(`^[^:\p{Cc} ]+@[^:\p{Cc} ]+\.[^:\p{Cc} ]+$`)
func IsEmail(address string) bool {
@ -174,7 +99,10 @@ func IsEmail(address string) bool {
func renderTemplate(name string, data interface{}) (string, error) {
var buffer bytes.Buffer
template := templates.GetTemplate(name)
template, hasTemplate := templates.Templates[name]
if !hasTemplate {
return "", oops.New(nil, "Template not found: %s", name)
}
err := template.Execute(&buffer, data)
if err != nil {
return "", oops.New(err, "Failed to render template for email")

View File

@ -129,7 +129,7 @@ func FetchEmbed(ctx context.Context, urlStr string, httpClient *http.Client, max
filename := ""
u, err := url.Parse(urlStr)
if err == nil {
lastSlash := utils.Max(strings.LastIndex(u.Path, "/"), 0)
lastSlash := utils.IntMax(strings.LastIndex(u.Path, "/"), 0)
filename = u.Path[lastSlash:]
}
result := Embed{

View File

@ -19,7 +19,6 @@ type Jam struct {
Event
Name string
Slug string
UrlSlug string
}
var WRJ2021 = Jam{
@ -29,7 +28,6 @@ var WRJ2021 = Jam{
},
Name: "Wheel Reinvention Jam 2021",
Slug: "WRJ2021",
UrlSlug: "2021",
}
var WRJ2022 = Jam{
@ -39,7 +37,6 @@ var WRJ2022 = Jam{
},
Name: "Wheel Reinvention Jam 2022",
Slug: "WRJ2022",
UrlSlug: "2022",
}
var VJ2023 = Jam{
@ -49,43 +46,8 @@ var VJ2023 = Jam{
},
Name: "Visibility Jam 2023",
Slug: "VJ2023",
UrlSlug: "visibility-2023",
}
var WRJ2023 = Jam{
Event: Event{
StartTime: time.Date(2023, 9, 25, 10, 0, 0, 0, utils.Must1(time.LoadLocation("Europe/London"))),
EndTime: time.Date(2023, 10, 1, 20, 0, 0, 0, utils.Must1(time.LoadLocation("Europe/London"))),
},
Name: "Wheel Reinvention Jam 2023",
Slug: "WRJ2023",
UrlSlug: "2023",
}
var LJ2024 = Jam{
Event: Event{
StartTime: time.Date(2024, 8, 15, 17, 0, 0, 0, time.UTC),
EndTime: time.Date(2024, 8, 25, 0, 0, 0, 0, time.UTC),
},
Name: "Learning Jam 2024",
Slug: "LJ2024",
UrlSlug: "learning-2024",
}
var VJ2024 = Jam{
// Trying looser times this year.
// Start: 6am Seattle / 8am Minneapolis / 1pm UTC / 2pm London / 4pm Jerusalem
// End: 10pm Seattle / 12am Minneapolis / 5am UTC / 6am London / 8am Jerusalem
Event: Event{
StartTime: time.Date(2024, 7, 19, 13, 0, 0, 0, time.UTC),
EndTime: time.Date(2024, 7, 22, 5, 0, 0, 0, time.UTC),
},
Name: "Visibility Jam 2024",
Slug: "VJ2024",
UrlSlug: "visibility-2024",
}
// Conferences
var HMS2022 = Event{
StartTime: time.Date(2022, 11, 16, 0, 0, 0, 0, utils.Must1(time.LoadLocation("America/Los_Angeles"))),
EndTime: time.Date(2022, 11, 18, 0, 0, 0, 0, utils.Must1(time.LoadLocation("America/Los_Angeles"))),
@ -101,17 +63,7 @@ var HMBoston2023 = Event{
EndTime: time.Date(2023, 8, 4, 0, 0, 0, 0, utils.Must1(time.LoadLocation("America/Los_Angeles"))),
}
var HMS2024 = Event{
StartTime: time.Date(2024, 11, 20, 0, 0, 0, 0, utils.Must1(time.LoadLocation("America/Los_Angeles"))),
EndTime: time.Date(2024, 11, 22, 0, 0, 0, 0, utils.Must1(time.LoadLocation("America/Los_Angeles"))),
}
var HMBoston2024 = Event{
StartTime: time.Date(2024, 8, 9, 0, 0, 0, 0, utils.Must1(time.LoadLocation("America/Los_Angeles"))),
EndTime: time.Date(2024, 8, 10, 0, 0, 0, 0, utils.Must1(time.LoadLocation("America/Los_Angeles"))),
}
var AllJams = []Jam{WRJ2021, WRJ2022, VJ2023, WRJ2023, LJ2024, VJ2024}
var AllJams = []Jam{WRJ2021, WRJ2022}
func CurrentJam() *Jam {
now := time.Now()
@ -123,17 +75,6 @@ func CurrentJam() *Jam {
return nil
}
func PreviousJam() *Jam {
now := time.Now()
var mostRecent *Jam
for i, jam := range AllJams {
if jam.EndTime.Before(now) {
mostRecent = &AllJams[i]
}
}
return mostRecent
}
func JamBySlug(slug string) Jam {
for _, jam := range AllJams {
if jam.Slug == slug {

View File

@ -23,7 +23,6 @@ type ProjectsQuery struct {
// are generally visible to all users.
Lifecycles []models.ProjectLifecycle // If empty, defaults to visible lifecycles. Do not conflate this with permissions; those are checked separately.
Types ProjectTypeQuery // bitfield
FeaturedOnly bool
IncludeHidden bool
// Ignored when using FetchProject
@ -39,9 +38,8 @@ type ProjectsQuery struct {
type ProjectAndStuff struct {
Project models.Project
LogoLightAsset *models.Asset
LogoDarkAsset *models.Asset
HeaderImage *models.Asset
LogoLightAsset *models.Asset `db:"logolight_asset"`
LogoDarkAsset *models.Asset `db:"logodark_asset"`
Owners []*models.User
Tag *models.Tag
}
@ -74,7 +72,6 @@ func FetchProjects(
Project models.Project `db:"project"`
LogoLightAsset *models.Asset `db:"logolight_asset"`
LogoDarkAsset *models.Asset `db:"logodark_asset"`
HeaderAsset *models.Asset `db:"header_asset"`
Tag *models.Tag `db:"tag"`
}
@ -89,7 +86,6 @@ func FetchProjects(
project
LEFT JOIN asset AS logolight_asset ON logolight_asset.id = project.logolight_asset_id
LEFT JOIN asset AS logodark_asset ON logodark_asset.id = project.logodark_asset_id
LEFT JOIN asset AS header_asset ON header_asset.id = project.header_asset_id
LEFT JOIN tag ON project.tag = tag.id
`)
if len(q.OwnerIDs) > 0 {
@ -134,9 +130,6 @@ func FetchProjects(
}
qb.Add(`)`)
}
if q.FeaturedOnly {
qb.Add(`AND project.featured`)
}
if !q.IncludeHidden {
qb.Add(`AND NOT project.hidden`)
}
@ -226,7 +219,6 @@ func FetchProjects(
Project: p.Project,
LogoLightAsset: p.LogoLightAsset,
LogoDarkAsset: p.LogoDarkAsset,
HeaderImage: p.HeaderAsset,
Owners: owners,
Tag: p.Tag,
})
@ -526,20 +518,3 @@ func SetProjectTag(
return resultTag, nil
}
func UpdateSnippetLastPostedForAllProjects(ctx context.Context, dbConn db.ConnOrTx) error {
_, err := dbConn.Exec(ctx,
`
UPDATE project p SET (snippet_last_posted, all_last_updated) = (
SELECT
COALESCE(MAX(s."when"), 'epoch'),
GREATEST(p.forum_last_updated, p.blog_last_updated, p.annotation_last_updated, MAX(s."when"))
FROM
snippet s
JOIN snippet_project sp ON s.id = sp.snippet_id
WHERE sp.project_id = p.id
)
`,
)
return err
}

View File

@ -16,8 +16,6 @@ type SnippetQuery struct {
Tags []int
DiscordMessageIDs []string
FeaturedOnly bool
Limit, Offset int // if empty, no pagination
}
@ -46,8 +44,6 @@ func FetchSnippets(
}
defer tx.Rollback(ctx)
isFiltering := len(q.IDs) > 0 || len(q.Tags) > 0 || len(q.ProjectIDs) > 0
var tagSnippetIDs []int
if len(q.Tags) > 0 {
// Get snippet IDs with this tag, then use that in the main query
@ -66,6 +62,11 @@ func FetchSnippets(
return nil, oops.New(err, "failed to get snippet IDs for tag")
}
// special early-out: no snippets found for these tags at all
if len(snippetIDs) == 0 {
return nil, nil
}
tagSnippetIDs = snippetIDs
}
@ -86,6 +87,11 @@ func FetchSnippets(
return nil, oops.New(err, "failed to get snippet IDs for tag")
}
// special early-out: no snippets found for these projects at all
if len(snippetIDs) == 0 {
return nil, nil
}
projectSnippetIDs = snippetIDs
}
@ -103,25 +109,18 @@ func FetchSnippets(
TRUE
`,
)
allSnippetIDs := make([]int, 0, len(q.IDs)+len(tagSnippetIDs)+len(projectSnippetIDs))
allSnippetIDs = append(allSnippetIDs, q.IDs...)
allSnippetIDs = append(allSnippetIDs, tagSnippetIDs...)
allSnippetIDs = append(allSnippetIDs, projectSnippetIDs...)
if isFiltering && len(allSnippetIDs) == 0 {
// We already managed to filter out all snippets, and all further
// parts of this query are more filters, so we can just fail everything
// else from right here.
qb.Add(`AND FALSE`)
} else if len(allSnippetIDs) > 0 && len(q.OwnerIDs) > 0 {
qb.Add(`AND (snippet.id = ANY ($?) OR snippet.owner_id = ANY ($?))`, allSnippetIDs, q.OwnerIDs)
} else {
if len(allSnippetIDs) > 0 {
qb.Add(`AND snippet.id = ANY ($?)`, allSnippetIDs)
if len(q.IDs) > 0 {
qb.Add(`AND snippet.id = ANY ($?)`, q.IDs)
}
if len(tagSnippetIDs) > 0 {
qb.Add(`AND snippet.id = ANY ($?)`, tagSnippetIDs)
}
if len(projectSnippetIDs) > 0 {
qb.Add(`AND snippet.id = ANY ($?)`, projectSnippetIDs)
}
if len(q.OwnerIDs) > 0 {
qb.Add(`AND snippet.owner_id = ANY ($?)`, q.OwnerIDs)
}
}
if len(q.DiscordMessageIDs) > 0 {
qb.Add(`AND snippet.discord_message_id = ANY ($?)`, q.DiscordMessageIDs)
}

View File

@ -375,16 +375,12 @@ func FetchPosts(
models.VisibleProjectLifecycles,
models.HMNProjectID,
)
if len(q.ProjectIDs) > 0 && len(q.UserIDs) > 0 {
qb.Add(`AND (project.id = ANY($?) OR post.author_id = ANY($?))`, q.ProjectIDs, q.UserIDs)
} else {
if len(q.ProjectIDs) > 0 {
qb.Add(`AND project.id = ANY ($?)`, q.ProjectIDs)
}
if len(q.UserIDs) > 0 {
qb.Add(`AND post.author_id = ANY ($?)`, q.UserIDs)
}
}
if len(q.ThreadIDs) > 0 {
qb.Add(`AND post.thread_id = ANY ($?)`, q.ThreadIDs)
}

View File

@ -1,327 +0,0 @@
package hmndata
import (
"context"
"strings"
"git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/perf"
)
type TimelineQuery struct {
OwnerIDs []int
ProjectIDs []int
SkipSnippets bool
SkipPosts bool
IncludePostDescription bool
Limit, Offset int
}
type TimelineItemAndStuff struct {
Item models.TimelineItem `db:"item"`
Owner *models.User `db:"owner"`
AvatarAsset *models.Asset `db:"avatar"`
Asset *models.Asset `db:"asset"`
DiscordMessage *models.DiscordMessage `db:"discord_message"`
Projects []*ProjectAndStuff
}
func FetchTimeline(
ctx context.Context,
dbConn db.ConnOrTx,
currentUser *models.User,
q TimelineQuery,
) ([]*TimelineItemAndStuff, error) {
perf := perf.ExtractPerf(ctx)
perf.StartBlock("SQL", "Fetch timeline")
defer perf.EndBlock()
var qb db.QueryBuilder
currentUserId := -1
if currentUser != nil {
currentUserId = currentUser.ID
}
currentUserIsAdmin := false
if currentUser != nil {
currentUserIsAdmin = currentUser.IsStaff
}
itemSelects := []string{}
if !q.SkipSnippets {
itemSelects = append(itemSelects, "SELECT * from snippet_item")
}
if !q.SkipPosts {
itemSelects = append(itemSelects, "SELECT * from post_item")
}
if len(itemSelects) == 0 {
return nil, nil
}
itemSelect := strings.Join(itemSelects, " UNION ALL ")
qb.Add(
`
WITH visible_project AS (
SELECT *
FROM project
WHERE
$? = true
OR project.id = $?
OR (SELECT count(*) > 0 FROM user_project WHERE project_id = project.id AND user_id = $?)
OR (
project.lifecycle = ANY($?)
AND NOT project.hidden
AND (SELECT every(hmn_user.status = $?)
FROM user_project
JOIN hmn_user ON hmn_user.id = user_project.user_id
WHERE user_project.project_id = project.id
)
)
),`,
currentUserIsAdmin,
models.HMNProjectID,
currentUserId,
models.VisibleProjectLifecycles,
models.UserStatusApproved,
)
qb.Add(
`
snippet_item AS (
SELECT id,
"when",
'snippet' AS timeline_type,
owner_id,
'' AS title,
_description_html AS parsed_desc,
description AS raw_desc,
asset_id,
discord_message_id,
url,
0 AS project_id,
0 AS thread_id,
0 AS subforum_id,
0 AS thread_type,
TRUE AS first_post
FROM snippet
WHERE TRUE
`,
)
if len(q.ProjectIDs)+len(q.OwnerIDs) > 0 {
qb.Add(`AND (`)
if len(q.ProjectIDs) > 0 {
qb.Add(
`
(
SELECT count(*)
FROM snippet_project
WHERE
snippet_project.snippet_id = snippet.id
AND
snippet_project.project_id = ANY($?)
) > 0
`,
q.ProjectIDs,
)
} else {
qb.Add("FALSE")
}
qb.Add(" OR ")
if len(q.OwnerIDs) > 0 {
qb.Add(`owner_id = ANY($?)`, q.OwnerIDs)
} else {
qb.Add("FALSE")
}
qb.Add(`)`)
}
qb.Add(
`
),
post_item AS (
SELECT post.id,
postdate AS "when",
'post' AS timeline_type,
author_id AS owner_id,
thread.title AS title,
`,
)
if q.IncludePostDescription {
qb.Add(
`
post_version.text_parsed AS parsed_desc,
post_version.text_raw AS raw_desc,
`,
)
} else {
qb.Add(
`
'' AS parsed_desc,
'' AS raw_desc,
`,
)
}
qb.Add(
`
NULL::uuid AS asset_id,
NULL AS discord_message_id,
NULL AS url,
post.project_id,
thread_id,
subforum_id,
thread_type,
(post.id = thread.first_id) AS first_post
FROM post
JOIN thread ON thread.id = thread_id
JOIN post_version ON post_version.id = current_id
JOIN visible_project ON visible_project.id = post.project_id
WHERE
post.deleted = false AND thread.deleted = false
`,
)
if len(q.OwnerIDs)+len(q.ProjectIDs) > 0 {
qb.Add(`AND (`)
if len(q.ProjectIDs) > 0 {
qb.Add(`post.project_id = ANY($?)`, q.ProjectIDs)
} else {
qb.Add("FALSE")
}
qb.Add(" OR ")
if len(q.OwnerIDs) > 0 {
qb.Add(`post.author_id = ANY($?)`, q.OwnerIDs)
} else {
qb.Add("FALSE")
}
qb.Add(`)`)
}
qb.Add(
`
),
item AS (
`,
)
qb.Add(itemSelect)
qb.Add(
`
)
SELECT $columns FROM item
LEFT JOIN thread ON thread.id = thread_id
LEFT JOIN hmn_user AS owner ON owner_id = owner.id
LEFT JOIN asset AS avatar ON avatar.id = owner.avatar_asset_id
LEFT JOIN asset ON asset_id = asset.id
LEFT JOIN discord_message ON discord_message_id = discord_message.id
WHERE TRUE
`,
)
if currentUser == nil {
qb.Add(
`AND owner.status = $? -- snippet owner is Approved`,
models.UserStatusApproved,
)
} else if !currentUser.IsStaff {
qb.Add(
`
AND (
owner.status = $? -- snippet owner is Approved
OR owner.id = $? -- current user is the snippet owner
)
`,
models.UserStatusApproved,
currentUser.ID,
)
}
qb.Add(
`
ORDER BY "when" DESC
`,
)
if q.Limit > 0 {
qb.Add(`LIMIT $? OFFSET $?`, q.Limit, q.Offset)
}
perf.StartBlock("SQL", "Query timeline")
results, err := db.Query[TimelineItemAndStuff](ctx, dbConn, qb.String(), qb.Args()...)
perf.EndBlock()
if err != nil {
return nil, oops.New(err, "failed to fetch timeline items")
}
for idx := range results {
if results[idx].Owner != nil {
results[idx].Owner.AvatarAsset = results[idx].AvatarAsset
}
}
perf.StartBlock("TIMELINE", "Fixup projects")
defer perf.EndBlock()
projectsSeen := make(map[int]bool)
var projectIds []int
var snippetIds []int
projectTargets := make(map[int][]*TimelineItemAndStuff)
snippetItems := make(map[int]*TimelineItemAndStuff)
for _, r := range results {
if r.Item.ProjectID != 0 {
if _, found := projectsSeen[r.Item.ProjectID]; !found {
projectIds = append(projectIds, r.Item.ProjectID)
projectsSeen[r.Item.ProjectID] = true
}
projectTargets[r.Item.ProjectID] = append(projectTargets[r.Item.ProjectID], r)
}
if r.Item.Type == models.TimelineItemTypeSnippet {
snippetIds = append(snippetIds, r.Item.ID)
snippetItems[r.Item.ID] = r
}
}
type snippetProjectRow struct {
SnippetID int `db:"snippet_id"`
ProjectID int `db:"project_id"`
}
perf.StartBlock("SQL", "Fetch snippet projects")
snippetProjects, err := db.Query[snippetProjectRow](ctx, dbConn,
`
SELECT $columns
FROM snippet_project
WHERE snippet_id = ANY($1)
`,
snippetIds,
)
perf.EndBlock()
if err != nil {
return nil, oops.New(err, "failed to fetch project ids for timeline")
}
for _, sp := range snippetProjects {
if _, found := projectsSeen[sp.ProjectID]; !found {
projectIds = append(projectIds, sp.ProjectID)
projectsSeen[sp.ProjectID] = true
}
projectTargets[sp.ProjectID] = append(projectTargets[sp.ProjectID], snippetItems[sp.SnippetID])
}
projects, err := FetchProjects(ctx, dbConn, currentUser, ProjectsQuery{
ProjectIDs: projectIds,
IncludeHidden: true,
Lifecycles: models.AllProjectLifecycles,
})
if err != nil {
return nil, oops.New(err, "failed to fetch projects for timeline")
}
for pIdx := range projects {
targets := projectTargets[projects[pIdx].Project.ID]
for _, t := range targets {
t.Projects = append(t.Projects, &projects[pIdx])
}
}
return results, nil
}

View File

@ -8,7 +8,6 @@ import (
"git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/perf"
)
const InvalidUserTwitchID = "INVALID_USER"
@ -22,58 +21,27 @@ type TwitchStreamer struct {
var twitchRegex = regexp.MustCompile(`twitch\.tv/(?P<login>[^/]+)$`)
type TwitchStreamersQuery struct {
UserIDs []int
ProjectIDs []int
}
func FetchTwitchStreamers(ctx context.Context, dbConn db.ConnOrTx, q TwitchStreamersQuery) ([]TwitchStreamer, error) {
perf := perf.ExtractPerf(ctx)
perf.StartBlock("SQL", "Fetch twitch streamers")
defer perf.EndBlock()
var qb db.QueryBuilder
qb.Add(
func FetchTwitchStreamers(ctx context.Context, dbConn db.ConnOrTx) ([]TwitchStreamer, error) {
dbStreamers, err := db.Query[models.Link](ctx, dbConn,
`
SELECT $columns{link}
FROM
link
LEFT JOIN hmn_user AS link_owner ON link_owner.id = link.user_id
WHERE
TRUE
`,
)
if len(q.UserIDs) > 0 && len(q.ProjectIDs) > 0 {
qb.Add(
`AND (link.user_id = ANY ($?) OR link.project_id = ANY ($?))`,
q.UserIDs,
q.ProjectIDs,
)
} else {
if len(q.UserIDs) > 0 {
qb.Add(`AND link.user_id = ANY ($?)`, q.UserIDs)
}
if len(q.ProjectIDs) > 0 {
qb.Add(`AND link.project_id = ANY ($?)`, q.ProjectIDs)
}
}
qb.Add(
`
AND url ~* 'twitch\.tv/([^/]+)$'
AND ((link.user_id IS NOT NULL AND link_owner.status = $?) OR (link.project_id IS NOT NULL AND
url ~* 'twitch\.tv/([^/]+)$' AND
((link.user_id IS NOT NULL AND link_owner.status = $1) OR (link.project_id IS NOT NULL AND
(SELECT COUNT(*)
FROM
user_project AS hup
JOIN hmn_user AS project_owner ON project_owner.id = hup.user_id
WHERE
hup.project_id = link.project_id AND
project_owner.status != $?
project_owner.status != $1
) = 0))
`,
models.UserStatusApproved,
models.UserStatusApproved,
)
dbStreamers, err := db.Query[models.Link](ctx, dbConn, qb.String(), qb.Args()...)
if err != nil {
return nil, oops.New(err, "failed to fetch twitch links")
}

View File

@ -40,6 +40,18 @@ func TestHomepage(t *testing.T) {
AssertSubdomain(t, hero.BuildHomepage(), "hero")
}
func TestShowcase(t *testing.T) {
AssertRegexMatch(t, BuildShowcase(), RegexShowcase, nil)
}
func TestStreams(t *testing.T) {
AssertRegexMatch(t, BuildStreams(), RegexStreams, nil)
}
func TestWhenIsIt(t *testing.T) {
AssertRegexMatch(t, BuildWhenIsIt(), RegexWhenIsIt, nil)
}
func TestAtomFeed(t *testing.T) {
AssertRegexMatch(t, BuildAtomFeed(), RegexAtomFeed, nil)
AssertRegexMatch(t, BuildAtomFeedForProjects(), RegexAtomFeed, map[string]string{"feedtype": "projects"})
@ -58,12 +70,8 @@ func TestLoginPage(t *testing.T) {
AssertRegexMatch(t, BuildLoginPage(""), RegexLoginPage, nil)
}
func TestLoginWithDiscord(t *testing.T) {
AssertRegexMatch(t, BuildLoginWithDiscord(""), RegexLoginWithDiscord, nil)
}
func TestLogoutAction(t *testing.T) {
AssertRegexMatch(t, BuildLogoutAction(""), RegexLogout, nil)
AssertRegexMatch(t, BuildLogoutAction(""), RegexLogoutAction, nil)
}
func TestRegister(t *testing.T) {
@ -87,7 +95,6 @@ func TestPasswordReset(t *testing.T) {
func TestStaticPages(t *testing.T) {
AssertRegexMatch(t, BuildManifesto(), RegexManifesto, nil)
AssertRegexMatch(t, BuildAbout(), RegexAbout, nil)
AssertRegexMatch(t, BuildFoundation(), RegexFoundation, nil)
AssertRegexMatch(t, BuildCommunicationGuidelines(), RegexCommunicationGuidelines, nil)
AssertRegexMatch(t, BuildContactPage(), RegexContactPage, nil)
AssertRegexMatch(t, BuildMonthlyUpdatePolicy(), RegexMonthlyUpdatePolicy, nil)
@ -128,7 +135,9 @@ func TestFeed(t *testing.T) {
}
func TestProjectIndex(t *testing.T) {
AssertRegexMatch(t, BuildProjectIndex(), RegexProjectIndex, nil)
AssertRegexMatch(t, BuildProjectIndex(1), RegexProjectIndex, nil)
AssertRegexMatch(t, BuildProjectIndex(2), RegexProjectIndex, map[string]string{"page": "2"})
assert.Panics(t, func() { BuildProjectIndex(0) })
}
func TestProjectNew(t *testing.T) {
@ -320,6 +329,10 @@ func TestAssetUpload(t *testing.T) {
AssertSubdomain(t, hero.BuildAssetUpload(), "hero")
}
func TestProjectCSS(t *testing.T) {
AssertRegexMatch(t, BuildProjectCSS("000000"), RegexProjectCSS, nil)
}
func TestMarkdownWorkerJS(t *testing.T) {
AssertRegexMatch(t, BuildMarkdownWorkerJS(), RegexMarkdownWorkerJS, nil)
}
@ -356,10 +369,6 @@ func TestS3Asset(t *testing.T) {
AssertRegexMatchFull(t, BuildS3Asset("hello"), RegexS3Asset, map[string]string{"key": "hello"})
}
func TestJamsIndex(t *testing.T) {
AssertRegexMatch(t, BuildJamsIndex(), RegexJamsIndex, nil)
}
func TestJamIndex(t *testing.T) {
AssertRegexMatch(t, BuildJamIndex(), RegexJamIndex, nil)
AssertSubdomain(t, BuildJamIndex(), "")
@ -380,72 +389,6 @@ func TestJamFeed2022(t *testing.T) {
AssertSubdomain(t, BuildJamFeed2022(), "")
}
func TestJamIndex2023_Visibility(t *testing.T) {
AssertRegexMatch(t, BuildJamIndex2023_Visibility(), RegexJamIndex2023_Visibility, nil)
AssertSubdomain(t, BuildJamIndex2023_Visibility(), "")
}
func TestJamFeed2023_Visibility(t *testing.T) {
AssertRegexMatch(t, BuildJamFeed2023_Visibility(), RegexJamFeed2023_Visibility, nil)
AssertSubdomain(t, BuildJamFeed2023_Visibility(), "")
}
func TestJamRecap2023_Visibility(t *testing.T) {
AssertRegexMatch(t, BuildJamRecap2023_Visibility(), RegexJamRecap2023_Visibility, nil)
AssertSubdomain(t, BuildJamRecap2023_Visibility(), "")
}
func TestJamIndex2023(t *testing.T) {
AssertRegexMatch(t, BuildJamIndex2023(), RegexJamIndex2023, nil)
AssertSubdomain(t, BuildJamIndex2023(), "")
}
func TestJamFeed2023(t *testing.T) {
AssertRegexMatch(t, BuildJamFeed2023(), RegexJamFeed2023, nil)
AssertSubdomain(t, BuildJamFeed2023(), "")
}
func TestJamIndex2024_Learning(t *testing.T) {
AssertRegexMatch(t, BuildJamIndex2024_Learning(), RegexJamIndex2024_Learning, nil)
AssertSubdomain(t, BuildJamIndex2024_Learning(), "")
}
func TestJamFeed2024_Learning(t *testing.T) {
AssertRegexMatch(t, BuildJamFeed2024_Learning(), RegexJamFeed2024_Learning, nil)
AssertSubdomain(t, BuildJamFeed2024_Learning(), "")
}
func TestTimeMachine(t *testing.T) {
AssertRegexMatch(t, BuildTimeMachine(), RegexTimeMachine, nil)
AssertSubdomain(t, BuildTimeMachine(), "")
}
func TestTimeMachineSubmissions(t *testing.T) {
AssertRegexMatch(t, BuildTimeMachineSubmissions(), RegexTimeMachineSubmissions, nil)
AssertRegexMatch(t, BuildTimeMachineSubmission(123), RegexTimeMachineSubmissions, nil)
AssertSubdomain(t, BuildTimeMachineSubmissions(), "")
}
func TestTimeMachineAtomFeed(t *testing.T) {
AssertRegexMatch(t, BuildTimeMachineAtomFeed(), RegexTimeMachineAtomFeed, nil)
AssertSubdomain(t, BuildTimeMachineAtomFeed(), "")
}
func TestTimeMachineForm(t *testing.T) {
AssertRegexMatch(t, BuildTimeMachineForm(), RegexTimeMachineForm, nil)
AssertSubdomain(t, BuildTimeMachineForm(), "")
}
func TestTimeMachineFormDone(t *testing.T) {
AssertRegexMatch(t, BuildTimeMachineFormDone(), RegexTimeMachineFormDone, nil)
AssertSubdomain(t, BuildTimeMachineFormDone(), "")
}
func TestNewsletterSignup(t *testing.T) {
AssertRegexMatch(t, BuildAPINewsletterSignup(), RegexAPINewsletterSignup, nil)
AssertSubdomain(t, BuildAPINewsletterSignup(), "")
}
func TestProjectNewJam(t *testing.T) {
AssertRegexMatch(t, BuildProjectNewJam(), RegexProjectNew, nil)
AssertSubdomain(t, BuildProjectNewJam(), "")

View File

@ -28,11 +28,25 @@ func (c *UrlContext) BuildHomepage() string {
return c.Url("/", nil)
}
var RegexJamsIndex = regexp.MustCompile("^/jams$")
var RegexShowcase = regexp.MustCompile("^/showcase$")
func BuildJamsIndex() string {
func BuildShowcase() string {
defer CatchPanic()
return Url("/jams", nil)
return Url("/showcase", nil)
}
var RegexStreams = regexp.MustCompile("^/streams$")
func BuildStreams() string {
defer CatchPanic()
return Url("/streams", nil)
}
var RegexWhenIsIt = regexp.MustCompile("^/whenisit$")
func BuildWhenIsIt() string {
defer CatchPanic()
return Url("/whenisit", nil)
}
var RegexJamIndex = regexp.MustCompile("^/jam$")
@ -56,27 +70,6 @@ func BuildJamIndex2022() string {
return Url("/jam/2022", nil)
}
var RegexJamFeed2022 = regexp.MustCompile("^/jam/2022/feed$")
func BuildJamFeed2022() string {
defer CatchPanic()
return Url("/jam/2022/feed", nil)
}
var RegexJamIndex2023 = regexp.MustCompile("^/jam/2023$")
func BuildJamIndex2023() string {
defer CatchPanic()
return Url("/jam/2023", nil)
}
var RegexJamFeed2023 = regexp.MustCompile("^/jam/2023/feed$")
func BuildJamFeed2023() string {
defer CatchPanic()
return Url("/jam/2023/feed", nil)
}
var RegexJamIndex2023_Visibility = regexp.MustCompile("^/jam/visibility-2023$")
func BuildJamIndex2023_Visibility() string {
@ -91,105 +84,11 @@ func BuildJamFeed2023_Visibility() string {
return Url("/jam/visibility-2023/feed", nil)
}
var RegexJamRecap2023_Visibility = regexp.MustCompile("^/jam/visibility-2023/recap$")
var RegexJamFeed2022 = regexp.MustCompile("^/jam/2022/feed$")
func BuildJamRecap2023_Visibility() string {
func BuildJamFeed2022() string {
defer CatchPanic()
return Url("/jam/visibility-2023/recap", nil)
}
var RegexJamIndex2024_Learning = regexp.MustCompile("^/jam/learning-2024$")
func BuildJamIndex2024_Learning() string {
defer CatchPanic()
return Url("/jam/learning-2024", nil)
}
var RegexJamFeed2024_Learning = regexp.MustCompile("^/jam/learning-2024/feed$")
func BuildJamFeed2024_Learning() string {
defer CatchPanic()
return Url("/jam/learning-2024/feed", nil)
}
var RegexJamGuidelines2024_Learning = regexp.MustCompile("^/jam/learning-2024/guidelines$")
func BuildJamGuidelines2024_Learning() string {
defer CatchPanic()
return Url("/jam/learning-2024/guidelines", nil)
}
var RegexJamIndex2024_Visibility = regexp.MustCompile("^/jam/visibility-2024$")
func BuildJamIndex2024_Visibility() string {
defer CatchPanic()
return Url("/jam/visibility-2024", nil)
}
var RegexJamSaveTheDate = regexp.MustCompile("^/jam/upcoming$")
func BuildJamSaveTheDate() string {
defer CatchPanic()
return Url("/jam/upcoming", nil)
}
func BuildJamIndexAny(slug string) string {
defer CatchPanic()
return Url(fmt.Sprintf("/jam/%s", slug), nil)
}
var RegexTimeMachine = regexp.MustCompile("^/timemachine$")
func BuildTimeMachine() string {
defer CatchPanic()
return Url("/timemachine", nil)
}
var RegexTimeMachineSubmissions = regexp.MustCompile("^/timemachine/submissions$")
func BuildTimeMachineSubmissions() string {
defer CatchPanic()
return Url("/timemachine/submissions", nil)
}
func BuildTimeMachineSubmission(id int) string {
defer CatchPanic()
return UrlWithFragment("/timemachine/submissions", nil, strconv.Itoa(id))
}
var RegexTimeMachineAtomFeed = regexp.MustCompile("^/timemachine/submissions/atom$")
func BuildTimeMachineAtomFeed() string {
defer CatchPanic()
return Url("/timemachine/submissions/atom", nil)
}
var RegexTimeMachineForm = regexp.MustCompile("^/timemachine/submit$")
func BuildTimeMachineForm() string {
defer CatchPanic()
return Url("/timemachine/submit", nil)
}
var RegexTimeMachineFormDone = regexp.MustCompile("^/timemachine/thanks$")
func BuildTimeMachineFormDone() string {
defer CatchPanic()
return Url("/timemachine/thanks", nil)
}
var RegexCalendarIndex = regexp.MustCompile("^/calendar$")
func BuildCalendarIndex() string {
defer CatchPanic()
return Url("/calendar", nil)
}
var RegexCalendarICal = regexp.MustCompile("^/Handmade Network.ical$")
func BuildCalendarICal() string {
defer CatchPanic()
return Url("/Handmade Network.ical", nil)
return Url("/jam/2022/feed", nil)
}
// QUESTION(ben): Can we change these routes?
@ -205,21 +104,10 @@ var RegexLoginPage = regexp.MustCompile("^/login$")
func BuildLoginPage(redirectTo string) string {
defer CatchPanic()
var q []Q
if redirectTo != "" {
q = append(q, Q{Name: "redirect", Value: redirectTo})
}
return Url("/login", q)
return Url("/login", []Q{{Name: "redirect", Value: redirectTo}})
}
var RegexLoginWithDiscord = regexp.MustCompile("^/login-with-discord$")
func BuildLoginWithDiscord(redirectTo string) string {
defer CatchPanic()
return Url("/login-with-discord", []Q{{Name: "redirect", Value: redirectTo}})
}
var RegexLogout = regexp.MustCompile("^/logout$")
var RegexLogoutAction = regexp.MustCompile("^/logout$")
func BuildLogoutAction(redir string) string {
defer CatchPanic()
@ -340,6 +228,24 @@ func BuildConferences() string {
return Url("/conferences", nil)
}
/*
* Volunteer/Staff Roles
*/
var RegexStaffRolesIndex = regexp.MustCompile(`^/roles$`)
func BuildStaffRolesIndex() string {
defer CatchPanic()
return Url("/roles", nil)
}
var RegexStaffRole = regexp.MustCompile(`^/roles/(?P<slug>[^/]+)$`)
func BuildStaffRole(slug string) string {
defer CatchPanic()
return Url(fmt.Sprintf("/roles/%s", slug), nil)
}
/*
* User
*/
@ -453,11 +359,18 @@ func BuildAtomFeedForShowcase() string {
* Projects
*/
var RegexProjectIndex = regexp.MustCompile(`^/projects$`)
var RegexProjectIndex = regexp.MustCompile("^/projects(/(?P<page>.+)?)?$")
func BuildProjectIndex() string {
func BuildProjectIndex(page int) string {
defer CatchPanic()
if page < 1 {
panic(oops.New(nil, "page must be >= 1"))
}
if page == 1 {
return Url("/projects", nil)
} else {
return Url(fmt.Sprintf("/projects/%d", page), nil)
}
}
var RegexProjectNew = regexp.MustCompile("^/p/new$")
@ -613,12 +526,6 @@ func BuildEducationRerender() string {
return Url("/education/rerender", nil)
}
/*
* Style test
*/
var RegexStyleTest = regexp.MustCompile(`^/debug/styles$`)
/*
* Forums
*/
@ -892,12 +799,6 @@ func BuildDiscordShowcaseBacklog() string {
return Url("/discord_showcase_backlog", nil)
}
var RegexDiscordBotDebugPage = regexp.MustCompile("^/discord_bot_debug$")
func BuildDiscordBotDebugPage() string {
return Url("/discord_bot_debug", nil)
}
/*
* API
*/
@ -908,12 +809,6 @@ func BuildAPICheckUsername() string {
return Url("/api/check_username", nil)
}
var RegexAPINewsletterSignup = regexp.MustCompile("^/api/newsletter_signup$")
func BuildAPINewsletterSignup() string {
return Url("/api/newsletter_signup", nil)
}
/*
* Twitch stuff
*/
@ -926,28 +821,6 @@ func BuildTwitchEventSubCallback() string {
var RegexTwitchDebugPage = regexp.MustCompile("^/twitch_debug$")
/*
* Following
*/
var RegexFollowingTest = regexp.MustCompile("^/following$")
func BuildFollowingTest() string {
return Url("/following", nil)
}
var RegexFollowUser = regexp.MustCompile("^/follow/user$")
func BuildFollowUser() string {
return Url("/follow/user", nil)
}
var RegexFollowProject = regexp.MustCompile("^/follow/project$")
func BuildFollowProject() string {
return Url("/follow/project", nil)
}
/*
* User assets
*/
@ -963,6 +836,13 @@ func (c *UrlContext) BuildAssetUpload() string {
* Assets
*/
var RegexProjectCSS = regexp.MustCompile("^/assets/project.css$")
func BuildProjectCSS(color string) string {
defer CatchPanic()
return Url("/assets/project.css", []Q{{"color", color}})
}
var RegexMarkdownWorkerJS = regexp.MustCompile("^/assets/markdown_worker.js$")
func BuildMarkdownWorkerJS() string {
@ -978,13 +858,6 @@ func BuildS3Asset(s3key string) string {
return res
}
var RegexEsBuild = regexp.MustCompile("^/esbuild$")
func BuildEsBuild() string {
defer CatchPanic()
return Url("/esbuild", nil)
}
var RegexPublic = regexp.MustCompile("^/public/.+$")
func BuildPublic(filepath string, cachebust bool) string {
@ -1032,12 +905,6 @@ func BuildUserFile(filepath string) string {
return BuildPublic(fmt.Sprintf("media/%s", filepath), false)
}
/*
* Redirects
*/
var RegexUnwind = regexp.MustCompile(`^/unwind$`)
/*
* Other
*/

View File

@ -1,94 +0,0 @@
package links
import "regexp"
//
// This is all in its own package so we can compile it to wasm without building extra junk.
//
// An online site/service for which we recognize the link
type Service struct {
Name string
IconName string
Regex *regexp.Regexp
}
var Services = []Service{
// {
// Name: "itch.io",
// IconName: "itch",
// Regex: regexp.MustCompile(`://(?P<username>[\w-]+)\.itch\.io`),
// },
{
Name: "App Store",
IconName: "app-store",
Regex: regexp.MustCompile(`^https?://apps.apple.com`),
},
{
Name: "Bluesky",
IconName: "bluesky",
Regex: regexp.MustCompile(`^https?://bsky.app/profile/(?P<username>[\w.-]+)$`),
},
{
Name: "Discord",
IconName: "discord",
Regex: regexp.MustCompile(`^https?://discord\.gg`),
},
{
Name: "GitHub",
IconName: "github",
Regex: regexp.MustCompile(`^https?://github\.com/(?P<username>[\w/-]+)`),
},
{
Name: "GitLab",
IconName: "gitlab",
Regex: regexp.MustCompile(`^https?://gitlab\.com/(?P<username>[\w/-]+)`),
},
{
Name: "Google Play",
IconName: "google-play",
Regex: regexp.MustCompile(`^https?://play\.google\.com`),
},
{
Name: "Patreon",
IconName: "patreon",
Regex: regexp.MustCompile(`^https?://patreon\.com/(?P<username>[\w-]+)`),
},
{
Name: "Twitch",
IconName: "twitch",
Regex: regexp.MustCompile(`^https?://twitch\.tv/(?P<username>[\w/-]+)`),
},
{
Name: "Twitter",
IconName: "twitter",
Regex: regexp.MustCompile(`^https?://(twitter|x)\.com/(?P<username>\w+)`),
},
{
Name: "Vimeo",
IconName: "vimeo",
Regex: regexp.MustCompile(`^https?://vimeo\.com/(?P<username>\w+)`),
},
{
Name: "YouTube",
IconName: "youtube",
Regex: regexp.MustCompile(`youtube\.com/(c/)?(?P<username>[@\w/-]+)$`),
},
}
func ParseKnownServicesForUrl(url string) (service Service, username string) {
for _, svc := range Services {
match := svc.Regex.FindStringSubmatch(url)
if match != nil {
username := ""
if idx := svc.Regex.SubexpIndex("username"); idx >= 0 {
username = match[idx]
}
return svc, username
}
}
return Service{
IconName: "website",
}, ""
}

View File

@ -365,20 +365,24 @@ func ResetDB() {
ctx := context.Background()
// Create the HMN database user
{
credentials := append(
[]pgCredentials{
{config.Config.Postgres.User, config.Config.Postgres.Password, false}, // Existing HMN user
{getSystemUsername(), "", true}, // Postgres.app on Mac
},
guessCredentials()...,
)
var superuserConn *pgconn.PgConn
var workingCred pgCredentials
var createUserConn *pgconn.PgConn
var connErrors []error
for _, cred := range credentials {
// NOTE(asaf): We have to use the low-level API of pgconn, because the pgx Exec always wraps the query in a transaction.
var err error
superuserConn, err = connectLowLevel(ctx, cred.User, cred.Password)
createUserConn, err = connectLowLevel(ctx, cred.User, cred.Password)
if err == nil {
workingCred = cred
if cred.SafeToPrint {
fmt.Printf("Connected by guessing username \"%s\" and password \"%s\".\n", cred.User, cred.Password)
}
@ -387,7 +391,7 @@ func ResetDB() {
connErrors = append(connErrors, err)
}
}
if superuserConn == nil {
if createUserConn == nil {
fmt.Println("Failed to connect to the db to reset it.")
fmt.Println("The following errors occurred for each attempted set of credentials:")
for _, err := range connErrors {
@ -408,27 +412,34 @@ func ResetDB() {
fmt.Println("and add the username and password to your config.")
os.Exit(1)
}
defer superuserConn.Close(ctx)
defer createUserConn.Close(ctx)
// Create the HMN user
{
result := superuserConn.ExecParams(ctx, fmt.Sprintf(`
userExists := workingCred.User == config.Config.Postgres.User && workingCred.Password == config.Config.Postgres.Password
if !userExists {
result := createUserConn.ExecParams(ctx, fmt.Sprintf(`
CREATE USER %s WITH
ENCRYPTED PASSWORD '%s'
CREATEDB
`, config.Config.Postgres.User, config.Config.Postgres.Password), nil, nil, nil, nil)
_, err := result.Close()
pgErr, isPgError := err.(*pgconn.PgError)
if err != nil {
if !(isPgError && pgErr.SQLState() == "42710") { // NOTE(ben): 42710 means "duplicate object", i.e. already exists
panic(fmt.Errorf("failed to create HMN user: %w", err))
}
}
}
}
// Connect as the HMN user
conn, err := connectLowLevel(ctx, config.Config.Postgres.User, config.Config.Postgres.Password)
if err != nil {
panic(fmt.Errorf("failed to connect to db: %w", err))
}
// Disconnect all other users
{
result := superuserConn.ExecParams(ctx, fmt.Sprintf(`
result := conn.ExecParams(ctx, fmt.Sprintf(`
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname IN ('%s', 'template1') AND pid <> pg_backend_pid()
@ -438,14 +449,6 @@ func ResetDB() {
panic(fmt.Errorf("failed to disconnect other users: %w", err))
}
}
superuserConn.Close(ctx)
// Connect as the HMN user
conn, err := connectLowLevel(ctx, config.Config.Postgres.User, config.Config.Postgres.Password)
if err != nil {
panic(fmt.Errorf("failed to connect to db: %w", err))
}
defer conn.Close(ctx)
// Drop the database
{

View File

@ -1,53 +0,0 @@
package migrations
import (
"context"
"time"
"git.handmade.network/hmn/hmn/src/migration/types"
"github.com/jackc/pgx/v5"
)
func init() {
registerMigration(AddPendingSignups{})
}
type AddPendingSignups struct{}
func (m AddPendingSignups) Version() types.MigrationVersion {
return types.MigrationVersion(time.Date(2023, 5, 4, 2, 47, 12, 0, time.UTC))
}
func (m AddPendingSignups) Name() string {
return "AddPendingSignups"
}
func (m AddPendingSignups) Description() string {
return "Adds the pending login table"
}
func (m AddPendingSignups) Up(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx,
`
CREATE TABLE pending_login (
id VARCHAR(40) NOT NULL PRIMARY KEY,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
destination_url VARCHAR(999) NOT NULL
)
`,
)
if err != nil {
return err
}
return nil
}
func (m AddPendingSignups) Down(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx, `DROP TABLE pending_login`)
if err != nil {
return err
}
return nil
}

View File

@ -1,47 +0,0 @@
package migrations
import (
"context"
"time"
"git.handmade.network/hmn/hmn/src/migration/types"
"github.com/jackc/pgx/v5"
)
func init() {
registerMigration(AddThumbnailToAsset{})
}
type AddThumbnailToAsset struct{}
func (m AddThumbnailToAsset) Version() types.MigrationVersion {
return types.MigrationVersion(time.Date(2023, 5, 10, 23, 52, 9, 0, time.UTC))
}
func (m AddThumbnailToAsset) Name() string {
return "AddThumbnailToAsset"
}
func (m AddThumbnailToAsset) Description() string {
return "Adds thumbnail S3 key to assets"
}
func (m AddThumbnailToAsset) Up(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx,
`
ALTER TABLE asset
ADD COLUMN thumbnail_s3_key VARCHAR(2000);
`,
)
return err
}
func (m AddThumbnailToAsset) Down(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx,
`
ALTER TABLE asset
DROP COLUMN thumbnail_s3_key;
`,
)
return err
}

View File

@ -1,47 +0,0 @@
package migrations
import (
"context"
"time"
"git.handmade.network/hmn/hmn/src/migration/types"
"github.com/jackc/pgx/v5"
)
func init() {
registerMigration(AddBackfillToDiscordMessage{})
}
type AddBackfillToDiscordMessage struct{}
func (m AddBackfillToDiscordMessage) Version() types.MigrationVersion {
return types.MigrationVersion(time.Date(2024, 3, 28, 18, 41, 7, 0, time.UTC))
}
func (m AddBackfillToDiscordMessage) Name() string {
return "AddBackfillToDiscordMessage"
}
func (m AddBackfillToDiscordMessage) Description() string {
return "Add a backfill flag to discord messages"
}
func (m AddBackfillToDiscordMessage) Up(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx,
`
ALTER TABLE discord_message
ADD COLUMN backfilled BOOLEAN NOT NULL default FALSE;
`,
)
return err
}
func (m AddBackfillToDiscordMessage) Down(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx,
`
ALTER TABLE discord_message
DROP COLUMN backfilled;
`,
)
return err
}

View File

@ -1,47 +0,0 @@
package migrations
import (
"context"
"time"
"git.handmade.network/hmn/hmn/src/migration/types"
"github.com/jackc/pgx/v5"
)
func init() {
registerMigration(newsletter{})
}
type newsletter struct{}
func (m newsletter) Version() types.MigrationVersion {
return types.MigrationVersion(time.Date(2024, 5, 7, 1, 34, 32, 0, time.UTC))
}
func (m newsletter) Name() string {
return "newsletter"
}
func (m newsletter) Description() string {
return "Adds the newsletter signup"
}
func (m newsletter) Up(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx,
`
CREATE TABLE newsletter_emails (
email VARCHAR(255) NOT NULL PRIMARY KEY
);
`,
)
return err
}
func (m newsletter) Down(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx,
`
DROP TABLE newsletter_emails;
`,
)
return err
}

View File

@ -1,47 +0,0 @@
package migrations
import (
"context"
"time"
"git.handmade.network/hmn/hmn/src/migration/types"
"github.com/jackc/pgx/v5"
)
func init() {
registerMigration(AddHeaderImage{})
}
type AddHeaderImage struct{}
func (m AddHeaderImage) Version() types.MigrationVersion {
return types.MigrationVersion(time.Date(2024, 6, 1, 2, 1, 18, 0, time.UTC))
}
func (m AddHeaderImage) Name() string {
return "AddHeaderImage"
}
func (m AddHeaderImage) Description() string {
return "Adds a header image to projects"
}
func (m AddHeaderImage) Up(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx,
`
ALTER TABLE project
ADD COLUMN header_asset_id UUID REFERENCES asset (id) ON DELETE SET NULL;
`,
)
return err
}
func (m AddHeaderImage) Down(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx,
`
ALTER TABLE project
DROP COLUMN header_asset_id;
`,
)
return err
}

View File

@ -1,47 +0,0 @@
package migrations
import (
"context"
"time"
"git.handmade.network/hmn/hmn/src/migration/types"
"github.com/jackc/pgx/v5"
)
func init() {
registerMigration(RemoveDarkTheme{})
}
type RemoveDarkTheme struct{}
func (m RemoveDarkTheme) Version() types.MigrationVersion {
return types.MigrationVersion(time.Date(2024, 6, 18, 2, 25, 36, 0, time.UTC))
}
func (m RemoveDarkTheme) Name() string {
return "RemoveDarkTheme"
}
func (m RemoveDarkTheme) Description() string {
return "Remove the darktheme field from users"
}
func (m RemoveDarkTheme) Up(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx,
`
ALTER TABLE hmn_user
DROP COLUMN darktheme;
`,
)
return err
}
func (m RemoveDarkTheme) Down(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx,
`
ALTER TABLE hmn_user
ADD COLUMN darktheme BOOLEAN NOT NULL DEFAULT FALSE;
`,
)
return err
}

Some files were not shown because too many files have changed in this diff Show More