Compare commits
210 Commits
snippet_ed
...
master
Author | SHA1 | Date |
---|---|---|
Ben Visness | 6dd2a32780 | |
Ben Visness | b681bacc16 | |
Ben Visness | 9ecb65985e | |
Ben Visness | 04db4ad709 | |
Asaf Gartner | 1aab3b47b3 | |
Ben Visness | 23b1b30d22 | |
Ben Visness | ac54793fd0 | |
Ben Visness | 852ff7e53f | |
Ben Visness | 22265c9081 | |
Asaf Gartner | 7fd57f692b | |
Asaf Gartner | 71d236f1c2 | |
Asaf Gartner | 4568def378 | |
Ben Visness | e8201a254e | |
Asaf Gartner | 2065bad860 | |
Ben Visness | 5dd4880d4c | |
Ben Visness | ee491c7696 | |
Asaf Gartner | f085858e9e | |
Asaf Gartner | 639ea17a88 | |
Asaf Gartner | 2eb3288b2a | |
Ben Visness | 27dcdb4704 | |
Ben Visness | 5aa9dde8a1 | |
Asaf Gartner | cd36eb44eb | |
Asaf Gartner | 444f43a195 | |
Ben Visness | 6fba490392 | |
Asaf Gartner | 7800cd9fe1 | |
Ben Visness | 1e5c0c7b42 | |
Ben Visness | 032d6c435e | |
Ben Visness | 16e4b0327f | |
Ben Visness | 97e6c74c52 | |
Ben Visness | c5e458be8c | |
Asaf Gartner | b5d4fe9ba2 | |
Ben Visness | d896298117 | |
Asaf Gartner | 845a2d377c | |
Asaf Gartner | 79dcef9b7f | |
Asaf Gartner | d347b42e44 | |
Asaf Gartner | 8bc4b5a66c | |
Ben Visness | 76be9b668a | |
Ben Visness | 594860a080 | |
Ben Visness | 0276e5228c | |
Asaf Gartner | edeb519ddb | |
Asaf Gartner | 38acf4a904 | |
Ben Visness | c6893f3f3f | |
Ben Visness | 25cc5ef11b | |
Ben Visness | 45b4928d83 | |
Ben Visness | ca46c23d31 | |
Ben Visness | 36753d2a45 | |
Ben Visness | 16020a5b30 | |
Ben Visness | 2e3cb658af | |
Ben Visness | c83458fd30 | |
Ben Visness | a9ef54b98b | |
Ben Visness | b5ef12fa60 | |
Ben Visness | cae9fecc7c | |
Ben Visness | 6616e72ca6 | |
Asaf Gartner | 4533e8ae66 | |
Asaf Gartner | fdc7582701 | |
Ben Visness | 74f438afad | |
Ben Visness | ad62793262 | |
Ben Visness | 922690244d | |
Ben Visness | 368e657a79 | |
Ben Visness | 7b2d016fe2 | |
Ben Visness | 0895660972 | |
Ben Visness | 8aa18901b2 | |
Ben Visness | 5bc118d9e0 | |
Asaf Gartner | 633f8f1007 | |
Ben Visness | 54aa6682b1 | |
Ben Visness | c8808e21bf | |
Ben Visness | 8be575875d | |
Ben Visness | dd6e5e3b66 | |
Ben Visness | 57782aba5f | |
Ben Visness | 1ea9fbefbc | |
Ben Visness | ca28fe8063 | |
Ben Visness | a6caf8e9bd | |
Asaf Gartner | dcdbc67b6c | |
Ben Visness | 2d61286831 | |
Ben Visness | bb31644d6d | |
Ben Visness | f8b0d9ba85 | |
Ben Visness | be888a98f1 | |
Ben Visness | 88323ffbaa | |
Ben Visness | 1166bb6cf3 | |
Ben Visness | 96ea2e0268 | |
Jake Mason | 64d98c424f | |
Jake Mason | 03a08ad392 | |
Ilia Demianenko | 43b9f993dc | |
Jake Mason | a1c5086190 | |
Ben Visness | cace7fbcb1 | |
Ben Visness | 95bd54b39e | |
Ben Visness | cdacc5b3a0 | |
Asaf Gartner | 65aab39432 | |
Ben Visness | 8dc458da73 | |
bvisness | 0210a0784b | |
Ben Visness | 1b1c25da80 | |
Ben Visness | e4dd15d248 | |
Ben Visness | faac05a3a8 | |
Ben Visness | 79c4c14631 | |
Ben Visness | fe1d99b7ab | |
Ben Visness | 23e67638e4 | |
Ben Visness | 89dd2c52c7 | |
Ben Visness | b7d4883c6c | |
Ben Visness | 60a9ece643 | |
Ben Visness | 25f33451ca | |
Asaf Gartner | ef16a4f3e0 | |
Ben Visness | 4651e8a477 | |
Ben Visness | 44e055155e | |
Ben Visness | 2cb367ba18 | |
Ben Visness | fc6b979a46 | |
Ben Visness | 524cf8e27b | |
Asaf Gartner | 3a66b7a77d | |
Asaf Gartner | 4994cb7701 | |
Asaf Gartner | c50d8f8ea4 | |
Jake Mason | 45b5c7f4d4 | |
Jake Mason | f9f0e1b119 | |
Jake Mason | 54c9af9c08 | |
Jake Mason | e616cc51fd | |
Jake Mason | c6cdc8a473 | |
Jake Mason | 20c4a277ff | |
Jake Mason | 0ccf3aba1a | |
Ben Visness | cf345ca592 | |
Ben Visness | 81b92d3aef | |
Ben Visness | 5296e0a15a | |
Ben Visness | 55f56d8ae0 | |
Ben Visness | bb38d0b759 | |
Jake Mason | 7a6cdbad0e | |
Jake Mason | 395ca8e765 | |
Jake Mason | 4bb562823e | |
Jake Mason | 526a588a19 | |
Ben Visness | 6d24738be6 | |
Asaf Gartner | a324af8a0c | |
Asaf Gartner | 859a78c079 | |
Asaf Gartner | aa6428f3a4 | |
Asaf Gartner | 2af28d9f3f | |
Ben Visness | 9e27835e51 | |
Jake Mason | 1825e93b04 | |
Jake Mason | df253d0a33 | |
Jake Mason | ad904de16b | |
Jake Mason | 922d6283f8 | |
Jake Mason | b2f8bb45cf | |
Jake Mason | 299bf5e065 | |
Jake Mason | 1db187e9d3 | |
bvisness | 348feff4cf | |
skytrias | f3453355c4 | |
Asaf Gartner | 620533784a | |
Asaf Gartner | 77ac6d4200 | |
Asaf Gartner | 6d65785cef | |
Ben Visness | cd31085c67 | |
Ben Visness | 9b3a972e37 | |
Asaf Gartner | 587ac7643f | |
Asaf Gartner | badb845ab1 | |
Ben Visness | 5fe3e7560c | |
Ben Visness | 084b13ae34 | |
Ben Visness | f0597f3eb8 | |
Asaf Gartner | bd1edb2077 | |
Ben Visness | 5a983643a3 | |
Ben Visness | b75b59aad5 | |
Ben Visness | 2fbdf19f41 | |
Ben Visness | 48af5e650d | |
Ben Visness | 4e44ba0b45 | |
Ben Visness | 8eee541ccc | |
Asaf Gartner | df194dd041 | |
Asaf Gartner | 3abf4dd21b | |
Asaf Gartner | 851f9e0f67 | |
Asaf Gartner | a6ad01143a | |
Asaf Gartner | d70f7d6b6d | |
Asaf Gartner | 48451a49dd | |
Asaf Gartner | f548495813 | |
Asaf Gartner | 09d875a9f6 | |
Asaf Gartner | d7f1325ace | |
Asaf Gartner | 56929e6e44 | |
Asaf Gartner | cf809a3cdc | |
Asaf Gartner | 0f58cfc2da | |
Asaf Gartner | 5cc920dc2f | |
Ben Visness | 304371a9a9 | |
Ben Visness | 5aa5595115 | |
Ben Visness | 968e859dde | |
Ben Visness | b5dac2f4d8 | |
Ben Visness | 3d7c095c87 | |
ilidemi | 8189151a74 | |
Ben Visness | 0feeaada1b | |
Ben Visness | 8121830561 | |
Asaf Gartner | 4b9fe628e6 | |
Asaf Gartner | ca663d874b | |
Ben Visness | bdb08dd14b | |
Ben Visness | b27ddd1e7f | |
Asaf Gartner | c489d0ffa9 | |
Ben Visness | c9ee420dbb | |
Ben Visness | 045a2c2379 | |
Ben Visness | 168b210c5b | |
Ben Visness | 19d79a2a77 | |
bvisness | d2b34cb87d | |
Asaf Gartner | 42e1ed95fb | |
Ben Visness | 11c4dbe925 | |
Ben Visness | d7b43fdea0 | |
Ben Visness | 782db18cc3 | |
Ben Visness | 81d8d4559f | |
Ben Visness | a0cc2f5c66 | |
Ben Visness | 4f1989f663 | |
giggs | 9e786e132d | |
Ben Visness | bb48a77b9d | |
Asaf Gartner | 319b1a05b9 | |
Asaf Gartner | 822a489c09 | |
Asaf Gartner | b62954ba37 | |
Ben Visness | 7ea11ebd51 | |
Ben Visness | 9f88ce1223 | |
Ben Visness | 4289a1e27a | |
Ben Visness | 48490d83a9 | |
Asaf Gartner | 97ed892ce3 | |
Asaf Gartner | 4c1daae5e1 | |
Asaf Gartner | b995df4533 | |
Asaf Gartner | dc94262bfb | |
Asaf Gartner | 841264de0f | |
Asaf Gartner | d2f44f8e5f |
|
@ -15,3 +15,11 @@ adminmailer/adminmailer
|
|||
local/backups
|
||||
/tmp
|
||||
*.exe
|
||||
.DS_Store
|
||||
__debug_bin*
|
||||
|
||||
# vim session saves
|
||||
Session.vim
|
||||
|
||||
# tags files
|
||||
tags
|
||||
|
|
|
@ -10,10 +10,12 @@ 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.18 or higher: https://go.dev/
|
||||
- Go 1.18 or 1.19: 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`.
|
||||
|
||||
**PLEASE NOTE:** Go 1.20 currently does not work due to a bug in a third-party library. See [this issue](https://git.handmade.network/hmn/hmn/issues/59#issuecomment-1335).
|
||||
|
||||
- Postgres: https://www.postgresql.org/
|
||||
|
||||
Any Postgres installation should work fine, although less common distributions may not work as nicely with our scripts out of the box. On Mac, [Postgres.app](https://postgresapp.com/) is recommended.
|
||||
|
|
|
@ -4,6 +4,7 @@ const RecvAddress = "admin@example.com"
|
|||
const RecvName = "Admin"
|
||||
const FromName = "From Name"
|
||||
const FromAddress = "from@address.com"
|
||||
const FromAddressPassword = "password"
|
||||
const ServerUsername = "username"
|
||||
const ServerPassword = "password"
|
||||
const ServerAddress = "server.address"
|
||||
const ServerPort = 587
|
||||
|
|
|
@ -46,7 +46,7 @@ func sendMail(toAddress, toName, subject, contentHtml string) error {
|
|||
)
|
||||
return smtp.SendMail(
|
||||
fmt.Sprintf("%s:%d", ServerAddress, ServerPort),
|
||||
smtp.PlainAuth("", FromAddress, FromAddressPassword, ServerAddress),
|
||||
smtp.PlainAuth("", ServerUsername, ServerPassword, ServerAddress),
|
||||
FromAddress,
|
||||
[]string{toAddress},
|
||||
contents,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
- [ ] Export with [DiscordChatExporter](https://github.com/Tyrrrz/DiscordChatExporter) CLI 2.34
|
||||
- [ ] Export with [DiscordChatExporter](https://github.com/Tyrrrz/DiscordChatExporter) CLI 2.39.1
|
||||
|
||||
```
|
||||
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
|
||||
- [ ] Fix timestamps, validate they look correct
|
||||
|
||||
```
|
||||
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 whitney from files
|
||||
- [ ] Remove js, css and ggsans from files
|
||||
- [ ] Add content path to `fishbowl.go`
|
||||
- [ ] Test locally
|
||||
- [ ] Submit a pull request
|
||||
|
|
|
@ -4,11 +4,13 @@ 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)
|
||||
}
|
||||
|
||||
|
@ -22,17 +24,19 @@ func main() {
|
|||
|
||||
html := string(htmlBytes)
|
||||
|
||||
regex, err := regexp.Compile(
|
||||
"(<span class=\"chatlog__timestamp\">)(\\d+)-([A-Za-z]+)-(\\d+)( [^<]+</span>)",
|
||||
regex := regexp.MustCompile(
|
||||
`(<span class="?chatlog__timestamp"?><a href=[^>]+>)(\d+)/(\d+)/(\d+)( [^<]+</a></span>)`,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
htmlOut := regex.ReplaceAllString(
|
||||
html,
|
||||
"$1$3 $2, 20$4$5",
|
||||
)
|
||||
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])
|
||||
})
|
||||
|
||||
err = os.WriteFile(htmlOutPath, []byte(htmlOut), 0666)
|
||||
if err != nil {
|
||||
|
|
36
go.mod
|
@ -11,24 +11,25 @@ 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
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/jackc/pgconn v1.8.0
|
||||
github.com/jackc/pgtype v1.6.2
|
||||
github.com/jackc/pgx/v4 v4.10.1
|
||||
github.com/jackc/pgx-zerolog v0.0.0-20220923130014-7856b90a65ae
|
||||
github.com/jackc/pgx/v5 v5.2.0
|
||||
github.com/jpillora/backoff v1.0.0
|
||||
github.com/rs/zerolog v1.26.1
|
||||
github.com/rs/zerolog v1.28.0
|
||||
github.com/spf13/cobra v1.1.3
|
||||
github.com/stretchr/testify v1.7.0
|
||||
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.1
|
||||
github.com/yuin/goldmark v1.4.13
|
||||
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01
|
||||
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e
|
||||
golang.org/x/crypto v0.6.0
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d
|
||||
mvdan.cc/xurls/v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
@ -47,22 +48,23 @@ require (
|
|||
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/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.0.6 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||
github.com/jackc/puddle v1.1.3 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // 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
|
||||
github.com/mitchellh/copystructure v1.1.1 // indirect
|
||||
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
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
github.com/teambition/rrule-go v1.7.2 // indirect
|
||||
go.uber.org/atomic v1.10.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.0-20200313102051-9f266ea9e77c // indirect
|
||||
mvdan.cc/xurls/v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
|
|
185
go.sum
|
@ -15,8 +15,6 @@ 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=
|
||||
|
@ -72,17 +70,13 @@ 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 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||
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.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
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=
|
||||
|
@ -94,6 +88,8 @@ 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/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=
|
||||
|
@ -104,8 +100,6 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
|
|||
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 v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||
github.com/gofrs/uuid v3.2.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=
|
||||
|
@ -163,57 +157,16 @@ 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/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.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=
|
||||
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
|
||||
github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
|
||||
github.com/jackc/pgconn v1.8.0 h1:FmjZ0rOyXTr1wfWs45i4a9vjnjWUAGpMuQLD9OSs+lw=
|
||||
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||
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 h1:JVX6jT/XfzNqIjye4717ITLaNwV9mWbJx0dLCpcRzdA=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||
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/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.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.6 h1:b1105ZGEMFe7aCvrT1Cca3VoVb4ZFMaFJLJcg/3zD+8=
|
||||
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
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/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.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
|
||||
github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=
|
||||
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
|
||||
github.com/jackc/pgtype v1.6.2 h1:b3pDeuhbbzBYcg5kwNmNDun4pFUD/0AAr1kLXZLeNt8=
|
||||
github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
|
||||
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.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
|
||||
github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
|
||||
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
|
||||
github.com/jackc/pgx/v4 v4.10.1 h1:/6Q3ye4myIj6AaplUm+eRcz4OhK9HAvFf4ePsG40LJY=
|
||||
github.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA=
|
||||
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.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.3 h1:JnPg/5Q9xVJGfjsO5CPUOjnJps1JaRUm8I9FXVCFK94=
|
||||
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
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/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/v5 v5.2.0 h1:NdPpngX0Y6z6XDFKqmFQaE+bCtkqzvQIOt1wvBlAqs8=
|
||||
github.com/jackc/pgx/v5 v5.2.0/go.mod h1:Ptn7zmohNsWEsdxRawMzk3gaKma2obW+NWTnKa0S4nk=
|
||||
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=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
|
@ -226,32 +179,24 @@ 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 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
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.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
|
||||
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
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.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
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.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
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=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
|
@ -275,7 +220,6 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9
|
|||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
|
@ -291,26 +235,18 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
|
|||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
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.3.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.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
|
||||
github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
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 v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY=
|
||||
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/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=
|
||||
|
@ -326,52 +262,47 @@ 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 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
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/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=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300 h1:XQdibLKagjdevRB6vAjVY4qbSr8rQ610YzTkWcxzxSI=
|
||||
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.0/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.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.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=
|
||||
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-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e h1:1SzTfNOXwIS2oWiMF+6qu0OUDKb0dauo6MoDUQyu+yU=
|
||||
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
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=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
|
@ -392,7 +323,6 @@ 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.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
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=
|
||||
|
@ -407,10 +337,8 @@ 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-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
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=
|
||||
|
@ -420,6 +348,8 @@ 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.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=
|
||||
|
@ -428,31 +358,26 @@ 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-20190826190057-c7b8b68b1456/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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
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-20220811171246-fbc7d0a398ab/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/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.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
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=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
@ -464,24 +389,15 @@ 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.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
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=
|
||||
|
@ -508,22 +424,21 @@ google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiq
|
|||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
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/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=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
#!/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.")
|
|
@ -1,15 +0,0 @@
|
|||
#!/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."
|
|
@ -0,0 +1 @@
|
|||
boto3
|
After Width: | Height: | Size: 118 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 28 KiB |
|
@ -0,0 +1,24 @@
|
|||
<?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>
|
After Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 103 KiB |
After Width: | Height: | Size: 91 KiB |
After Width: | Height: | Size: 106 KiB |
|
@ -94,7 +94,7 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.fishbowl .chatlog__reference-symbol {
|
||||
.fishbowl .chatlog__reference-symbol, .fishbowl .chatlog__reply-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__reference, .fishbowl .chatlog__reply {
|
||||
display: flex;
|
||||
margin-bottom: 0.15rem;
|
||||
align-items: center;
|
||||
|
@ -133,45 +133,53 @@
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.fishbowl .chatlog__reference-avatar {
|
||||
.fishbowl .chatlog__reference-avatar, .fishbowl .chatlog__reply-avatar {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 0.25rem;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.fishbowl .chatlog__reference-author {
|
||||
.fishbowl .chatlog__reference-author, .fishbowl .chatlog__reply-author {
|
||||
margin-right: 0.3rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.fishbowl .chatlog__reference-content {
|
||||
.fishbowl .chatlog__reference-content, .fishbowl .chatlog__reply-content {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.fishbowl .chatlog__reference-link {
|
||||
.fishbowl .chatlog__reference-link, .fishbowl .chatlog__reply-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fishbowl .chatlog__reference-link * {
|
||||
.fishbowl .chatlog__reference-link *, .fishbowl .chatlog__reply-link * {
|
||||
display: inline;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.fishbowl .chatlog__reference-link .hljs {
|
||||
.fishbowl .chatlog__reference-link .hljs, .fishbowl .chatlog__reply-link .hljs {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.fishbowl .chatlog__reference-link:hover {
|
||||
.fishbowl .chatlog__reference-link .chatlog__markdown-quote, .fishbowl .chatlog__reply-link .chatlog__markdown-quote {
|
||||
display: inline
|
||||
}
|
||||
|
||||
.fishbowl .chatlog__reference-link .chatlog__markdown-pre, .fishbowl .chatlog__reply-link .chatlog__markdown-pre {
|
||||
display: inline
|
||||
}
|
||||
|
||||
.fishbowl .chatlog__reference-link:hover, .fishbowl .chatlog__reply-link:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.fishbowl .chatlog__reference-link:hover *:not(.chatlog__markdown-spoiler) {
|
||||
.fishbowl .chatlog__reference-link:hover *:not(.chatlog__markdown-spoiler), .fishbowl .chatlog__reply-link:hover *:not(.chatlog__markdown-spoiler) {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.fishbowl .chatlog__reference-edited-timestamp {
|
||||
.fishbowl .chatlog__reference-edited-timestamp, .fishbowl .chatlog__reply-edited-timestamp {
|
||||
margin-left: 0.25rem;
|
||||
color: #a3a6aa;
|
||||
font-size: 0.75rem;
|
||||
|
@ -263,8 +271,8 @@
|
|||
}
|
||||
|
||||
.fishbowl .chatlog__attachment-media {
|
||||
max-width: 45vw;
|
||||
max-height: 500px;
|
||||
max-width: 500px;
|
||||
max-height: 400px;
|
||||
vertical-align: top;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
|
|
@ -95,7 +95,7 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.fishbowl .chatlog__reference-symbol {
|
||||
.fishbowl .chatlog__reference-symbol, .fishbowl .chatlog__reply-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__reference, .fishbowl .chatlog__reply {
|
||||
display: flex;
|
||||
margin-bottom: 0.15rem;
|
||||
align-items: center;
|
||||
|
@ -134,45 +134,53 @@
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.fishbowl .chatlog__reference-avatar {
|
||||
.fishbowl .chatlog__reference-avatar, .fishbowl .chatlog__reply-avatar {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 0.25rem;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.fishbowl .chatlog__reference-author {
|
||||
.fishbowl .chatlog__reference-author, .fishbowl .chatlog__reply-author {
|
||||
margin-right: 0.3rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.fishbowl .chatlog__reference-content {
|
||||
.fishbowl .chatlog__reference-content, .fishbowl .chatlog__reply-content {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.fishbowl .chatlog__reference-link {
|
||||
.fishbowl .chatlog__reference-link, .fishbowl .chatlog__reply-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fishbowl .chatlog__reference-link * {
|
||||
.fishbowl .chatlog__reference-link *, .fishbowl .chatlog__reply-link * {
|
||||
display: inline;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.fishbowl .chatlog__reference-link .hljs {
|
||||
.fishbowl .chatlog__reference-link .hljs, .fishbowl .chatlog__reply-link .hljs {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.fishbowl .chatlog__reference-link:hover {
|
||||
.fishbowl .chatlog__reference-link .chatlog__markdown-quote, .fishbowl .chatlog__reply-link .chatlog__markdown-quote {
|
||||
display: inline
|
||||
}
|
||||
|
||||
.fishbowl .chatlog__reference-link .chatlog__markdown-pre, .fishbowl .chatlog__reply-link .chatlog__markdown-pre {
|
||||
display: inline
|
||||
}
|
||||
|
||||
.fishbowl .chatlog__reference-link:hover, .fishbowl .chatlog__reply-link:hover {
|
||||
color: #2f3136;
|
||||
}
|
||||
|
||||
.fishbowl .chatlog__reference-link:hover *:not(.chatlog__markdown-spoiler) {
|
||||
.fishbowl .chatlog__reference-link:hover *:not(.chatlog__markdown-spoiler), .fishbowl .chatlog__reply-link:hover *:not(.chatlog__markdown-spoiler) {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.fishbowl .chatlog__reference-edited-timestamp {
|
||||
.fishbowl .chatlog__reference-edited-timestamp, .fishbowl .chatlog__reply-edited-timestamp {
|
||||
margin-left: 0.25rem;
|
||||
color: #5e6772;
|
||||
font-size: 0.75rem;
|
||||
|
@ -264,8 +272,8 @@
|
|||
}
|
||||
|
||||
.fishbowl .chatlog__attachment-media {
|
||||
max-width: 45vw;
|
||||
max-height: 500px;
|
||||
max-width: 500px;
|
||||
max-height: 400px;
|
||||
vertical-align: top;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
|
|
@ -2,47 +2,18 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
"use strict";
|
||||
|
||||
(() => {
|
||||
// Map multiple JavaScript environments to a single common API,
|
||||
// preferring web standards over Node.js API.
|
||||
//
|
||||
// Environments considered:
|
||||
// - Browsers
|
||||
// - Node.js
|
||||
// - Electron
|
||||
// - Parcel
|
||||
// - Webpack
|
||||
|
||||
if (typeof global !== "undefined") {
|
||||
// global already exists
|
||||
} else if (typeof window !== "undefined") {
|
||||
window.global = window;
|
||||
} else if (typeof self !== "undefined") {
|
||||
self.global = self;
|
||||
} else {
|
||||
throw new Error("cannot export Go (neither global, window nor self is defined)");
|
||||
}
|
||||
|
||||
if (!global.require && typeof require !== "undefined") {
|
||||
global.require = require;
|
||||
}
|
||||
|
||||
if (!global.fs && global.require) {
|
||||
const fs = require("fs");
|
||||
if (typeof fs === "object" && fs !== null && Object.keys(fs).length !== 0) {
|
||||
global.fs = fs;
|
||||
}
|
||||
}
|
||||
|
||||
const enosys = () => {
|
||||
const err = new Error("not implemented");
|
||||
err.code = "ENOSYS";
|
||||
return err;
|
||||
};
|
||||
|
||||
if (!global.fs) {
|
||||
if (!globalThis.fs) {
|
||||
let outputBuf = "";
|
||||
global.fs = {
|
||||
globalThis.fs = {
|
||||
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
|
||||
writeSync(fd, buf) {
|
||||
outputBuf += decoder.decode(buf);
|
||||
|
@ -87,8 +58,8 @@
|
|||
};
|
||||
}
|
||||
|
||||
if (!global.process) {
|
||||
global.process = {
|
||||
if (!globalThis.process) {
|
||||
globalThis.process = {
|
||||
getuid() { return -1; },
|
||||
getgid() { return -1; },
|
||||
geteuid() { return -1; },
|
||||
|
@ -102,47 +73,26 @@
|
|||
}
|
||||
}
|
||||
|
||||
if (!global.crypto && global.require) {
|
||||
const nodeCrypto = require("crypto");
|
||||
global.crypto = {
|
||||
getRandomValues(b) {
|
||||
nodeCrypto.randomFillSync(b);
|
||||
},
|
||||
};
|
||||
}
|
||||
if (!global.crypto) {
|
||||
throw new Error("global.crypto is not available, polyfill required (getRandomValues only)");
|
||||
if (!globalThis.crypto) {
|
||||
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
|
||||
}
|
||||
|
||||
if (!global.performance) {
|
||||
global.performance = {
|
||||
now() {
|
||||
const [sec, nsec] = process.hrtime();
|
||||
return sec * 1000 + nsec / 1000000;
|
||||
},
|
||||
};
|
||||
if (!globalThis.performance) {
|
||||
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
|
||||
}
|
||||
|
||||
if (!global.TextEncoder && global.require) {
|
||||
global.TextEncoder = require("util").TextEncoder;
|
||||
}
|
||||
if (!global.TextEncoder) {
|
||||
throw new Error("global.TextEncoder is not available, polyfill required");
|
||||
if (!globalThis.TextEncoder) {
|
||||
throw new Error("globalThis.TextEncoder is not available, polyfill required");
|
||||
}
|
||||
|
||||
if (!global.TextDecoder && global.require) {
|
||||
global.TextDecoder = require("util").TextDecoder;
|
||||
if (!globalThis.TextDecoder) {
|
||||
throw new Error("globalThis.TextDecoder is not available, polyfill required");
|
||||
}
|
||||
if (!global.TextDecoder) {
|
||||
throw new Error("global.TextDecoder is not available, polyfill required");
|
||||
}
|
||||
|
||||
// End of polyfills for common API.
|
||||
|
||||
const encoder = new TextEncoder("utf-8");
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
|
||||
global.Go = class {
|
||||
globalThis.Go = class {
|
||||
constructor() {
|
||||
this.argv = ["js"];
|
||||
this.env = {};
|
||||
|
@ -296,8 +246,8 @@
|
|||
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
|
||||
},
|
||||
|
||||
// func walltime1() (sec int64, nsec int32)
|
||||
"runtime.walltime1": (sp) => {
|
||||
// func walltime() (sec int64, nsec int32)
|
||||
"runtime.walltime": (sp) => {
|
||||
sp >>>= 0;
|
||||
const msec = (new Date).getTime();
|
||||
setInt64(sp + 8, msec / 1000);
|
||||
|
@ -401,6 +351,7 @@
|
|||
storeValue(sp + 56, result);
|
||||
this.mem.setUint8(sp + 64, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 56, err);
|
||||
this.mem.setUint8(sp + 64, 0);
|
||||
}
|
||||
|
@ -417,6 +368,7 @@
|
|||
storeValue(sp + 40, result);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, err);
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
}
|
||||
|
@ -433,6 +385,7 @@
|
|||
storeValue(sp + 40, result);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, err);
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
}
|
||||
|
@ -514,7 +467,7 @@
|
|||
null,
|
||||
true,
|
||||
false,
|
||||
global,
|
||||
globalThis,
|
||||
this,
|
||||
];
|
||||
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
|
||||
|
@ -523,7 +476,7 @@
|
|||
[null, 2],
|
||||
[true, 3],
|
||||
[false, 4],
|
||||
[global, 5],
|
||||
[globalThis, 5],
|
||||
[this, 6],
|
||||
]);
|
||||
this._idPool = []; // unused ids that have been garbage collected
|
||||
|
@ -564,6 +517,13 @@
|
|||
offset += 8;
|
||||
});
|
||||
|
||||
// The linker guarantees global data starts from at least wasmMinDataAddr.
|
||||
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
|
||||
const wasmMinDataAddr = 4096 + 8192;
|
||||
if (offset >= wasmMinDataAddr) {
|
||||
throw new Error("total length of command line and environment variables exceeds limit");
|
||||
}
|
||||
|
||||
this._inst.exports.run(argc, argv);
|
||||
if (this.exited) {
|
||||
this._resolveExitPromise();
|
||||
|
@ -591,36 +551,4 @@
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
typeof module !== "undefined" &&
|
||||
global.require &&
|
||||
global.require.main === module &&
|
||||
global.process &&
|
||||
global.process.versions &&
|
||||
!global.process.versions.electron
|
||||
) {
|
||||
if (process.argv.length < 3) {
|
||||
console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const go = new Go();
|
||||
go.argv = process.argv.slice(2);
|
||||
go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env);
|
||||
go.exit = process.exit;
|
||||
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
|
||||
process.on("exit", (code) => { // Node.js exits if no event handler is pending
|
||||
if (code === 0 && !go.exited) {
|
||||
// deadlock, make Go print error and stack traces
|
||||
go._pendingEvent = { id: 0 };
|
||||
go._resume();
|
||||
}
|
||||
});
|
||||
return go.run(result.instance);
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -52,19 +52,30 @@ function makeShowcaseItem(timelineItem) {
|
|||
break;
|
||||
case TimelineMediaTypes.VIDEO:
|
||||
addThumbnailFunc = () => {
|
||||
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);
|
||||
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);
|
||||
};
|
||||
|
||||
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.preload = 'metadata';
|
||||
modalVideo.classList.add('mw-100', 'mh-60vh');
|
||||
return modalVideo;
|
||||
};
|
||||
|
@ -110,7 +121,11 @@ function makeShowcaseItem(timelineItem) {
|
|||
}
|
||||
}
|
||||
|
||||
modalEl.discord_link.href = timelineItem.discord_message_url;
|
||||
if (timelineItem.discord_message_url != "") {
|
||||
modalEl.discord_link.href = timelineItem.discord_message_url;
|
||||
} else {
|
||||
modalEl.discord_link.remove();
|
||||
}
|
||||
|
||||
function close() {
|
||||
modalEl.overlay.remove();
|
||||
|
|
|
@ -16,7 +16,7 @@ function readableByteSize(numBytes) {
|
|||
return new Intl.NumberFormat([], { maximumFractionDigits: (scale > 0 ? 2 : 0) }).format(numBytes) + scales[scale];
|
||||
}
|
||||
|
||||
function makeSnippetEdit(ownerName, ownerAvatar, ownerUrl, date, text, attachmentElement, projectIds, snippetId, originalSnippetEl) {
|
||||
function makeSnippetEdit(ownerName, ownerAvatar, ownerUrl, date, text, attachmentElement, projectIds, stickyProjectId, snippetId, originalSnippetEl) {
|
||||
let snippetEdit = snippetEditTemplate();
|
||||
let projectSelector = null;
|
||||
let originalAttachment = null;
|
||||
|
@ -55,15 +55,34 @@ function makeSnippetEdit(ownerName, ownerAvatar, ownerUrl, date, text, attachmen
|
|||
}
|
||||
updateProjectSelector();
|
||||
|
||||
if (originalSnippetEl) {
|
||||
snippetEdit.cancelLink.addEventListener("click", function() {
|
||||
cancel();
|
||||
});
|
||||
} else {
|
||||
snippetEdit.cancelLink.remove();
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
if (originalSnippetEl) {
|
||||
snippetEdit.root.parentElement.insertBefore(originalSnippetEl, snippetEdit.root);
|
||||
}
|
||||
snippetEdit.root.remove();
|
||||
}
|
||||
|
||||
function addProject(proj) {
|
||||
let projEl = snippetEditProjectTemplate();
|
||||
projEl.projectId.value = proj.id;
|
||||
projEl.projectLogo.src = proj.logo;
|
||||
projEl.projectName.textContent = proj.name;
|
||||
projEl.removeButton.addEventListener("click", function(ev) {
|
||||
projEl.root.remove();
|
||||
updateProjectSelector();
|
||||
});
|
||||
if (proj.id == stickyProjectId) {
|
||||
projEl.removeButton.remove();
|
||||
} else {
|
||||
projEl.removeButton.addEventListener("click", function(ev) {
|
||||
projEl.root.remove();
|
||||
updateProjectSelector();
|
||||
});
|
||||
}
|
||||
snippetEdit.projectList.appendChild(projEl.root);
|
||||
}
|
||||
|
||||
|
@ -185,15 +204,15 @@ function makeSnippetEdit(ownerName, ownerAvatar, ownerUrl, date, text, attachmen
|
|||
if (el) {
|
||||
snippetEdit.uploadBox.style.display = "none";
|
||||
snippetEdit.previewBox.style.display = "block";
|
||||
snippetEdit.uploadResetLink.style.display = "none";
|
||||
snippetEdit.uploadResetBox.style.display = "none";
|
||||
snippetEdit.previewContent = emptyElement(snippetEdit.previewContent);
|
||||
snippetEdit.previewContent.appendChild(el);
|
||||
snippetEdit.resetLink.style.display = (!originalAttachment || el == originalAttachment) ? "none" : "inline-block";
|
||||
} else {
|
||||
snippetEdit.uploadBox.style.display = "flex";
|
||||
snippetEdit.uploadBox.style.display = "block";
|
||||
snippetEdit.previewBox.style.display = "none";
|
||||
if (originalAttachment) {
|
||||
snippetEdit.uploadResetLink.style.display = "block";
|
||||
snippetEdit.uploadResetBox.style.display = "block";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -203,7 +222,7 @@ function makeSnippetEdit(ownerName, ownerAvatar, ownerUrl, date, text, attachmen
|
|||
if (snippetEdit.file.files.length > 0 && snippetEdit.file.files[0].size > maxFilesize) {
|
||||
// NOTE(asaf): Writing this out in bytes to make the limit exactly clear to the user.
|
||||
let readableSize = new Intl.NumberFormat([], { useGrouping: "always" }).format(maxFilesize);
|
||||
snippetEdit.errors.textContent = "File is too big! Max filesize is " + readableSize + " bytes";
|
||||
snippetEdit.errors.textContent = "File is too big! Max filesize is " + readableSize + " bytes.";
|
||||
sizeGood = false;
|
||||
} else {
|
||||
snippetEdit.errors.textContent = "";
|
||||
|
@ -260,7 +279,10 @@ function makeSnippetEdit(ownerName, ownerAvatar, ownerUrl, date, text, attachmen
|
|||
|
||||
snippetEdit.root.addEventListener("dragenter", function(ev) {
|
||||
enterCounter++;
|
||||
if (ev.dataTransfer && ev.dataTransfer.files && ev.dataTransfer.files.length > 0) {
|
||||
let droppable = Array.from(ev.dataTransfer.items).some(
|
||||
item => item.kind.toLowerCase() === "file"
|
||||
);
|
||||
if (droppable) {
|
||||
snippetEdit.root.classList.add("drop");
|
||||
}
|
||||
});
|
||||
|
@ -324,12 +346,16 @@ function makeSnippetEdit(ownerName, ownerAvatar, ownerUrl, date, text, attachmen
|
|||
if (originalSnippetEl && (!attachmentChanged && originalText == snippetEdit.text.value.trim() && !projectsChanged)) {
|
||||
// NOTE(asaf): We're in edit mode and nothing changed, so no need to submit to the server.
|
||||
ev.preventDefault();
|
||||
snippetEdit.root.parentElement.insertBefore(originalSnippetEl, snippetEdit.root);
|
||||
snippetEdit.root.remove();
|
||||
cancel();
|
||||
}
|
||||
});
|
||||
|
||||
snippetEdit.deleteButton.addEventListener("click", function(ev) {
|
||||
if (!window.confirm("Are you sure you want to delete this snippet?")) {
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
snippetEdit.file.value = "";
|
||||
});
|
||||
|
||||
|
@ -338,7 +364,7 @@ function makeSnippetEdit(ownerName, ownerAvatar, ownerUrl, date, text, attachmen
|
|||
return snippetEdit;
|
||||
}
|
||||
|
||||
function editTimelineSnippet(timelineItemEl) {
|
||||
function editTimelineSnippet(timelineItemEl, stickyProjectId) {
|
||||
let ownerName = timelineItemEl.querySelector(".user")?.textContent;
|
||||
let ownerUrl = timelineItemEl.querySelector(".user")?.href;
|
||||
let ownerAvatar = timelineItemEl.querySelector(".avatar-icon")?.src;
|
||||
|
@ -353,7 +379,7 @@ function editTimelineSnippet(timelineItemEl) {
|
|||
projectIds.push(projid);
|
||||
}
|
||||
}
|
||||
let snippetEdit = makeSnippetEdit(ownerName, ownerAvatar, ownerUrl, creationDate, rawDesc, attachment, projectIds, timelineItemEl.getAttribute("data-id"), timelineItemEl);
|
||||
let snippetEdit = makeSnippetEdit(ownerName, ownerAvatar, ownerUrl, creationDate, rawDesc, attachment, projectIds, stickyProjectId, timelineItemEl.getAttribute("data-id"), timelineItemEl);
|
||||
timelineItemEl.parentElement.insertBefore(snippetEdit.root, timelineItemEl);
|
||||
timelineItemEl.remove();
|
||||
}
|
||||
|
|
After Width: | Height: | Size: 228 KiB |
After Width: | Height: | Size: 267 KiB |
|
@ -0,0 +1 @@
|
|||
<?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>
|
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 2.8 KiB |
|
@ -0,0 +1 @@
|
|||
<?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>
|
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1 @@
|
|||
<?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>
|
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 872 KiB |
437
public/style.css
|
@ -216,6 +216,8 @@ pre, code, .codeblock {
|
|||
--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;
|
||||
|
@ -270,7 +272,7 @@ pre, code, .codeblock {
|
|||
--landing-search-background-hover: #181818;
|
||||
--editor-toolbar-background: #282828;
|
||||
--editor-toolbar-border-color: #333;
|
||||
--editor-toolbar-button-background: 282828;
|
||||
--editor-toolbar-button-background: #282828;
|
||||
--editor-toolbar-button-background-hover: #333;
|
||||
--editor-toolbar-button-border-color: #333;
|
||||
--post-blockquote-border-color: #555;
|
||||
|
@ -288,6 +290,7 @@ pre, code, .codeblock {
|
|||
--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;
|
||||
|
|
|
@ -234,6 +234,8 @@ pre, code, .codeblock {
|
|||
--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;
|
||||
|
@ -306,6 +308,7 @@ pre, code, .codeblock {
|
|||
--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;
|
||||
|
|
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 52 KiB |
After Width: | Height: | Size: 65 KiB |
After Width: | Height: | Size: 91 KiB |
After Width: | Height: | Size: 239 KiB |
After Width: | Height: | Size: 138 KiB |
After Width: | Height: | Size: 8.7 KiB |
After Width: | Height: | Size: 248 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 423 KiB |
After Width: | Height: | Size: 77 KiB |
|
@ -0,0 +1,49 @@
|
|||
<?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 461 280" 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;">
|
||||
<g id="Layer1">
|
||||
</g>
|
||||
<circle cx="56.5" cy="200.5" r="22.5" style="fill:white;"/>
|
||||
<circle cx="101.5" cy="147.5" r="22.5" style="fill:white;"/>
|
||||
<circle cx="183.5" cy="188.5" r="22.5" style="fill:white;"/>
|
||||
<circle cx="183.5" cy="97.5" r="22.5" style="fill:white;"/>
|
||||
<circle cx="83.5" cy="52.5" r="22.5" style="fill:white;"/>
|
||||
<path d="M83.948,51.442L101.948,146.442" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:6px;"/>
|
||||
<path d="M53.713,198.058L98.713,145.058" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:6px;"/>
|
||||
<path d="M183.59,99.544L103.59,149.544" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:6px;"/>
|
||||
<path d="M181.645,190.677L100.645,149.677" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:6px;"/>
|
||||
<g transform="matrix(-3.82857e-16,1,-1,-3.82857e-16,457,-385)">
|
||||
<path d="M442,41C442,38.24 439.76,36 437,36L426,36C423.24,36 421,38.24 421,41L421,85C421,87.76 423.24,90 426,90L437,90C439.76,90 442,87.76 442,85L442,41Z" style="fill:white;"/>
|
||||
</g>
|
||||
<g transform="matrix(-3.82857e-16,1,-1,-3.82857e-16,426,-296)">
|
||||
<path d="M382,70C382,67.24 379.76,65 377,65L366,65C363.24,65 361,67.24 361,70L361,114C361,116.76 363.24,119 366,119L377,119C379.76,119 382,116.76 382,114L382,70Z" style="fill:white;"/>
|
||||
</g>
|
||||
<g transform="matrix(-3.82857e-16,1,-1,-3.82857e-16,486,-356)">
|
||||
<path d="M442,70C442,67.24 439.76,65 437,65L426,65C423.24,65 421,67.24 421,70L421,159C421,161.76 423.24,164 426,164L437,164C439.76,164 442,161.76 442,159L442,70Z" style="fill:white;"/>
|
||||
</g>
|
||||
<g transform="matrix(-3.82857e-16,1,-1,-3.82857e-16,455,-267)">
|
||||
<path d="M382,99C382,96.24 379.76,94 377,94L366,94C363.24,94 361,96.24 361,99L361,188C361,190.76 363.24,193 366,193L377,193C379.76,193 382,190.76 382,188L382,99Z" style="fill:white;"/>
|
||||
</g>
|
||||
<g transform="matrix(-3.82857e-16,1,-1,-3.82857e-16,515,-327)">
|
||||
<path d="M442,99C442,96.24 439.76,94 437,94L426,94C423.24,94 421,96.24 421,99L421,158C421,160.76 423.24,163 426,163L437,163C439.76,163 442,160.76 442,158L442,99Z" style="fill:white;"/>
|
||||
</g>
|
||||
<g transform="matrix(-3.82857e-16,1,-1,-3.82857e-16,484,-238)">
|
||||
<path d="M382,128C382,125.24 379.76,123 377,123L366,123C363.24,123 361,125.24 361,128L361,187C361,189.76 363.24,192 366,192L377,192C379.76,192 382,189.76 382,187L382,128Z" style="fill:white;"/>
|
||||
</g>
|
||||
<g transform="matrix(-3.82857e-16,1,-1,-3.82857e-16,544,-298)">
|
||||
<path d="M442,128C442,125.24 439.76,123 437,123L426,123C423.24,123 421,125.24 421,128L421,238C421,240.76 423.24,243 426,243L437,243C439.76,243 442,240.76 442,238L442,128Z" style="fill:white;"/>
|
||||
</g>
|
||||
<g transform="matrix(-3.82857e-16,1,-1,-3.82857e-16,513,-209)">
|
||||
<path d="M382,157C382,154.24 379.76,152 377,152L366,152C363.24,152 361,154.24 361,157L361,267C361,269.76 363.24,272 366,272L377,272C379.76,272 382,269.76 382,267L382,157Z" style="fill:white;"/>
|
||||
</g>
|
||||
<g transform="matrix(-3.82857e-16,1,-1,-3.82857e-16,573,-269)">
|
||||
<path d="M442,157C442,154.24 439.76,152 437,152L426,152C423.24,152 421,154.24 421,157L421,187C421,189.76 423.24,192 426,192L437,192C439.76,192 442,189.76 442,187L442,157Z" style="fill:white;"/>
|
||||
</g>
|
||||
<g transform="matrix(-3.82857e-16,1,-1,-3.82857e-16,542,-180)">
|
||||
<path d="M382,186C382,183.24 379.76,181 377,181L366,181C363.24,181 361,183.24 361,186L361,216C361,218.76 363.24,221 366,221L377,221C379.76,221 382,218.76 382,216L382,186Z" style="fill:white;"/>
|
||||
</g>
|
||||
<path d="M461,21.739C461,9.741 451.259,0 439.261,0L21.739,0C9.741,0 0,9.741 0,21.739L0,258.261C0,270.259 9.741,280 21.739,280L439.261,280C451.259,280 461,270.259 461,258.261L461,21.739ZM447,21.739L447,258.261C447,262.532 443.532,266 439.261,266C439.261,266 21.739,266 21.739,266C17.468,266 14,262.532 14,258.261L14,21.739C14,17.468 17.468,14 21.739,14L439.261,14C443.532,14 447,17.468 447,21.739Z" style="fill:white;"/>
|
||||
<g transform="matrix(2.90789,0,0,1.65,-31.7105,-153.4)">
|
||||
<rect x="14" y="236" width="152" height="20" style="fill:white;"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 158 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 302 KiB |
After Width: | Height: | Size: 200 KiB |
After Width: | Height: | Size: 99 KiB |
After Width: | Height: | Size: 71 KiB |
After Width: | Height: | Size: 400 KiB |
After Width: | Height: | Size: 154 KiB |
After Width: | Height: | Size: 76 KiB |
After Width: | Height: | Size: 108 KiB |
After Width: | Height: | Size: 219 KiB |
After Width: | Height: | Size: 258 KiB |
After Width: | Height: | Size: 244 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 2.8 KiB |
|
@ -0,0 +1,36 @@
|
|||
<?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>
|
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 212 KiB |
|
@ -298,6 +298,16 @@ 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!
|
||||
|
||||
|
|
|
@ -1,25 +1,35 @@
|
|||
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/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/v4"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
@ -286,7 +296,7 @@ func init() {
|
|||
var err error
|
||||
switch emailType {
|
||||
case "registration":
|
||||
err = email.SendRegistrationEmail(toAddress, toName, "test_user", "test_token", p)
|
||||
err = email.SendRegistrationEmail(toAddress, toName, "test_user", "test_token", "", p)
|
||||
case "passwordreset":
|
||||
err = email.SendPasswordReset(toAddress, toName, "test_user", "test_token", time.Now().Add(time.Hour*24), p)
|
||||
default:
|
||||
|
@ -509,5 +519,103 @@ func init() {
|
|||
}
|
||||
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)
|
||||
|
||||
addProjectCommands(adminCommand)
|
||||
}
|
||||
|
|
|
@ -6,10 +6,19 @@ 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"
|
||||
|
@ -19,6 +28,7 @@ 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
|
||||
|
@ -118,21 +128,50 @@ 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, filename, size, mime_type, sha1sum, width, height, uploader_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
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)
|
||||
`,
|
||||
id,
|
||||
key,
|
||||
thumbnailKey,
|
||||
filename,
|
||||
len(in.Content),
|
||||
in.ContentType,
|
||||
checksum,
|
||||
in.Width,
|
||||
in.Height,
|
||||
width,
|
||||
height,
|
||||
in.UploaderID,
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -154,3 +193,175 @@ 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
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import (
|
|||
"git.handmade.network/hmn/hmn/src/oops"
|
||||
"git.handmade.network/hmn/hmn/src/utils"
|
||||
|
||||
"github.com/jackc/pgx/v4/pgxpool"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"golang.org/x/crypto/argon2"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
|
|
@ -16,7 +16,7 @@ import (
|
|||
"git.handmade.network/hmn/hmn/src/models"
|
||||
"git.handmade.network/hmn/hmn/src/oops"
|
||||
"git.handmade.network/hmn/hmn/src/utils"
|
||||
"github.com/jackc/pgx/v4/pgxpool"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
const SessionCookieName = "HMNSession"
|
||||
|
@ -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,7 +47,16 @@ 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", id)
|
||||
sess, err := db.QueryOne[models.Session](ctx, conn,
|
||||
`
|
||||
SELECT $columns
|
||||
FROM session
|
||||
WHERE
|
||||
id = $1
|
||||
AND expires_at > CURRENT_TIMESTAMP
|
||||
`,
|
||||
id,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.NotFound) {
|
||||
return nil, ErrNoSession
|
||||
|
@ -61,7 +70,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(),
|
||||
|
@ -134,7 +143,16 @@ func DeleteExpiredSessions(ctx context.Context, conn *pgxpool.Pool) (int64, erro
|
|||
return tag.RowsAffected(), nil
|
||||
}
|
||||
|
||||
func PeriodicallyDeleteExpiredSessions(ctx context.Context, conn *pgxpool.Pool) jobs.Job {
|
||||
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 {
|
||||
job := jobs.New()
|
||||
go func() {
|
||||
defer job.Done()
|
||||
|
@ -145,6 +163,7 @@ func PeriodicallyDeleteExpiredSessions(ctx context.Context, conn *pgxpool.Pool)
|
|||
case <-t.C:
|
||||
err := func() (err error) {
|
||||
defer utils.RecoverPanicAsError(&err)
|
||||
|
||||
n, err := DeleteExpiredSessions(ctx, conn)
|
||||
if err == nil {
|
||||
if n > 0 {
|
||||
|
@ -153,10 +172,20 @@ func PeriodicallyDeleteExpiredSessions(ctx context.Context, conn *pgxpool.Pool)
|
|||
} 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 PeriodicallyDeleteExpiredSessions")
|
||||
logging.Error().Err(err).Msg("Panicked in PeriodicallyDeleteExpiredStuff")
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
|
|
@ -0,0 +1,383 @@
|
|||
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(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)
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
//go:build !js
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/jackc/pgx/v4"
|
||||
"github.com/jackc/pgx/v5/tracelog"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
|
@ -12,12 +14,13 @@ var Config = HMNConfig{
|
|||
BaseUrl: "http://handmade.local:9001",
|
||||
LogLevel: zerolog.TraceLevel, // InfoLevel is recommended for production
|
||||
Postgres: PostgresConfig{
|
||||
// the "db seed" command depends on this user existing in Postgres, so you'll want to make sure it exists when running locally.
|
||||
User: "hmn",
|
||||
Password: "password",
|
||||
Hostname: "localhost",
|
||||
Port: 5432,
|
||||
DbName: "hmn",
|
||||
LogLevel: pgx.LogLevelTrace, // LogLevelWarn is recommended for production
|
||||
LogLevel: tracelog.LogLevelError, // LogLevelWarn is recommended for production
|
||||
MinConn: 2, // Keep these low for dev, high for production
|
||||
MaxConn: 10,
|
||||
},
|
||||
|
@ -33,8 +36,9 @@ var Config = HMNConfig{
|
|||
ServerAddress: "smtp.example.com",
|
||||
ServerPort: 587,
|
||||
FromAddress: "noreply@example.com",
|
||||
FromAddressPassword: "",
|
||||
FromName: "Handmade Network Team",
|
||||
MailerUsername: "actualuser@example.com",
|
||||
MailerPassword: "",
|
||||
|
||||
ForceToAddress: "localdev@example.com", // NOTE(asaf): If this is not empty, all emails will be sent to this address.
|
||||
},
|
||||
|
@ -74,8 +78,17 @@ 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
|
||||
},
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
//go:build js
|
||||
|
||||
package config
|
||||
|
||||
var Config = HMNConfig{
|
||||
BaseUrl: "https://handmade.network",
|
||||
}
|
|
@ -3,7 +3,7 @@ package config
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v4"
|
||||
"github.com/jackc/pgx/v5/tracelog"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
|
@ -16,19 +16,23 @@ const (
|
|||
)
|
||||
|
||||
type HMNConfig struct {
|
||||
Env Environment
|
||||
Addr string
|
||||
PrivateAddr string
|
||||
BaseUrl string
|
||||
LogLevel zerolog.Level
|
||||
Postgres PostgresConfig
|
||||
Auth AuthConfig
|
||||
Admin AdminConfig
|
||||
Email EmailConfig
|
||||
DigitalOcean DigitalOceanConfig
|
||||
Discord DiscordConfig
|
||||
Twitch TwitchConfig
|
||||
EpisodeGuide EpisodeGuide
|
||||
Env Environment
|
||||
Addr string
|
||||
PrivateAddr string
|
||||
BaseUrl string
|
||||
LogLevel zerolog.Level
|
||||
Postgres PostgresConfig
|
||||
Auth AuthConfig
|
||||
Admin AdminConfig
|
||||
Email EmailConfig
|
||||
DigitalOcean DigitalOceanConfig
|
||||
Discord DiscordConfig
|
||||
Twitch TwitchConfig
|
||||
Matrix MatrixConfig
|
||||
EpisodeGuide EpisodeGuide
|
||||
DevConfig DevConfig
|
||||
PreviewGeneration PreviewGenerationConfig
|
||||
Calendars []CalendarSource
|
||||
}
|
||||
|
||||
type PostgresConfig struct {
|
||||
|
@ -37,7 +41,7 @@ type PostgresConfig struct {
|
|||
Hostname string
|
||||
Port int
|
||||
DbName string
|
||||
LogLevel pgx.LogLevel
|
||||
LogLevel tracelog.LogLevel
|
||||
MinConn int32
|
||||
MaxConn int32
|
||||
}
|
||||
|
@ -60,12 +64,13 @@ type DigitalOceanConfig struct {
|
|||
}
|
||||
|
||||
type EmailConfig struct {
|
||||
ServerAddress string
|
||||
ServerPort int
|
||||
FromAddress string
|
||||
FromAddressPassword string
|
||||
FromName string
|
||||
ForceToAddress string
|
||||
ServerAddress string
|
||||
ServerPort int
|
||||
FromAddress string
|
||||
MailerUsername string
|
||||
MailerPassword string
|
||||
FromName string
|
||||
ForceToAddress string
|
||||
}
|
||||
|
||||
type DiscordConfig struct {
|
||||
|
@ -90,6 +95,18 @@ 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
|
||||
|
@ -100,6 +117,15 @@ 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
|
||||
}
|
||||
|
||||
func init() {
|
||||
if Config.EpisodeGuide.Projects == nil {
|
||||
Config.EpisodeGuide.Projects = make(map[string]string)
|
||||
|
|
59
src/db/db.go
|
@ -13,11 +13,12 @@ import (
|
|||
"git.handmade.network/hmn/hmn/src/oops"
|
||||
"git.handmade.network/hmn/hmn/src/utils"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgconn"
|
||||
"github.com/jackc/pgtype"
|
||||
"github.com/jackc/pgx/v4"
|
||||
"github.com/jackc/pgx/v4/log/zerologadapter"
|
||||
"github.com/jackc/pgx/v4/pgxpool"
|
||||
zerologadapter "github.com/jackc/pgx-zerolog"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/jackc/pgx/v5/tracelog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
|
@ -42,7 +43,12 @@ type ConnOrTx interface {
|
|||
Begin(ctx context.Context) (pgx.Tx, error)
|
||||
}
|
||||
|
||||
var connInfo = pgtype.NewConnInfo()
|
||||
var pgTypeMap = pgtype.NewMap()
|
||||
|
||||
func init() {
|
||||
// NOTE(asaf): Need to initialize it here to avoid potential race conditions later
|
||||
pgTypeMap.TypeForValue(nil)
|
||||
}
|
||||
|
||||
// Creates a new connection to the HMN database.
|
||||
// This connection is not safe for concurrent use.
|
||||
|
@ -55,8 +61,10 @@ func NewConnWithConfig(cfg config.PostgresConfig) *pgx.Conn {
|
|||
|
||||
pgcfg, err := pgx.ParseConfig(cfg.DSN())
|
||||
|
||||
pgcfg.Logger = zerologadapter.NewLogger(log.Logger)
|
||||
pgcfg.LogLevel = cfg.LogLevel
|
||||
pgcfg.Tracer = &tracelog.TraceLog{
|
||||
zerologadapter.NewLogger(log.Logger),
|
||||
cfg.LogLevel,
|
||||
}
|
||||
|
||||
conn, err := pgx.ConnectConfig(context.Background(), pgcfg)
|
||||
if err != nil {
|
||||
|
@ -79,10 +87,12 @@ func NewConnPoolWithConfig(cfg config.PostgresConfig) *pgxpool.Pool {
|
|||
|
||||
pgcfg.MinConns = cfg.MinConn
|
||||
pgcfg.MaxConns = cfg.MaxConn
|
||||
pgcfg.ConnConfig.Logger = zerologadapter.NewLogger(log.Logger)
|
||||
pgcfg.ConnConfig.LogLevel = cfg.LogLevel
|
||||
pgcfg.ConnConfig.Tracer = &tracelog.TraceLog{
|
||||
zerologadapter.NewLogger(log.Logger),
|
||||
cfg.LogLevel,
|
||||
}
|
||||
|
||||
conn, err := pgxpool.ConnectConfig(context.Background(), pgcfg)
|
||||
conn, err := pgxpool.NewWithConfig(context.Background(), pgcfg)
|
||||
if err != nil {
|
||||
panic(oops.New(err, "failed to create database connection pool"))
|
||||
}
|
||||
|
@ -158,7 +168,11 @@ func QueryOne[T any](
|
|||
|
||||
result, hasRow := rows.Next()
|
||||
if !hasRow {
|
||||
return nil, NotFound
|
||||
if readErr := rows.Err(); readErr != nil {
|
||||
return nil, readErr
|
||||
} else {
|
||||
return nil, NotFound
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
@ -244,7 +258,11 @@ func QueryOneScalar[T any](
|
|||
result, hasRow := rows.Next()
|
||||
if !hasRow {
|
||||
var zero T
|
||||
return zero, NotFound
|
||||
if readErr := rows.Err(); readErr != nil {
|
||||
return zero, readErr
|
||||
} else {
|
||||
return zero, NotFound
|
||||
}
|
||||
}
|
||||
|
||||
return *result, nil
|
||||
|
@ -469,7 +487,7 @@ primitive types and not structs, since the database only returns individual prim
|
|||
and it is our job to stitch them back together into structs later.
|
||||
*/
|
||||
func typeIsQueryable(t reflect.Type) bool {
|
||||
_, isRecognizedByPgtype := connInfo.DataTypeForValue(reflect.New(t).Elem().Interface()) // if pgtype recognizes it, we don't need to dig in further for more `db` tags
|
||||
_, isRecognizedByPgtype := pgTypeMap.TypeForValue(reflect.New(t).Elem().Interface()) // if pgtype recognizes it, we don't need to dig in further for more `db` tags
|
||||
// NOTE: boy it would be nice if we didn't have to do reflect.New here, considering that pgtype is just doing reflection on the value anyway
|
||||
|
||||
if isRecognizedByPgtype {
|
||||
|
@ -585,6 +603,10 @@ func (it *Iterator[T]) Next() (*T, bool) {
|
|||
}
|
||||
}
|
||||
|
||||
func (it *Iterator[T]) Err() error {
|
||||
return it.rows.Err()
|
||||
}
|
||||
|
||||
// Takes a value from a database query (reflected) and assigns it to the
|
||||
// destination. If the destination is a pointer, and the value is non-nil, it
|
||||
// will initialize the destination before assigning.
|
||||
|
@ -606,6 +628,15 @@ func setValueFromDB(dest reflect.Value, value reflect.Value) {
|
|||
dest.SetInt(value.Int())
|
||||
case reflect.String:
|
||||
dest.SetString(value.String())
|
||||
case reflect.Slice:
|
||||
valLen := value.Len()
|
||||
if valLen > 0 {
|
||||
destTemp := reflect.MakeSlice(dest.Type(), 0, valLen)
|
||||
for i := 0; i < valLen; i++ {
|
||||
destTemp = reflect.Append(destTemp, value.Index(i).Elem())
|
||||
}
|
||||
dest.Set(destTemp)
|
||||
}
|
||||
// TODO(ben): More kinds? All the kinds? It kind of feels like we should be able to assign to any destination whose underlying type is a primitive.
|
||||
default:
|
||||
dest.Set(value)
|
||||
|
|
|
@ -17,6 +17,9 @@ import (
|
|||
const SlashCommandProfile = "profile"
|
||||
const ProfileOptionUser = "user"
|
||||
|
||||
const SlashCommandWishlist = "wishlist"
|
||||
const SlashCommandManifesto = "manifesto"
|
||||
|
||||
// User command names
|
||||
const UserCommandProfile = "HMN Profile"
|
||||
|
||||
|
@ -46,6 +49,18 @@ func (bot *botInstance) createApplicationCommands(ctx context.Context) {
|
|||
Type: ApplicationCommandTypeUser,
|
||||
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,
|
||||
Description: "Read the Handmade manifesto",
|
||||
}))
|
||||
}
|
||||
|
||||
func (bot *botInstance) doInteraction(ctx context.Context, i *Interaction) {
|
||||
|
@ -68,6 +83,28 @@ 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,
|
||||
Data: &InteractionCallbackData{
|
||||
Content: "Read the Handmade manifesto at https://handmade.network/manifesto",
|
||||
Flags: FlagEphemeral,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
logging.ExtractLogger(ctx).Error().Err(err).Msg("failed to send manifesto response")
|
||||
}
|
||||
default:
|
||||
logging.ExtractLogger(ctx).Warn().Str("name", i.Data.Name).Msg("didn't recognize Discord interaction name")
|
||||
}
|
||||
|
|
|
@ -19,10 +19,38 @@ import (
|
|||
"git.handmade.network/hmn/hmn/src/oops"
|
||||
"git.handmade.network/hmn/hmn/src/utils"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/jackc/pgx/v4/pgxpool"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"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)
|
||||
|
@ -56,6 +84,11 @@ 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().
|
||||
|
@ -101,6 +134,8 @@ type botInstance struct {
|
|||
conn *websocket.Conn
|
||||
dbConn *pgxpool.Pool
|
||||
|
||||
resuming bool
|
||||
|
||||
heartbeatIntervalMs int
|
||||
forceHeartbeat chan struct{}
|
||||
|
||||
|
@ -193,6 +228,7 @@ 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`)
|
||||
|
@ -211,10 +247,10 @@ The connection process in short:
|
|||
- Gateway sends Ready, client is now connected to gateway
|
||||
|
||||
Or, if we have an existing session:
|
||||
- Gateway sends Hello, asking the client to heartbeat on some interval
|
||||
- Client sends Resume and starts heartbeat process
|
||||
- Gateway sends all missed events followed by a RESUMED event, or an Invalid Session if the
|
||||
session is ded
|
||||
- Gateway sends Hello, asking the client to heartbeat on some interval
|
||||
- Client sends Resume and starts heartbeat process
|
||||
- Gateway sends all missed events followed by a RESUMED event, or an Invalid Session if the
|
||||
session is ded
|
||||
|
||||
Note that some events probably won't be received until the Guild Create message is received.
|
||||
|
||||
|
@ -264,8 +300,11 @@ 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{
|
||||
|
@ -540,11 +579,20 @@ 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, "")
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
"git.handmade.network/hmn/hmn/src/logging"
|
||||
"git.handmade.network/hmn/hmn/src/models"
|
||||
"git.handmade.network/hmn/hmn/src/utils"
|
||||
"github.com/jackc/pgx/v4/pgxpool"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
func RunHistoryWatcher(ctx context.Context, dbConn *pgxpool.Pool) jobs.Job {
|
||||
|
@ -185,6 +185,7 @@ 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 {
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -19,9 +23,15 @@ 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
|
||||
|
@ -36,6 +46,10 @@ 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)
|
||||
}
|
||||
|
@ -44,6 +58,11 @@ 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
|
||||
}
|
||||
|
||||
|
@ -56,17 +75,7 @@ func CleanUpShowcase(ctx context.Context, dbConn db.ConnOrTx, msg *Message) (boo
|
|||
return deleted, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
if !messageShouldBeStored(msg) {
|
||||
err := DeleteMessage(ctx, msg.ChannelID, msg.ID)
|
||||
if err != nil {
|
||||
return deleted, oops.New(err, "failed to delete message")
|
||||
|
@ -82,7 +91,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. Discuss showcase content in #projects.",
|
||||
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.",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -137,8 +146,163 @@ 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 msg.ChannelID == config.Config.Discord.ShowcaseChannelID {
|
||||
if messageShouldBeStored(msg) {
|
||||
err := InternMessage(ctx, dbConn, msg)
|
||||
if errors.Is(err, errNotEnoughInfo) {
|
||||
logging.ExtractLogger(ctx).Warn().
|
||||
|
@ -190,8 +354,8 @@ func InternMessage(
|
|||
|
||||
_, err = dbConn.Exec(ctx,
|
||||
`
|
||||
INSERT INTO discord_message (id, channel_id, guild_id, url, user_id, sent_at, snippet_created)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
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)
|
||||
`,
|
||||
msg.ID,
|
||||
msg.ChannelID,
|
||||
|
@ -200,6 +364,7 @@ func InternMessage(
|
|||
msg.Author.ID,
|
||||
msg.Time(),
|
||||
false,
|
||||
msg.Backfilled,
|
||||
)
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to save new discord message")
|
||||
|
@ -241,7 +406,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, deleted bool, createSnippet bool) error {
|
||||
func HandleInternedMessage(ctx context.Context, dbConn db.ConnOrTx, msg *Message, removeInternedMessage bool, createSnippet bool) error {
|
||||
tx, err := dbConn.Begin(ctx)
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to start transaction")
|
||||
|
@ -252,7 +417,11 @@ 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 !deleted {
|
||||
if !removeInternedMessage {
|
||||
removeInternedMessage = !messageShouldBeStored(msg)
|
||||
}
|
||||
|
||||
if !removeInternedMessage {
|
||||
err = SaveMessageContents(ctx, tx, interned, msg)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -431,7 +600,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")
|
||||
|
@ -491,7 +660,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")
|
||||
}
|
||||
|
@ -561,7 +730,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) {
|
||||
|
@ -685,7 +854,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 blank content
|
||||
// NOTE(asaf): Messages that only have an attachment also have a content struct with an empty content string
|
||||
// TODO(asaf): Do we need to delete existing snippets in this case??? Not entirely sure how to trigger this through discord
|
||||
return nil
|
||||
}
|
||||
|
@ -735,43 +904,42 @@ 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)
|
||||
|
||||
_, err = tx.Exec(ctx,
|
||||
`
|
||||
INSERT INTO snippet (url, "when", description, _description_html, asset_id, discord_message_id, owner_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
`,
|
||||
url,
|
||||
interned.Message.SentAt,
|
||||
contentMarkdown,
|
||||
contentHTML,
|
||||
assetId,
|
||||
interned.Message.ID,
|
||||
interned.HMNUser.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to create snippet from attachment")
|
||||
}
|
||||
contentMarkdown := interned.MessageContent.LastContent
|
||||
contentHTML := parsing.ParseMarkdown(contentMarkdown, parsing.DiscordMarkdown)
|
||||
|
||||
existingSnippet, err = FetchSnippetForMessage(ctx, tx, interned.Message.ID)
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to fetch newly-created snippet")
|
||||
}
|
||||
_, err = tx.Exec(ctx,
|
||||
`
|
||||
INSERT INTO snippet (url, "when", description, _description_html, asset_id, discord_message_id, owner_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
`,
|
||||
url,
|
||||
interned.Message.SentAt,
|
||||
contentMarkdown,
|
||||
contentHTML,
|
||||
assetId,
|
||||
interned.Message.ID,
|
||||
interned.HMNUser.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to create snippet from attachment")
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx,
|
||||
`
|
||||
UPDATE discord_message
|
||||
SET snippet_created = TRUE
|
||||
WHERE id = $1
|
||||
`,
|
||||
interned.Message.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to mark message as having snippet")
|
||||
}
|
||||
existingSnippet, err = FetchSnippetForMessage(ctx, tx, interned.Message.ID)
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to fetch newly-created snippet")
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx,
|
||||
`
|
||||
UPDATE discord_message
|
||||
SET snippet_created = TRUE
|
||||
WHERE id = $1
|
||||
`,
|
||||
interned.Message.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to mark message as having snippet")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -838,7 +1006,8 @@ 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.
|
||||
//
|
||||
// Probably need to add word boundaries.
|
||||
var REDiscordTag = regexp.MustCompile(`&([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)`)
|
||||
|
||||
func getDiscordTags(content string) []string {
|
||||
|
@ -912,3 +1081,36 @@ 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
|
||||
}
|
||||
|
|
|
@ -244,20 +244,38 @@ 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"`
|
||||
ChannelID string `json:"channel_id"`
|
||||
GuildID *string `json:"guild_id"`
|
||||
Content string `json:"content"`
|
||||
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"`
|
||||
ID string `json:"id"`
|
||||
ChannelID string `json:"channel_id"`
|
||||
GuildID *string `json:"guild_id"`
|
||||
Content string `json:"content"`
|
||||
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 {
|
||||
|
@ -317,6 +335,7 @@ 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,
|
||||
}
|
||||
|
@ -346,6 +365,7 @@ 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 {
|
||||
|
@ -387,8 +407,9 @@ func GuildFromMap(m interface{}, k string) *Guild {
|
|||
|
||||
// https://discord.com/developers/docs/resources/guild#guild-member-object
|
||||
type GuildMember struct {
|
||||
User *User `json:"user"`
|
||||
Nick *string `json:"nick"`
|
||||
User *User `json:"user"`
|
||||
Nick *string `json:"nick"`
|
||||
Avatar *string `json:"avatar"`
|
||||
// more fields not yet handled here
|
||||
}
|
||||
|
||||
|
@ -409,8 +430,9 @@ func GuildMemberFromMap(m interface{}, k string) *GuildMember {
|
|||
}
|
||||
|
||||
gm := &GuildMember{
|
||||
User: UserFromMap(m, "user"),
|
||||
Nick: maybeStringP(mmap, "nick"),
|
||||
User: UserFromMap(m, "user"),
|
||||
Nick: maybeStringP(mmap, "nick"),
|
||||
Avatar: maybeStringP(mmap, "avatar"),
|
||||
}
|
||||
|
||||
return gm
|
||||
|
@ -1000,3 +1022,11 @@ 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)
|
||||
}
|
||||
|
|
|
@ -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{}, 100), // presumably this is big enough to handle bursts
|
||||
requests: make(chan struct{}, 200), // presumably this is big enough to handle bursts
|
||||
refills: make(chan rateLimiterRefill),
|
||||
})
|
||||
if !loaded {
|
||||
|
@ -124,7 +124,9 @@ func createLimiter(headers rateLimitHeaders, routeName string) {
|
|||
select {
|
||||
case limiter.requests <- struct{}{}:
|
||||
default:
|
||||
log.Warn().Msg("rate limiting channel was too small; you should increase the default capacity")
|
||||
log.Warn().
|
||||
Int("remaining", headers.Remaining).
|
||||
Msg("rate limiting channel was too small; you should increase the default capacity")
|
||||
break prefillloop
|
||||
}
|
||||
}
|
||||
|
@ -158,7 +160,9 @@ func createLimiter(headers rateLimitHeaders, routeName string) {
|
|||
select {
|
||||
case limiter.requests <- struct{}{}:
|
||||
default:
|
||||
log.Warn().Msg("rate limiting channel was too small; you should increase the default capacity")
|
||||
log.Warn().
|
||||
Int("maxRequests", refill.maxRequests).
|
||||
Msg("rate limiting channel was too small; you should increase the default capacity")
|
||||
break refillloop
|
||||
}
|
||||
}
|
||||
|
|
|
@ -652,11 +652,17 @@ func EditOriginalInteractionResponse(ctx context.Context, interactionToken strin
|
|||
return &msg, nil
|
||||
}
|
||||
|
||||
func GetAuthorizeUrl(state string) string {
|
||||
func GetAuthorizeUrl(state string, includeEmail bool) string {
|
||||
scope := "identify"
|
||||
if includeEmail {
|
||||
scope = "identify email"
|
||||
}
|
||||
|
||||
params := make(url.Values)
|
||||
params.Set("response_type", "code")
|
||||
params.Set("client_id", config.Config.Discord.OAuthClientID)
|
||||
params.Set("scope", "identify")
|
||||
params.Set("scope", scope)
|
||||
params.Set("prompt", "none") // immediately redirect back to HMN if already authorized
|
||||
params.Set("state", state)
|
||||
params.Set("redirect_uri", hmnurl.BuildDiscordOAuthCallback())
|
||||
return fmt.Sprintf("%s?%s", buildUrl("/oauth2/authorize"), params.Encode())
|
||||
|
|
|
@ -5,24 +5,28 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/config"
|
||||
"git.handmade.network/hmn/hmn/src/db"
|
||||
"git.handmade.network/hmn/hmn/src/hmndata"
|
||||
"git.handmade.network/hmn/hmn/src/logging"
|
||||
"git.handmade.network/hmn/hmn/src/models"
|
||||
"git.handmade.network/hmn/hmn/src/oops"
|
||||
)
|
||||
|
||||
// NOTE(asaf): Updates or creates a discord message according to the following rules:
|
||||
// Create when:
|
||||
// * No previous message exists
|
||||
// * We have non-zero live streamers
|
||||
// * Message exists, but we're adding a new streamer that wasn't in the existing message
|
||||
// * Message exists, but is not the most recent message in the channel
|
||||
// Update otherwise
|
||||
// That way we ensure that the message doesn't get scrolled offscreen, and the
|
||||
// new message indicator for the channel doesn't trigger when a streamer goes offline or
|
||||
// updates the stream title.
|
||||
//
|
||||
// Create when:
|
||||
// * No previous message exists
|
||||
// * We have non-zero live streamers
|
||||
// * Message exists, but we're adding a new streamer that wasn't in the existing message
|
||||
// * Message exists, but is not the most recent message in the channel
|
||||
// Update otherwise
|
||||
// That way we ensure that the message doesn't get scrolled offscreen, and the
|
||||
// new message indicator for the channel doesn't trigger when a streamer goes offline or
|
||||
// updates the stream title.
|
||||
//
|
||||
// NOTE(asaf): No-op if StreamsChannelID is not specified in the config
|
||||
func UpdateStreamers(ctx context.Context, dbConn db.ConnOrTx, streamers []hmndata.StreamDetails) error {
|
||||
if len(config.Config.Discord.StreamsChannelID) == 0 {
|
||||
|
@ -44,6 +48,7 @@ func UpdateStreamers(ctx context.Context, dbConn db.ConnOrTx, streamers []hmndat
|
|||
}
|
||||
|
||||
if editExisting {
|
||||
// Make sure we have a message to edit
|
||||
_, err := GetChannelMessage(ctx, config.Config.Discord.StreamsChannelID, livestreamMessage.MessageID)
|
||||
if err != nil {
|
||||
if err == NotFound {
|
||||
|
@ -54,88 +59,146 @@ func UpdateStreamers(ctx context.Context, dbConn db.ConnOrTx, streamers []hmndat
|
|||
}
|
||||
}
|
||||
|
||||
if editExisting {
|
||||
existingStreamers := livestreamMessage.Streamers
|
||||
for _, s := range streamers {
|
||||
found := false
|
||||
for _, es := range existingStreamers {
|
||||
if es.Username == s.Username {
|
||||
found = true
|
||||
if len(streamers) == 0 {
|
||||
if livestreamMessage != nil {
|
||||
err = DeleteMessage(ctx, config.Config.Discord.StreamsChannelID, livestreamMessage.MessageID)
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to delete livestream message from discord")
|
||||
}
|
||||
err = hmndata.RemovePersistentVar(ctx, dbConn, hmndata.VarNameDiscordLivestreamMessage)
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to clear discord persistent var")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if editExisting {
|
||||
// Check if we have new streamers to add
|
||||
existingStreamers := livestreamMessage.Streamers
|
||||
for _, s := range streamers {
|
||||
found := false
|
||||
for _, es := range existingStreamers {
|
||||
if es.Username == s.Username {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
editExisting = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
}
|
||||
|
||||
if editExisting {
|
||||
// Check that our editable message is the latest in the channel
|
||||
messages, err := GetChannelMessages(ctx, config.Config.Discord.StreamsChannelID, GetChannelMessagesInput{
|
||||
Limit: 1,
|
||||
})
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to fetch messages from discord")
|
||||
}
|
||||
if len(messages) == 0 || messages[0].ID != livestreamMessage.MessageID {
|
||||
editExisting = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if editExisting && len(streamers) > 0 {
|
||||
messages, err := GetChannelMessages(ctx, config.Config.Discord.StreamsChannelID, GetChannelMessagesInput{
|
||||
Limit: 1,
|
||||
})
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to fetch messages from discord")
|
||||
}
|
||||
if len(messages) == 0 || messages[0].ID != livestreamMessage.MessageID {
|
||||
editExisting = false
|
||||
}
|
||||
}
|
||||
|
||||
messageContent := ""
|
||||
if len(streamers) == 0 {
|
||||
messageContent = "No one is currently streaming."
|
||||
} else {
|
||||
messageContent := ""
|
||||
var builder strings.Builder
|
||||
for _, s := range streamers {
|
||||
builder.WriteString(fmt.Sprintf(":red_circle: **%s** is live: <https://twitch.tv/%s>\n> _%s_\nStarted <t:%d:R>\n\n", s.Username, s.Username, s.Title, s.StartTime.Unix()))
|
||||
}
|
||||
messageContent = builder.String()
|
||||
|
||||
msgJson, err := json.Marshal(CreateMessageRequest{
|
||||
Content: messageContent,
|
||||
Flags: FlagSuppressEmbeds,
|
||||
AllowedMentions: &MessageAllowedMentions{},
|
||||
})
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to marshal discord message")
|
||||
}
|
||||
|
||||
newMessageID := ""
|
||||
if editExisting {
|
||||
updatedMessage, err := EditMessage(ctx, config.Config.Discord.StreamsChannelID, livestreamMessage.MessageID, string(msgJson))
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to update discord message for streams channel")
|
||||
}
|
||||
|
||||
newMessageID = updatedMessage.ID
|
||||
} else {
|
||||
if livestreamMessage != nil {
|
||||
err = DeleteMessage(ctx, config.Config.Discord.StreamsChannelID, livestreamMessage.MessageID)
|
||||
if err != nil {
|
||||
log := logging.ExtractLogger(ctx)
|
||||
log.Error().Err(err).Msg("failed to delete existing discord message from streams channel")
|
||||
}
|
||||
}
|
||||
|
||||
sentMessage, err := CreateMessage(ctx, config.Config.Discord.StreamsChannelID, string(msgJson))
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to create discord message for streams channel")
|
||||
}
|
||||
|
||||
newMessageID = sentMessage.ID
|
||||
}
|
||||
|
||||
data := hmndata.DiscordLivestreamMessage{
|
||||
MessageID: newMessageID,
|
||||
Streamers: streamers,
|
||||
}
|
||||
err = hmndata.StorePersistentVar(ctx, dbConn, hmndata.VarNameDiscordLivestreamMessage, &data)
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to store persistent var for discord streams")
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func PostStreamHistory(ctx context.Context, history *models.TwitchStreamHistory) (string, error) {
|
||||
if len(config.Config.Discord.StreamsChannelID) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
approximated := ""
|
||||
if history.EndApproximated {
|
||||
approximated = "about "
|
||||
}
|
||||
duration := history.EndedAt.Sub(history.StartedAt).Truncate(time.Second).String()
|
||||
messageContent := fmt.Sprintf(
|
||||
":o: **%s** was live: https://twitch.tv/%s\n> _%s_\nOn <t:%d:F> for %s%s",
|
||||
history.TwitchLogin,
|
||||
history.TwitchLogin,
|
||||
history.Title,
|
||||
history.StartedAt.Unix(),
|
||||
approximated,
|
||||
duration,
|
||||
)
|
||||
if history.VODUrl != "" {
|
||||
messageContent += fmt.Sprintf("\nVOD: %s", history.VODUrl)
|
||||
}
|
||||
msgJson, err := json.Marshal(CreateMessageRequest{
|
||||
Content: messageContent,
|
||||
Flags: FlagSuppressEmbeds,
|
||||
AllowedMentions: &MessageAllowedMentions{},
|
||||
})
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to marshal discord message")
|
||||
return "", oops.New(err, "failed to marshal discord message")
|
||||
}
|
||||
|
||||
newMessageID := ""
|
||||
if editExisting {
|
||||
updatedMessage, err := EditMessage(ctx, config.Config.Discord.StreamsChannelID, livestreamMessage.MessageID, string(msgJson))
|
||||
messageID := ""
|
||||
if history.DiscordMessageID != "" {
|
||||
updatedMessage, err := EditMessage(ctx, config.Config.Discord.StreamsChannelID, history.DiscordMessageID, string(msgJson))
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to update discord message for streams channel")
|
||||
return "", oops.New(err, "failed to update discord message for stream history")
|
||||
}
|
||||
|
||||
newMessageID = updatedMessage.ID
|
||||
messageID = updatedMessage.ID
|
||||
} else {
|
||||
if livestreamMessage != nil {
|
||||
err = DeleteMessage(ctx, config.Config.Discord.StreamsChannelID, livestreamMessage.MessageID)
|
||||
if err != nil {
|
||||
log := logging.ExtractLogger(ctx)
|
||||
log.Error().Err(err).Msg("failed to delete existing discord message from streams channel")
|
||||
}
|
||||
}
|
||||
|
||||
sentMessage, err := CreateMessage(ctx, config.Config.Discord.StreamsChannelID, string(msgJson))
|
||||
msg, err := CreateMessage(ctx, config.Config.Discord.StreamsChannelID, string(msgJson))
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to create discord message for streams channel")
|
||||
return "", oops.New(err, "failed to create discord message for stream history")
|
||||
}
|
||||
|
||||
newMessageID = sentMessage.ID
|
||||
messageID = msg.ID
|
||||
}
|
||||
|
||||
data := hmndata.DiscordLivestreamMessage{
|
||||
MessageID: newMessageID,
|
||||
Streamers: streamers,
|
||||
}
|
||||
err = hmndata.StorePersistentVar(ctx, dbConn, hmndata.VarNameDiscordLivestreamMessage, &data)
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to store persistent var for discord streams")
|
||||
}
|
||||
|
||||
return nil
|
||||
return messageID, nil
|
||||
}
|
||||
|
|
|
@ -26,14 +26,21 @@ type RegistrationEmailData struct {
|
|||
CompleteRegistrationUrl string
|
||||
}
|
||||
|
||||
func SendRegistrationEmail(toAddress string, toName string, username string, completionToken string, perf *perf.RequestPerf) error {
|
||||
func SendRegistrationEmail(
|
||||
toAddress string,
|
||||
toName string,
|
||||
username string,
|
||||
completionToken string,
|
||||
destination string,
|
||||
perf *perf.RequestPerf,
|
||||
) error {
|
||||
perf.StartBlock("EMAIL", "Registration email")
|
||||
|
||||
perf.StartBlock("EMAIL", "Rendering template")
|
||||
contents, err := renderTemplate("email_registration.html", RegistrationEmailData{
|
||||
Name: toName,
|
||||
HomepageUrl: hmnurl.BuildHomepage(),
|
||||
CompleteRegistrationUrl: hmnurl.BuildEmailConfirmation(username, completionToken),
|
||||
CompleteRegistrationUrl: hmnurl.BuildEmailConfirmation(username, completionToken, destination),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -41,7 +48,47 @@ func SendRegistrationEmail(toAddress string, toName string, username string, com
|
|||
perf.EndBlock()
|
||||
|
||||
perf.StartBlock("EMAIL", "Sending email")
|
||||
err = sendMail(toAddress, toName, "[handmade.network] Registration confirmation", contents)
|
||||
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)
|
||||
if err != nil {
|
||||
return oops.New(err, "Failed to send email")
|
||||
}
|
||||
|
@ -73,7 +120,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")
|
||||
}
|
||||
|
@ -84,6 +131,41 @@ 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 {
|
||||
|
@ -92,10 +174,7 @@ func IsEmail(address string) bool {
|
|||
|
||||
func renderTemplate(name string, data interface{}) (string, error) {
|
||||
var buffer bytes.Buffer
|
||||
template, hasTemplate := templates.Templates[name]
|
||||
if !hasTemplate {
|
||||
return "", oops.New(nil, "Template not found: %s", name)
|
||||
}
|
||||
template := templates.GetTemplate(name)
|
||||
err := template.Execute(&buffer, data)
|
||||
if err != nil {
|
||||
return "", oops.New(err, "Failed to render template for email")
|
||||
|
@ -117,7 +196,7 @@ func sendMail(toAddress, toName, subject, contentHtml string) error {
|
|||
)
|
||||
return smtp.SendMail(
|
||||
fmt.Sprintf("%s:%d", config.Config.Email.ServerAddress, config.Config.Email.ServerPort),
|
||||
smtp.PlainAuth("", config.Config.Email.FromAddress, config.Config.Email.FromAddressPassword, config.Config.Email.ServerAddress),
|
||||
smtp.PlainAuth("", config.Config.Email.MailerUsername, config.Config.Email.MailerPassword, config.Config.Email.ServerAddress),
|
||||
config.Config.Email.FromAddress,
|
||||
[]string{toAddress},
|
||||
contents,
|
||||
|
|
|
@ -11,28 +11,94 @@ import (
|
|||
"git.handmade.network/hmn/hmn/src/utils"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
StartTime, EndTime time.Time
|
||||
}
|
||||
|
||||
type Jam struct {
|
||||
Name string
|
||||
Slug string
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
Event
|
||||
Name string
|
||||
Slug string
|
||||
UrlSlug string
|
||||
}
|
||||
|
||||
var WRJ2021 = Jam{
|
||||
Name: "Wheel Reinvention Jam 2021",
|
||||
Slug: "WRJ2021",
|
||||
StartTime: time.Date(2021, 9, 27, 0, 0, 0, 0, time.UTC),
|
||||
EndTime: time.Date(2021, 10, 4, 0, 0, 0, 0, time.UTC),
|
||||
Event: Event{
|
||||
StartTime: time.Date(2021, 9, 27, 0, 0, 0, 0, time.UTC),
|
||||
EndTime: time.Date(2021, 10, 4, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
Name: "Wheel Reinvention Jam 2021",
|
||||
Slug: "WRJ2021",
|
||||
UrlSlug: "2021",
|
||||
}
|
||||
|
||||
var WRJ2022 = Jam{
|
||||
Name: "Wheel Reinvention Jam 2022",
|
||||
Slug: "WRJ2022",
|
||||
StartTime: time.Date(2022, 8, 3, 8, 0, 0, 0, utils.Must1(time.LoadLocation("America/Los_Angeles"))),
|
||||
EndTime: time.Date(2022, 8, 22, 8, 0, 0, 0, utils.Must1(time.LoadLocation("America/Los_Angeles"))),
|
||||
Event: Event{
|
||||
StartTime: time.Date(2022, 8, 15, 0, 0, 0, 0, utils.Must1(time.LoadLocation("America/Los_Angeles"))),
|
||||
EndTime: time.Date(2022, 8, 22, 8, 0, 0, 0, utils.Must1(time.LoadLocation("America/Los_Angeles"))),
|
||||
},
|
||||
Name: "Wheel Reinvention Jam 2022",
|
||||
Slug: "WRJ2022",
|
||||
UrlSlug: "2022",
|
||||
}
|
||||
|
||||
var AllJams = []Jam{WRJ2021, WRJ2022}
|
||||
var VJ2023 = Jam{
|
||||
Event: Event{
|
||||
StartTime: time.Date(2023, 4, 14, 0, 0, 0, 0, time.UTC),
|
||||
EndTime: time.Date(2023, 4, 17, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
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, 3, 15, 17, 0, 0, 0, time.UTC),
|
||||
EndTime: time.Date(2024, 3, 25, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
Name: "Learning Jam 2024",
|
||||
Slug: "LJ2024",
|
||||
UrlSlug: "learning-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"))),
|
||||
}
|
||||
|
||||
var HMS2023 = Event{
|
||||
StartTime: time.Date(2023, 11, 15, 0, 0, 0, 0, utils.Must1(time.LoadLocation("America/Los_Angeles"))),
|
||||
EndTime: time.Date(2023, 11, 17, 0, 0, 0, 0, utils.Must1(time.LoadLocation("America/Los_Angeles"))),
|
||||
}
|
||||
|
||||
var HMBoston2023 = Event{
|
||||
StartTime: time.Date(2023, 8, 3, 0, 0, 0, 0, utils.Must1(time.LoadLocation("America/Los_Angeles"))),
|
||||
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}
|
||||
|
||||
func CurrentJam() *Jam {
|
||||
now := time.Now()
|
||||
|
@ -44,6 +110,17 @@ 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 {
|
||||
|
|
|
@ -84,3 +84,17 @@ func StorePersistentVar[T any](
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func RemovePersistentVar(ctx context.Context, dbConn db.ConnOrTx, name PersistentVarName) error {
|
||||
_, err := dbConn.Exec(ctx,
|
||||
`
|
||||
DELETE FROM persistent_var
|
||||
WHERE name = $1
|
||||
`,
|
||||
name,
|
||||
)
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to delete var")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -152,7 +152,7 @@ func FetchProjects(
|
|||
}
|
||||
|
||||
// Do the query
|
||||
projectRows, err := db.Query[projectRow](ctx, dbConn, qb.String(), qb.Args()...)
|
||||
projectRows, err := db.Query[projectRow](ctx, tx, qb.String(), qb.Args()...)
|
||||
if err != nil {
|
||||
return nil, oops.New(err, "failed to fetch projects")
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import (
|
|||
"git.handmade.network/hmn/hmn/src/parsing"
|
||||
"git.handmade.network/hmn/hmn/src/perf"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v4"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type ThreadsQuery struct {
|
||||
|
|
|
@ -29,7 +29,7 @@ func StartServer(ctx context.Context) jobs.Job {
|
|||
return jobs.Noop()
|
||||
}
|
||||
|
||||
utils.Must0(os.MkdirAll(dir, fs.ModePerm))
|
||||
utils.Must(os.MkdirAll(dir, fs.ModePerm))
|
||||
|
||||
s := server{
|
||||
log: logging.ExtractLogger(ctx).With().
|
||||
|
@ -84,7 +84,7 @@ func (s *server) putObject(w http.ResponseWriter, r *http.Request) {
|
|||
bucket, key := bucketKey(r)
|
||||
|
||||
w.Header().Set("Location", fmt.Sprintf("/%s", bucket))
|
||||
utils.Must0(os.MkdirAll(filepath.Join(dir, bucket), fs.ModePerm))
|
||||
utils.Must(os.MkdirAll(filepath.Join(dir, bucket), fs.ModePerm))
|
||||
if key != "" {
|
||||
file := utils.Must1(os.Create(filepath.Join(dir, bucket, key)))
|
||||
io.Copy(file, r.Body)
|
||||
|
|
|
@ -28,13 +28,13 @@ func QFromURL(u *url.URL) []Q {
|
|||
}
|
||||
|
||||
var baseUrlParsed url.URL
|
||||
var cacheBust string
|
||||
var cacheBustVersion string
|
||||
var S3BaseUrl string
|
||||
var isTest bool
|
||||
|
||||
func init() {
|
||||
SetGlobalBaseUrl(config.Config.BaseUrl)
|
||||
SetCacheBust(fmt.Sprint(time.Now().Unix()))
|
||||
SetCacheBustVersion(fmt.Sprint(time.Now().Unix()))
|
||||
SetS3BaseUrl(config.Config.DigitalOcean.AssetsPublicUrlRoot)
|
||||
}
|
||||
|
||||
|
@ -50,8 +50,8 @@ func SetGlobalBaseUrl(fullBaseUrl string) {
|
|||
baseUrlParsed = *parsed
|
||||
}
|
||||
|
||||
func SetCacheBust(newCacheBust string) {
|
||||
cacheBust = newCacheBust
|
||||
func SetCacheBustVersion(newCacheBustVersion string) {
|
||||
cacheBustVersion = newCacheBustVersion
|
||||
}
|
||||
|
||||
func SetS3BaseUrl(base string) {
|
||||
|
|
|
@ -44,10 +44,6 @@ 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)
|
||||
}
|
||||
|
@ -70,12 +66,16 @@ 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(""), RegexLogoutAction, nil)
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildRegister(), RegexRegister, nil)
|
||||
AssertRegexMatch(t, BuildRegister(""), RegexRegister, nil)
|
||||
}
|
||||
|
||||
func TestRegistrationSuccess(t *testing.T) {
|
||||
|
@ -83,7 +83,7 @@ func TestRegistrationSuccess(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestEmailConfirmation(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildEmailConfirmation("mruser", "test_token"), RegexEmailConfirmation, map[string]string{"username": "mruser", "token": "test_token"})
|
||||
AssertRegexMatch(t, BuildEmailConfirmation("mruser", "test_token", ""), RegexEmailConfirmation, map[string]string{"username": "mruser", "token": "test_token"})
|
||||
}
|
||||
|
||||
func TestPasswordReset(t *testing.T) {
|
||||
|
@ -95,6 +95,9 @@ 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, BuildStaffRole("test"), RegexStaffRole, nil)
|
||||
AssertRegexMatch(t, BuildStaffRolesIndex(), RegexStaffRolesIndex, nil)
|
||||
AssertRegexMatch(t, BuildCommunicationGuidelines(), RegexCommunicationGuidelines, nil)
|
||||
AssertRegexMatch(t, BuildContactPage(), RegexContactPage, nil)
|
||||
AssertRegexMatch(t, BuildMonthlyUpdatePolicy(), RegexMonthlyUpdatePolicy, nil)
|
||||
|
@ -112,7 +115,7 @@ func TestUserSettings(t *testing.T) {
|
|||
func TestAdmin(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildAdminAtomFeed(), RegexAdminAtomFeed, nil)
|
||||
AssertRegexMatch(t, BuildAdminApprovalQueue(), RegexAdminApprovalQueue, nil)
|
||||
AssertRegexMatch(t, BuildAdminSetUserStatus(), RegexAdminSetUserStatus, nil)
|
||||
AssertRegexMatch(t, BuildAdminSetUserOptions(), RegexAdminSetUserOptions, nil)
|
||||
AssertRegexMatch(t, BuildAdminNukeUser(), RegexAdminNukeUser, nil)
|
||||
}
|
||||
|
||||
|
@ -135,9 +138,10 @@ func TestFeed(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestProjectIndex(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildProjectIndex(1), RegexProjectIndex, nil)
|
||||
AssertRegexMatch(t, BuildProjectIndex(2), RegexProjectIndex, map[string]string{"page": "2"})
|
||||
assert.Panics(t, func() { BuildProjectIndex(0) })
|
||||
AssertRegexMatch(t, BuildProjectIndex(1, ""), RegexProjectIndex, nil)
|
||||
AssertRegexMatch(t, BuildProjectIndex(1, "test"), RegexProjectIndex, map[string]string{"category": "test"})
|
||||
AssertRegexMatch(t, BuildProjectIndex(2, "test"), RegexProjectIndex, map[string]string{"page": "2", "category": "test"})
|
||||
assert.Panics(t, func() { BuildProjectIndex(0, "") })
|
||||
}
|
||||
|
||||
func TestProjectNew(t *testing.T) {
|
||||
|
@ -185,6 +189,36 @@ func TestFishbowl(t *testing.T) {
|
|||
AssertRegexNoMatch(t, BuildFishbowl("oop")+"/otherfiles/whatever", RegexFishbowl)
|
||||
}
|
||||
|
||||
func TestEducationIndex(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildEducationIndex(), RegexEducationIndex, nil)
|
||||
AssertRegexNoMatch(t, BuildEducationArticle("foo"), RegexEducationIndex)
|
||||
}
|
||||
|
||||
func TestEducationGlossary(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildEducationGlossary(""), RegexEducationGlossary, map[string]string{"slug": ""})
|
||||
AssertRegexMatch(t, BuildEducationGlossary("foo"), RegexEducationGlossary, map[string]string{"slug": "foo"})
|
||||
}
|
||||
|
||||
func TestEducationArticle(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildEducationArticle("foo"), RegexEducationArticle, map[string]string{"slug": "foo"})
|
||||
}
|
||||
|
||||
func TestEducationArticleNew(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildEducationArticleNew(), RegexEducationArticleNew, nil)
|
||||
}
|
||||
|
||||
func TestEducationArticleEdit(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildEducationArticleEdit("foo"), RegexEducationArticleEdit, map[string]string{"slug": "foo"})
|
||||
}
|
||||
|
||||
func TestEducationArticleDelete(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildEducationArticleDelete("foo"), RegexEducationArticleDelete, map[string]string{"slug": "foo"})
|
||||
}
|
||||
|
||||
func TestEducationRerender(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildEducationRerender(), RegexEducationRerender, nil)
|
||||
}
|
||||
|
||||
func TestForum(t *testing.T) {
|
||||
AssertRegexMatch(t, hmn.BuildForum(nil, 1), RegexForum, nil)
|
||||
AssertRegexMatch(t, hmn.BuildForum([]string{"wip"}, 2), RegexForum, map[string]string{"subforums": "wip", "page": "2"})
|
||||
|
@ -282,22 +316,6 @@ func TestBlogPostReply(t *testing.T) {
|
|||
AssertSubdomain(t, hero.BuildBlogPostReply(1, 2), "hero")
|
||||
}
|
||||
|
||||
func TestLibrary(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildLibrary(), RegexLibrary, nil)
|
||||
}
|
||||
|
||||
func TestLibraryAll(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildLibraryAll(), RegexLibraryAll, nil)
|
||||
}
|
||||
|
||||
func TestLibraryTopic(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildLibraryTopic(1), RegexLibraryTopic, map[string]string{"topicid": "1"})
|
||||
}
|
||||
|
||||
func TestLibraryResource(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildLibraryResource(1), RegexLibraryResource, map[string]string{"resourceid": "1"})
|
||||
}
|
||||
|
||||
func TestEpisodeGuide(t *testing.T) {
|
||||
AssertRegexMatch(t, hero.BuildEpisodeList(""), RegexEpisodeList, map[string]string{"topic": ""})
|
||||
AssertRegexMatch(t, hero.BuildEpisodeList("code"), RegexEpisodeList, map[string]string{"topic": "code"})
|
||||
|
@ -355,11 +373,101 @@ 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(), "")
|
||||
}
|
||||
|
||||
func TestJamIndex2021(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildJamIndex2021(), RegexJamIndex2021, nil)
|
||||
AssertSubdomain(t, BuildJamIndex2021(), "")
|
||||
}
|
||||
|
||||
func TestJamIndex2022(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildJamIndex2022(), RegexJamIndex2022, nil)
|
||||
AssertSubdomain(t, BuildJamIndex2022(), "")
|
||||
}
|
||||
|
||||
func TestJamFeed2022(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildJamFeed2022(), RegexJamFeed2022, nil)
|
||||
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(), RegexNewsletterSignup, nil)
|
||||
AssertSubdomain(t, BuildAPINewsletterSignup(), "")
|
||||
}
|
||||
|
||||
func TestProjectNewJam(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildProjectNewJam(), RegexProjectNew, nil)
|
||||
AssertSubdomain(t, BuildProjectNewJam(), "")
|
||||
}
|
||||
|
||||
func TestDiscordOAuthCallback(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildDiscordOAuthCallback(), RegexDiscordOAuthCallback, nil)
|
||||
}
|
||||
|
|
|
@ -35,13 +35,6 @@ func BuildShowcase() string {
|
|||
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 {
|
||||
|
@ -49,6 +42,13 @@ func BuildWhenIsIt() string {
|
|||
return Url("/whenisit", nil)
|
||||
}
|
||||
|
||||
var RegexJamsIndex = regexp.MustCompile("^/jams$")
|
||||
|
||||
func BuildJamsIndex() string {
|
||||
defer CatchPanic()
|
||||
return Url("/jams", nil)
|
||||
}
|
||||
|
||||
var RegexJamIndex = regexp.MustCompile("^/jam$")
|
||||
|
||||
func BuildJamIndex() string {
|
||||
|
@ -77,6 +77,128 @@ func BuildJamFeed2022() string {
|
|||
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 {
|
||||
defer CatchPanic()
|
||||
return Url("/jam/visibility-2023", nil)
|
||||
}
|
||||
|
||||
var RegexJamFeed2023_Visibility = regexp.MustCompile("^/jam/visibility-2023/feed$")
|
||||
|
||||
func BuildJamFeed2023_Visibility() string {
|
||||
defer CatchPanic()
|
||||
return Url("/jam/visibility-2023/feed", nil)
|
||||
}
|
||||
|
||||
var RegexJamRecap2023_Visibility = regexp.MustCompile("^/jam/visibility-2023/recap$")
|
||||
|
||||
func BuildJamRecap2023_Visibility() 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 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)
|
||||
}
|
||||
|
||||
// QUESTION(ben): Can we change these routes?
|
||||
|
||||
var RegexLoginAction = regexp.MustCompile("^/login$")
|
||||
|
@ -90,7 +212,18 @@ var RegexLoginPage = regexp.MustCompile("^/login$")
|
|||
|
||||
func BuildLoginPage(redirectTo string) string {
|
||||
defer CatchPanic()
|
||||
return Url("/login", []Q{{Name: "redirect", Value: redirectTo}})
|
||||
var q []Q
|
||||
if redirectTo != "" {
|
||||
q = append(q, Q{Name: "redirect", Value: redirectTo})
|
||||
}
|
||||
return Url("/login", q)
|
||||
}
|
||||
|
||||
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 RegexLogoutAction = regexp.MustCompile("^/logout$")
|
||||
|
@ -105,9 +238,13 @@ func BuildLogoutAction(redir string) string {
|
|||
|
||||
var RegexRegister = regexp.MustCompile("^/register$")
|
||||
|
||||
func BuildRegister() string {
|
||||
func BuildRegister(destination string) string {
|
||||
defer CatchPanic()
|
||||
return Url("/register", nil)
|
||||
var query []Q
|
||||
if destination != "" {
|
||||
query = append(query, Q{"destination", destination})
|
||||
}
|
||||
return Url("/register", query)
|
||||
}
|
||||
|
||||
var RegexRegistrationSuccess = regexp.MustCompile("^/registered_successfully$")
|
||||
|
@ -119,9 +256,13 @@ func BuildRegistrationSuccess() string {
|
|||
|
||||
var RegexEmailConfirmation = regexp.MustCompile("^/email_confirmation/(?P<username>[^/]+)/(?P<token>[^/]+)$")
|
||||
|
||||
func BuildEmailConfirmation(username, token string) string {
|
||||
func BuildEmailConfirmation(username, token string, destination string) string {
|
||||
defer CatchPanic()
|
||||
return Url(fmt.Sprintf("/email_confirmation/%s/%s", url.PathEscape(username), token), nil)
|
||||
var query []Q
|
||||
if destination != "" {
|
||||
query = append(query, Q{"destination", destination})
|
||||
}
|
||||
return Url(fmt.Sprintf("/email_confirmation/%s/%s", url.PathEscape(username), token), query)
|
||||
}
|
||||
|
||||
var RegexRequestPasswordReset = regexp.MustCompile("^/password_reset$")
|
||||
|
@ -164,6 +305,13 @@ func BuildAbout() string {
|
|||
return Url("/about", nil)
|
||||
}
|
||||
|
||||
var RegexFoundation = regexp.MustCompile("^/foundation$")
|
||||
|
||||
func BuildFoundation() string {
|
||||
defer CatchPanic()
|
||||
return Url("/foundation", nil)
|
||||
}
|
||||
|
||||
var RegexCommunicationGuidelines = regexp.MustCompile("^/communication-guidelines$")
|
||||
|
||||
func BuildCommunicationGuidelines() string {
|
||||
|
@ -199,6 +347,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
|
||||
*/
|
||||
|
@ -237,11 +403,11 @@ func BuildAdminApprovalQueue() string {
|
|||
return Url("/admin/approvals", nil)
|
||||
}
|
||||
|
||||
var RegexAdminSetUserStatus = regexp.MustCompile(`^/admin/setuserstatus$`)
|
||||
var RegexAdminSetUserOptions = regexp.MustCompile(`^/admin/setuseroptions$`)
|
||||
|
||||
func BuildAdminSetUserStatus() string {
|
||||
func BuildAdminSetUserOptions() string {
|
||||
defer CatchPanic()
|
||||
return Url("/admin/setuserstatus", nil)
|
||||
return Url("/admin/setuseroptions", nil)
|
||||
}
|
||||
|
||||
var RegexAdminNukeUser = regexp.MustCompile(`^/admin/nukeuser$`)
|
||||
|
@ -312,17 +478,21 @@ func BuildAtomFeedForShowcase() string {
|
|||
* Projects
|
||||
*/
|
||||
|
||||
var RegexProjectIndex = regexp.MustCompile("^/projects(/(?P<page>.+)?)?$")
|
||||
var RegexProjectIndex = regexp.MustCompile(`^/projects(/(?P<category>[a-z0-9-]+)(/(?P<page>\d+))?)?$`)
|
||||
|
||||
func BuildProjectIndex(page int) string {
|
||||
func BuildProjectIndex(page int, category string) string {
|
||||
defer CatchPanic()
|
||||
if page < 1 {
|
||||
panic(oops.New(nil, "page must be >= 1"))
|
||||
}
|
||||
catpath := ""
|
||||
if category != "" {
|
||||
catpath = "/" + category
|
||||
}
|
||||
if page == 1 {
|
||||
return Url("/projects", nil)
|
||||
return Url(fmt.Sprintf("/projects%s", catpath), nil)
|
||||
} else {
|
||||
return Url(fmt.Sprintf("/projects/%d", page), nil)
|
||||
return Url(fmt.Sprintf("/projects%s/%d", catpath, page), nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -337,7 +507,7 @@ func BuildProjectNew() string {
|
|||
func BuildProjectNewJam() string {
|
||||
defer CatchPanic()
|
||||
|
||||
return Url("/p/new", []Q{Q{Name: "jam", Value: "1"}})
|
||||
return Url("/p/new", []Q{{Name: "jam", Value: "1"}})
|
||||
}
|
||||
|
||||
var RegexPersonalProject = regexp.MustCompile("^/p/(?P<projectid>[0-9]+)(/(?P<projectslug>[a-zA-Z0-9-]+))?")
|
||||
|
@ -426,6 +596,59 @@ func BuildFishbowl(slug string) string {
|
|||
|
||||
var RegexFishbowlFiles = regexp.MustCompile(`^/fishbowl/(?P<slug>[^/]+)(?P<path>/.+)$`)
|
||||
|
||||
/*
|
||||
* Education
|
||||
*/
|
||||
|
||||
var RegexEducationIndex = regexp.MustCompile(`^/education$`)
|
||||
|
||||
func BuildEducationIndex() string {
|
||||
defer CatchPanic()
|
||||
return Url("/education", nil)
|
||||
}
|
||||
|
||||
var RegexEducationGlossary = regexp.MustCompile(`^/education/glossary(/(?P<slug>[^/]+))?$`)
|
||||
|
||||
func BuildEducationGlossary(termSlug string) string {
|
||||
defer CatchPanic()
|
||||
|
||||
if termSlug == "" {
|
||||
return Url("/education/glossary", nil)
|
||||
} else {
|
||||
return Url(fmt.Sprintf("/education/glossary/%s", termSlug), nil)
|
||||
}
|
||||
}
|
||||
|
||||
var RegexEducationArticle = regexp.MustCompile(`^/education/(?P<slug>[^/]+)$`)
|
||||
|
||||
func BuildEducationArticle(slug string) string {
|
||||
return Url(fmt.Sprintf("/education/%s", slug), nil)
|
||||
}
|
||||
|
||||
var RegexEducationArticleNew = regexp.MustCompile(`^/education/new$`)
|
||||
|
||||
func BuildEducationArticleNew() string {
|
||||
return Url("/education/new", nil)
|
||||
}
|
||||
|
||||
var RegexEducationArticleEdit = regexp.MustCompile(`^/education/(?P<slug>[^/]+)/edit$`)
|
||||
|
||||
func BuildEducationArticleEdit(slug string) string {
|
||||
return Url(fmt.Sprintf("/education/%s/edit", slug), nil)
|
||||
}
|
||||
|
||||
var RegexEducationArticleDelete = regexp.MustCompile(`^/education/(?P<slug>[^/]+)/delete$`)
|
||||
|
||||
func BuildEducationArticleDelete(slug string) string {
|
||||
return Url(fmt.Sprintf("/education/%s/delete", slug), nil)
|
||||
}
|
||||
|
||||
var RegexEducationRerender = regexp.MustCompile(`^/education/rerender$`)
|
||||
|
||||
func BuildEducationRerender() string {
|
||||
return Url("/education/rerender", nil)
|
||||
}
|
||||
|
||||
/*
|
||||
* Forums
|
||||
*/
|
||||
|
@ -640,50 +863,11 @@ func (c *UrlContext) BuildBlogPostReply(threadId int, postId int) string {
|
|||
}
|
||||
|
||||
/*
|
||||
* Library
|
||||
* Library (old)
|
||||
*/
|
||||
|
||||
// Any library route. Remove after we port the library.
|
||||
var RegexLibraryAny = regexp.MustCompile(`^/library`)
|
||||
|
||||
var RegexLibrary = regexp.MustCompile(`^/library$`)
|
||||
|
||||
func BuildLibrary() string {
|
||||
defer CatchPanic()
|
||||
return Url("/library", nil)
|
||||
}
|
||||
|
||||
var RegexLibraryAll = regexp.MustCompile(`^/library/all$`)
|
||||
|
||||
func BuildLibraryAll() string {
|
||||
defer CatchPanic()
|
||||
return Url("/library/all", nil)
|
||||
}
|
||||
|
||||
var RegexLibraryTopic = regexp.MustCompile(`^/library/topic/(?P<topicid>\d+)$`)
|
||||
|
||||
func BuildLibraryTopic(topicId int) string {
|
||||
defer CatchPanic()
|
||||
if topicId < 1 {
|
||||
panic(oops.New(nil, "Invalid library topic ID (%d), must be >= 1", topicId))
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
builder.WriteString("/library/topic/")
|
||||
builder.WriteString(strconv.Itoa(topicId))
|
||||
|
||||
return Url(builder.String(), nil)
|
||||
}
|
||||
|
||||
var RegexLibraryResource = regexp.MustCompile(`^/library/resource/(?P<resourceid>\d+)$`)
|
||||
|
||||
func BuildLibraryResource(resourceId int) string {
|
||||
defer CatchPanic()
|
||||
builder := buildLibraryResourcePath(resourceId)
|
||||
|
||||
return Url(builder.String(), nil)
|
||||
}
|
||||
|
||||
/*
|
||||
* Episode Guide
|
||||
*/
|
||||
|
@ -738,6 +922,12 @@ 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
|
||||
*/
|
||||
|
@ -748,6 +938,12 @@ 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
|
||||
*/
|
||||
|
@ -821,7 +1017,7 @@ func BuildPublic(filepath string, cachebust bool) string {
|
|||
}
|
||||
var query []Q
|
||||
if cachebust {
|
||||
query = []Q{{"v", cacheBust}}
|
||||
query = []Q{{"v", cacheBustVersion}}
|
||||
}
|
||||
return Url(builder.String(), query)
|
||||
}
|
||||
|
@ -844,6 +1040,12 @@ func BuildUserFile(filepath string) string {
|
|||
return BuildPublic(fmt.Sprintf("media/%s", filepath), false)
|
||||
}
|
||||
|
||||
/*
|
||||
* Redirects
|
||||
*/
|
||||
|
||||
var RegexUnwind = regexp.MustCompile(`^/unwind$`)
|
||||
|
||||
/*
|
||||
* Other
|
||||
*/
|
||||
|
|