implement web push
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -214,3 +214,6 @@ $RECYCLE.BIN/
|
||||
# End of https://www.toptal.com/developers/gitignore/api/windows,macos,linux,goland,go
|
||||
|
||||
.env
|
||||
vapid_public_key
|
||||
vapid_private_key
|
||||
data.sqlite
|
||||
|
15
.idea/dataSources.xml
generated
Normal file
15
.idea/dataSources.xml
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="data.sqlite" uuid="90aa1a10-2d39-42ec-9013-ebbfdf9182c5">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/data.sqlite</jdbc-url>
|
||||
<jdbc-additional-properties>
|
||||
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||
</jdbc-additional-properties>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
11
.idea/runConfigurations/Generate_VAPID.xml
generated
Normal file
11
.idea/runConfigurations/Generate_VAPID.xml
generated
Normal file
@@ -0,0 +1,11 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Generate VAPID" type="GoApplicationRunConfiguration" factoryName="Go Application">
|
||||
<module name="7am" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<kind value="FILE" />
|
||||
<package value="code.nym.sh/7am" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$/generate_vapid.go" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
11
.idea/runConfigurations/go_build_code_nym_sh_7am.xml
generated
Normal file
11
.idea/runConfigurations/go_build_code_nym_sh_7am.xml
generated
Normal file
@@ -0,0 +1,11 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="go build code.nym.sh/7am" type="GoApplicationRunConfiguration" factoryName="Go Application" nameIsGenerated="true">
|
||||
<module name="7am" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<kind value="FILE" />
|
||||
<package value="code.nym.sh/7am" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$/main.go" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
6
.idea/sqldialects.xml
generated
Normal file
6
.idea/sqldialects.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SqlDialectMappings">
|
||||
<file url="file://$PROJECT_DIR$/main.go" dialect="SQLite" />
|
||||
</component>
|
||||
</project>
|
30
generate_vapid.go
Normal file
30
generate_vapid.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/SherClockHolmes/webpush-go"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
pub, priv, err := webpush.GenerateVAPIDKeys()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
f, err := os.OpenFile("vapid_public_key", os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
f.Write([]byte(pub))
|
||||
|
||||
f, err = os.OpenFile("vapid_private_key", os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
f.Write([]byte(priv))
|
||||
}
|
21
go.mod
21
go.mod
@@ -1,19 +1,21 @@
|
||||
module code.nym.sh/weatherboy
|
||||
module code.nym.sh/7am
|
||||
|
||||
go 1.24.3
|
||||
|
||||
require (
|
||||
github.com/coder/websocket v1.8.13
|
||||
github.com/SherClockHolmes/webpush-go v1.4.0
|
||||
github.com/go-co-op/gocron/v2 v2.16.1
|
||||
github.com/joho/godotenv v1.5.1
|
||||
golang.org/x/time v0.11.0
|
||||
google.golang.org/genai v1.4.0
|
||||
modernc.org/sqlite v1.37.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.116.0 // indirect
|
||||
cloud.google.com/go/auth v0.9.3 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/s2a-go v0.1.8 // indirect
|
||||
@@ -21,13 +23,20 @@ require (
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/crypto v0.27.0 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
golang.org/x/net v0.29.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
google.golang.org/grpc v1.66.2 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
modernc.org/libc v1.62.1 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.9.1 // indirect
|
||||
)
|
||||
|
123
go.sum
123
go.sum
@@ -6,20 +6,25 @@ cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842Bg
|
||||
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
|
||||
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/SherClockHolmes/webpush-go v1.4.0 h1:ocnzNKWN23T9nvHi6IfyrQjkIc0oJWv1B1pULsf9i3s=
|
||||
github.com/SherClockHolmes/webpush-go v1.4.0/go.mod h1:XSq8pKX11vNV8MJEMwjrlTkxhAj1zKfxmyhdV7Pd6UA=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/go-co-op/gocron/v2 v2.16.1 h1:ux/5zxVRveCaCuTtNI3DiOk581KC1KpJbpJFYUEVYwo=
|
||||
github.com/go-co-op/gocron/v2 v2.16.1/go.mod h1:opexeOFy5BplhsKdA7bzY9zeYih8I8/WNJ4arTIFPVc=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
@@ -42,6 +47,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
|
||||
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
@@ -55,9 +62,15 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -68,49 +81,111 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
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-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@@ -145,3 +220,27 @@ 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-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic=
|
||||
modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU=
|
||||
modernc.org/ccgo/v4 v4.25.1/go.mod h1:njjuAYiPflywOOrm3B7kCB444ONP5pAVr8PIEoE0uDw=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s=
|
||||
modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g=
|
||||
modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
|
||||
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
|
306
main.go
306
main.go
@@ -2,24 +2,27 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"embed"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/coder/websocket"
|
||||
"github.com/SherClockHolmes/webpush-go"
|
||||
"github.com/go-co-op/gocron/v2"
|
||||
"github.com/google/uuid"
|
||||
"github.com/joho/godotenv"
|
||||
"golang.org/x/time/rate"
|
||||
"google.golang.org/genai"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
"mime"
|
||||
_ "modernc.org/sqlite"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -37,21 +40,39 @@ type pageTemplate struct {
|
||||
summary *template.Template
|
||||
}
|
||||
|
||||
type state struct {
|
||||
ctx context.Context
|
||||
apiKey apiKey
|
||||
template pageTemplate
|
||||
summaries sync.Map
|
||||
summaryChans map[string]chan string
|
||||
genai *genai.Client
|
||||
subscriberCount atomic.Int64
|
||||
}
|
||||
|
||||
type summaryTemplateData struct {
|
||||
Summary string
|
||||
Location string
|
||||
}
|
||||
|
||||
type updateSubscription struct {
|
||||
Subscription webpush.Subscription `json:"subscription"`
|
||||
Locations []string `json:"locations"`
|
||||
}
|
||||
|
||||
type registeredSubscription struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Subscription *webpush.Subscription `json:"-"`
|
||||
Locations []string `json:"locations"`
|
||||
}
|
||||
|
||||
type state struct {
|
||||
ctx context.Context
|
||||
db *sql.DB
|
||||
genai *genai.Client
|
||||
apiKey apiKey
|
||||
template pageTemplate
|
||||
|
||||
summaries sync.Map
|
||||
summaryChans map[string]chan string
|
||||
|
||||
subscriptions map[string][]registeredSubscription
|
||||
subscriptionsMutex sync.Mutex
|
||||
|
||||
vapidPublicKey string
|
||||
vapidPrivateKey string
|
||||
}
|
||||
|
||||
//go:embed web
|
||||
var webDir embed.FS
|
||||
|
||||
@@ -72,6 +93,11 @@ func main() {
|
||||
log.Fatalln("Please create a .env file using the provided template!")
|
||||
}
|
||||
|
||||
db, err := initDB()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to initialize db: %e\n", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
@@ -88,6 +114,7 @@ func main() {
|
||||
|
||||
state := state{
|
||||
ctx: ctx,
|
||||
db: db,
|
||||
apiKey: apiKey{
|
||||
openWeatherMap: os.Getenv("OPEN_WEATHER_MAP_API_KEY"),
|
||||
},
|
||||
@@ -97,6 +124,11 @@ func main() {
|
||||
summaries: sync.Map{},
|
||||
summaryChans: map[string]chan string{},
|
||||
genai: genaiClient,
|
||||
|
||||
subscriptions: map[string][]registeredSubscription{},
|
||||
|
||||
vapidPublicKey: os.Getenv("VAPID_PUBLIC_KEY_BASE64"),
|
||||
vapidPrivateKey: os.Getenv("VAPID_PRIVATE_KEY_BASE64"),
|
||||
}
|
||||
|
||||
var schedulers []gocron.Scheduler
|
||||
@@ -114,17 +146,26 @@ func main() {
|
||||
|
||||
_, err = s.NewJob(
|
||||
gocron.DurationJob(time.Minute),
|
||||
gocron.NewTask(updateSummaries, &state, locKey, &loc))
|
||||
gocron.NewTask(updateSummaries, &state, locKey, &loc),
|
||||
gocron.WithStartAt(gocron.WithStartImmediately()),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
schedulers = append(schedulers, s)
|
||||
state.summaryChans[locKey] = make(chan string)
|
||||
c := make(chan string)
|
||||
|
||||
state.subscriptions[locKey] = []registeredSubscription{}
|
||||
state.summaryChans[locKey] = c
|
||||
|
||||
go listenForSummaryUpdates(&state, locKey)
|
||||
|
||||
s.Start()
|
||||
}
|
||||
|
||||
loadSubscriptions(&state)
|
||||
|
||||
http.HandleFunc("/", handleHTTPRequest(&state))
|
||||
http.ListenAndServe(":8080", nil)
|
||||
|
||||
@@ -137,28 +178,83 @@ func handleHTTPRequest(state *state) http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
path := strings.TrimPrefix(request.URL.Path, "/")
|
||||
|
||||
switch path {
|
||||
case "":
|
||||
index, _ := webDir.ReadFile("web/index.html")
|
||||
writer.Write(index)
|
||||
|
||||
case "ws":
|
||||
conn, err := websocket.Accept(writer, request, nil)
|
||||
if err != nil {
|
||||
log.Printf("error accepting incoming ws connection: %e\n", err)
|
||||
if path == "" {
|
||||
if request.Method == "" || request.Method == "GET" {
|
||||
index, _ := webDir.ReadFile("web/index.html")
|
||||
writer.Write(index)
|
||||
} else {
|
||||
writer.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
defer conn.CloseNow()
|
||||
} else if path == "vapid" {
|
||||
if request.Method == "" || request.Method == "GET" {
|
||||
writer.Write([]byte(state.vapidPublicKey))
|
||||
} else {
|
||||
writer.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
} else if strings.HasPrefix(path, "registrations") {
|
||||
if path == "registrations" && request.Method == "POST" {
|
||||
defer request.Body.Close()
|
||||
|
||||
log.Println("accepted incoming websocket connection")
|
||||
update := updateSubscription{}
|
||||
err := json.NewDecoder(request.Body).Decode(&update)
|
||||
if err != nil {
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
locKey := request.URL.Query().Get("location")
|
||||
if c, ok := state.summaryChans[locKey]; ok {
|
||||
state.subscriberCount.Add(1)
|
||||
sendSummaryUpdates(state, c, conn)
|
||||
state.subscriberCount.Add(-1)
|
||||
reg, err := registerSubscription(state, &update)
|
||||
if err != nil {
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = json.NewEncoder(writer).Encode(reg)
|
||||
if err != nil {
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
} else if request.Method == "PATCH" {
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) < 2 {
|
||||
writer.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
regID, err := uuid.Parse(parts[1])
|
||||
if err != nil {
|
||||
writer.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
defer request.Body.Close()
|
||||
|
||||
update := updateSubscription{}
|
||||
err = json.NewDecoder(request.Body).Decode(&update)
|
||||
if err != nil {
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
reg, err := updateRegisteredSubscription(state, regID, &update)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
writer.WriteHeader(http.StatusNotFound)
|
||||
} else {
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(writer).Encode(reg)
|
||||
} else {
|
||||
writer.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
} else {
|
||||
if request.Method != "" && request.Method != "GET" {
|
||||
writer.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
default:
|
||||
summary, ok := state.summaries.Load(path)
|
||||
if ok {
|
||||
state.template.summary.Execute(writer, summaryTemplateData{summary.(string), path})
|
||||
@@ -178,34 +274,113 @@ func handleHTTPRequest(state *state) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func sendSummaryUpdates(state *state, c <-chan string, conn *websocket.Conn) {
|
||||
l := rate.NewLimiter(rate.Every(time.Millisecond*100), 10)
|
||||
ctx, cancel := context.WithCancel(state.ctx)
|
||||
defer cancel()
|
||||
func initDB() (*sql.DB, error) {
|
||||
db, err := sql.Open("sqlite", "file:data.sqlite")
|
||||
if err != nil {
|
||||
log.Fatalln("failed to initialize database")
|
||||
}
|
||||
|
||||
for {
|
||||
err := l.Wait(ctx)
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS subscriptions(
|
||||
id TEXT PRIMARY KEY,
|
||||
locations TEXT NOT NULL,
|
||||
subscription_json TEXT NOT NULL
|
||||
);
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func loadSubscriptions(state *state) error {
|
||||
rows, err := state.db.Query(`SELECT id, locations, subscription_json FROM subscriptions;`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var id string
|
||||
var locations string
|
||||
var j string
|
||||
|
||||
err := rows.Scan(&id, &locations, &j)
|
||||
if err != nil {
|
||||
return
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
case summary := <-c:
|
||||
log.Println("summary updated. sending updates via sockets...")
|
||||
s := webpush.Subscription{}
|
||||
err = json.Unmarshal([]byte(j), &s)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
w, err := conn.Writer(ctx, websocket.MessageText)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = w.Write([]byte(summary))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
w.Close()
|
||||
case <-ctx.Done():
|
||||
return
|
||||
reg := registeredSubscription{
|
||||
ID: uuid.MustParse(id),
|
||||
Locations: strings.Split(locations, ","),
|
||||
Subscription: &s,
|
||||
}
|
||||
|
||||
for _, l := range reg.Locations {
|
||||
state.subscriptions[l] = append(state.subscriptions[l], reg)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateRegisteredSubscription(state *state, id uuid.UUID, update *updateSubscription) (*registeredSubscription, error) {
|
||||
j, err := json.Marshal(update.Subscription)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = state.db.Exec(
|
||||
"UPDATE subscriptions SET subscription_json = ?, locations = ? WHERE id = ?",
|
||||
string(j), strings.Join(update.Locations, ","), id,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ®isteredSubscription{
|
||||
ID: id,
|
||||
Subscription: &update.Subscription,
|
||||
Locations: update.Locations,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func registerSubscription(state *state, sub *updateSubscription) (*registeredSubscription, error) {
|
||||
j, err := json.Marshal(sub.Subscription)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id, err := uuid.NewV7()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = state.db.Exec(
|
||||
"INSERT INTO subscriptions (id, locations, subscription_json) VALUES (?, ?, ?);",
|
||||
id, strings.Join(sub.Locations, ","), string(j),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reg := registeredSubscription{
|
||||
ID: id,
|
||||
Subscription: &sub.Subscription,
|
||||
Locations: sub.Locations,
|
||||
}
|
||||
|
||||
for _, l := range sub.Locations {
|
||||
state.subscriptions[l] = append(state.subscriptions[l], reg)
|
||||
}
|
||||
|
||||
return ®, nil
|
||||
}
|
||||
|
||||
func updateSummaries(state *state, locKey string, loc *location) {
|
||||
@@ -239,9 +414,32 @@ func updateSummaries(state *state, locKey string, loc *location) {
|
||||
c := state.summaryChans[locKey]
|
||||
|
||||
state.summaries.Store(locKey, summary)
|
||||
if state.subscriberCount.Load() > 0 {
|
||||
if len(state.subscriptions[locKey]) > 0 {
|
||||
c <- summary
|
||||
}
|
||||
|
||||
log.Printf("updated summary for %v successfully\n", locKey)
|
||||
}
|
||||
|
||||
func listenForSummaryUpdates(state *state, locKey string) {
|
||||
c := state.summaryChans[locKey]
|
||||
for {
|
||||
select {
|
||||
case summary := <-c:
|
||||
log.Printf("sending summary for %v to subscribers...\n", locKey)
|
||||
for _, sub := range state.subscriptions[locKey] {
|
||||
_, err := webpush.SendNotificationWithContext(state.ctx, []byte(summary), sub.Subscription, &webpush.Options{
|
||||
VAPIDPublicKey: state.vapidPublicKey,
|
||||
VAPIDPrivateKey: state.vapidPrivateKey,
|
||||
TTL: 30,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("failed to send notification %e\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
case <-state.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
117
web/summary.js
117
web/summary.js
@@ -1,39 +1,104 @@
|
||||
const KEY_UPDATES_ENABLED = "updatesEnabled"
|
||||
const KEY_SUBSCRIPTION = "subscription"
|
||||
|
||||
const canReceiveUpdates = "Notification" in window && "serviceWorker" in navigator
|
||||
const getSummaryButton = document.getElementById("get-summary-btn")
|
||||
const loc = getSummaryButton.dataset.loc
|
||||
getSummaryButton.style.display = "none"
|
||||
|
||||
console.log("can receive updates?", canReceiveUpdates)
|
||||
|
||||
if (canReceiveUpdates) {
|
||||
async function main() {
|
||||
window.addEventListener("load", () => {
|
||||
navigator.serviceWorker.register("/sw.js")
|
||||
})
|
||||
|
||||
navigator.serviceWorker.ready.then((reg) => {
|
||||
if (Notification.permission === "granted" && localStorage.getItem(KEY_UPDATES_ENABLED) === "true") {
|
||||
getSummaryButton.innerText = "Stop updates"
|
||||
reg.active.postMessage(loc)
|
||||
} else {
|
||||
getSummaryButton.innerText = "Get daily updates at 7am"
|
||||
}
|
||||
const reg = await navigator.serviceWorker.ready
|
||||
|
||||
getSummaryButton.addEventListener("click", async () => {
|
||||
const currentlyEnabled = localStorage.getItem(KEY_UPDATES_ENABLED) === "true"
|
||||
const worker = await navigator.serviceWorker.ready
|
||||
if (currentlyEnabled) {
|
||||
localStorage.removeItem(KEY_UPDATES_ENABLED)
|
||||
worker.active.postMessage("cancel")
|
||||
} else if (await Notification.requestPermission()) {
|
||||
localStorage.setItem(KEY_UPDATES_ENABLED, "true")
|
||||
worker.active.postMessage(loc)
|
||||
} else {
|
||||
console.log("notification denied")
|
||||
}
|
||||
})
|
||||
const existingSubscriptionJson = localStorage.getItem(KEY_SUBSCRIPTION)
|
||||
const existingSubscription = existingSubscriptionJson ? JSON.parse(existingSubscriptionJson) : null
|
||||
|
||||
getSummaryButton.style.display = "block"
|
||||
})
|
||||
if (existingSubscription?.locations?.includes(loc) ?? false) {
|
||||
getSummaryButton.innerText = "Stop updates"
|
||||
reg.active.postMessage(loc)
|
||||
} else {
|
||||
getSummaryButton.innerText = "Get daily updates at 7am"
|
||||
}
|
||||
|
||||
getSummaryButton.addEventListener("click", onButtonClick)
|
||||
getSummaryButton.style.display = "block"
|
||||
}
|
||||
|
||||
async function onButtonClick() {
|
||||
const reg = await navigator.serviceWorker.ready
|
||||
|
||||
const existingSubscriptionJson = localStorage.getItem(KEY_SUBSCRIPTION)
|
||||
const existingSubscription = existingSubscriptionJson ? JSON.parse(existingSubscriptionJson) : null
|
||||
const currentlyEnabled = existingSubscription?.locations?.includes(loc) ?? false
|
||||
|
||||
if (currentlyEnabled) {
|
||||
await reg.pushManager.getSubscription().then((sub) => sub?.unsubscribe())
|
||||
localStorage.removeItem(KEY_SUBSCRIPTION)
|
||||
getSummaryButton.innerText = "Get daily updates at 7am"
|
||||
} else {
|
||||
const worker = await navigator.serviceWorker.ready
|
||||
|
||||
try {
|
||||
const publicKey = await fetch("/vapid").then((res) => {
|
||||
if (res.status === 200) {
|
||||
return res.text()
|
||||
}
|
||||
throw new Error(`${res.status}`)
|
||||
})
|
||||
const pushSub = await worker.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: publicKey
|
||||
}).catch((error) => {
|
||||
console.error(error)
|
||||
})
|
||||
|
||||
let newSubscription
|
||||
if (existingSubscription) {
|
||||
newSubscription = await fetch(`/registrations/${existingSubscription.id}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
subscription: pushSub,
|
||||
locations: [...existingSubscription.locations, loc]
|
||||
})
|
||||
}).then((res) => {
|
||||
if (res.status === 200) {
|
||||
return res.json()
|
||||
}
|
||||
throw new Error(`${res.status}`)
|
||||
})
|
||||
} else {
|
||||
newSubscription = await fetch("/registrations", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
subscription: pushSub,
|
||||
locations: [loc]
|
||||
})
|
||||
}).then((res) => {
|
||||
if (res.status === 200) {
|
||||
return res.json()
|
||||
}
|
||||
throw new Error(`${res.status}`)
|
||||
})
|
||||
}
|
||||
|
||||
localStorage.setItem(KEY_SUBSCRIPTION, JSON.stringify(newSubscription))
|
||||
|
||||
getSummaryButton.innerText = "Stop updates"
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (canReceiveUpdates) {
|
||||
main()
|
||||
}
|
||||
|
56
web/sw.js
56
web/sw.js
@@ -1,47 +1,17 @@
|
||||
let ws
|
||||
let shouldRetry = false
|
||||
self.addEventListener('install', function (event) {
|
||||
event.waitUntil(self.skipWaiting());
|
||||
});
|
||||
|
||||
function start(location) {
|
||||
shouldRetry = true
|
||||
const port = self.location.port ? `:${self.location.port}` : ""
|
||||
const socket = new WebSocket(`ws://${self.location.hostname}${port}/ws?location=${location}`)
|
||||
socket.onopen = () => {
|
||||
ws = socket
|
||||
}
|
||||
socket.onclose = () => {
|
||||
reconnect()
|
||||
}
|
||||
socket.onerror = () => {
|
||||
reconnect()
|
||||
}
|
||||
socket.onmessage = (event) => {
|
||||
self.registration.showNotification("7am weather summary", {
|
||||
body: event.data
|
||||
})
|
||||
}
|
||||
}
|
||||
self.addEventListener('activate', function (event) {
|
||||
event.waitUntil(self.clients.claim());
|
||||
});
|
||||
|
||||
function cancel() {
|
||||
shouldRetry = false
|
||||
if (ws) {
|
||||
ws.close()
|
||||
ws = null
|
||||
}
|
||||
}
|
||||
|
||||
async function reconnect() {
|
||||
while (shouldRetry) {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 5 * 60 * 1000)
|
||||
})
|
||||
start(location)
|
||||
}
|
||||
}
|
||||
|
||||
self.addEventListener("message", (event) => {
|
||||
if (event.data === "cancel") {
|
||||
cancel()
|
||||
} else {
|
||||
start(event.data)
|
||||
self.addEventListener("push", (event) => {
|
||||
if (event.data) {
|
||||
event.waitUntil(
|
||||
self.registration.showNotification("7am weather summary", {
|
||||
body: event.data.text()
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
Reference in New Issue
Block a user