Compare commits

...

9 Commits

32 changed files with 472 additions and 1769 deletions

12
.idea/dataSources.xml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="hy_interactive@localhost" uuid="a2f70c83-03f8-4240-bb8b-ac697502cfe2">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/hy_interactive</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

43
go.mod
View File

@ -3,7 +3,8 @@ module git.solsynth.dev/hydrogen/interactive
go 1.21.6
require (
git.solsynth.dev/hydrogen/passport v0.0.0-20240504085931-7c418a3cd32f
git.solsynth.dev/hydrogen/paperclip v0.0.0-20240518085442-715238074040
git.solsynth.dev/hydrogen/passport v0.0.0-20240517123434-ebef35a619f5
github.com/go-playground/validator/v10 v10.17.0
github.com/gofiber/fiber/v2 v2.52.4
github.com/gofiber/template/html/v2 v2.1.1
@ -15,7 +16,7 @@ require (
github.com/rs/zerolog v1.31.0
github.com/samber/lo v1.39.0
github.com/spf13/viper v1.18.2
golang.org/x/crypto v0.21.0
golang.org/x/crypto v0.23.0
google.golang.org/grpc v1.61.1
gorm.io/datatypes v1.2.0
gorm.io/driver/postgres v1.5.4
@ -23,32 +24,15 @@ require (
)
require (
cloud.google.com/go v0.112.0 // indirect
cloud.google.com/go/compute v1.24.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/firestore v1.14.0 // indirect
cloud.google.com/go/iam v1.1.6 // indirect
cloud.google.com/go/longrunning v0.5.5 // indirect
cloud.google.com/go/storage v1.36.0 // indirect
firebase.google.com/go v3.13.0+incompatible // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/fasthttp/websocket v1.5.8 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/gofiber/contrib/websocket v1.3.0 // indirect
github.com/gofiber/template v1.8.3 // indirect
github.com/gofiber/utils v1.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
@ -65,13 +49,11 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nicksnyder/go-i18n/v2 v2.4.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
@ -81,25 +63,12 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.52.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
go.etcd.io/bbolt v1.3.9 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect
go.opentelemetry.io/otel v1.22.0 // indirect
go.opentelemetry.io/otel/metric v1.22.0 // indirect
go.opentelemetry.io/otel/trace v1.22.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/oauth2 v0.16.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/api v0.162.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect

177
go.sum
View File

@ -1,58 +1,22 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM=
cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4=
cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg=
cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/firestore v1.14.0 h1:8aLcKnMPoldYU3YHgu4t2exrKhLQkqaXAGqT0ljrFVw=
cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LRpH9Xp9YZfzQ=
cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc=
cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI=
cloud.google.com/go/longrunning v0.5.5 h1:GOE6pZFdSrTb4KAiKnXsJBtlE6mEyaW44oKyMILWnOg=
cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s=
cloud.google.com/go/storage v1.36.0 h1:P0mOkAcaJxhCTvAkMhxMfrTKiNcub4YmmPBtlhAyTr8=
cloud.google.com/go/storage v1.36.0/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8=
firebase.google.com/go v3.13.0+incompatible h1:3TdYC3DDi6aHn20qoRkxwGqNgdjtblwVAyRLQwGn/+4=
firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs=
git.solsynth.dev/hydrogen/passport v0.0.0-20240504085931-7c418a3cd32f h1:sKrQrKZc5C+dwefRsnc0uAGttzpSUWXUBoFaCXLkaTo=
git.solsynth.dev/hydrogen/passport v0.0.0-20240504085931-7c418a3cd32f/go.mod h1:3JRFPtf0dXRk2UQ1yVIgIspNfytM2yLBeBePJChgLZE=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
git.solsynth.dev/hydrogen/paperclip v0.0.0-20240517123600-145c5563a55e h1:jQNErCjKl76zVO2+nkBvKJK5eEkOVhVXQh4FMm8G0Xc=
git.solsynth.dev/hydrogen/paperclip v0.0.0-20240517123600-145c5563a55e/go.mod h1:uTNEtJcNdgt7DhOgsewPaLQQ5kTN9H+tGNRT2CshHGs=
git.solsynth.dev/hydrogen/paperclip v0.0.0-20240518085442-715238074040 h1:B/3gXFaxoLdD7icLu33OXrx166raEHtHdfhCFrPYgTE=
git.solsynth.dev/hydrogen/paperclip v0.0.0-20240518085442-715238074040/go.mod h1:uTNEtJcNdgt7DhOgsewPaLQQ5kTN9H+tGNRT2CshHGs=
git.solsynth.dev/hydrogen/passport v0.0.0-20240517123434-ebef35a619f5 h1:iEnty5+OHZiIaa27/e9qXfj7lmlhUVe8Oog/BJmLsRM=
git.solsynth.dev/hydrogen/passport v0.0.0-20240517123434-ebef35a619f5/go.mod h1:mEcDEKashAh3jvoGDbNLefK+HgsJaMj4xEc6vkLZ+Zc=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
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/cncf/xds/go v0.0.0-20231109132714-523115ebc101 h1:7To3pQ+pZo0i3dsWEbinPNFs5gPSBOsJtx3wTT94VBY=
github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA=
github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
github.com/fasthttp/websocket v1.5.8 h1:k5DpirKkftIF/w1R8ZzjSgARJrs54Je9YJK37DL/Ah8=
github.com/fasthttp/websocket v1.5.8/go.mod h1:d08g8WaT6nnyvg9uMm8K9zMYyDjfKyj3170AtPRuVU0=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@ -65,8 +29,6 @@ github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofiber/contrib/websocket v1.3.0 h1:XADFAGorer1VJ1bqC4UkCjqS37kwRTV0415+050NrMk=
github.com/gofiber/contrib/websocket v1.3.0/go.mod h1:xguaOzn2ZZ759LavtosEP+rcxIgBEE/rdumPINhR+Xo=
github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM=
github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
@ -81,45 +43,15 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
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=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
@ -167,8 +99,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
@ -177,7 +107,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@ -194,8 +123,6 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 h1:KanIMPX0QdEdB4R3CiimCAbxFrhB3j7h0/OvpYGVQa8=
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
@ -213,7 +140,6 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
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/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
@ -228,63 +154,28 @@ github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHY
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 h1:UNQQKPfTDe1J81ViolILjTKPr9WetKW6uei2hFgJmFs=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw=
go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg=
go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE=
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
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.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
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.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
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/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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.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=
@ -293,63 +184,27 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
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=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.162.0 h1:Vhs54HkaEpkMBdgGdOT2P6F0csGG/vxDS0hWHJzmmps=
google.golang.org/api v0.162.0/go.mod h1:6SulDkfoBIg4NFmCuZ39XeeAgSHCPecfSUuDyYlAHs0=
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=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s=
google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014 h1:x9PwdEgd11LgK+orcck69WVRo7DezSO4VUMPI4xpc8A=
google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014/go.mod h1:rbHMSEDyoYX62nRVLOCc4Qt1HbsdytAYoVwgjiOhF3I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c h1:NUsgEN92SQQqzfA+YtqYNqYmB3DMMYLlIwUZAQFVFbo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY=
google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
@ -359,8 +214,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
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/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=
@ -377,5 +230,3 @@ gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHD
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.6 h1:V92+vVda1wEISSOMtodHVRcUIOPYa2tgQtyF+DfFx+A=
gorm.io/gorm v1.25.6/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
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=

View File

@ -41,8 +41,11 @@ func main() {
}
// Connect other services
if err := grpc.ConnectPaperclip(); err != nil {
log.Fatal().Err(err).Msg("An error occurred when connecting to paperclip...")
}
if err := grpc.ConnectPassport(); err != nil {
log.Fatal().Err(err).Msg("An error occurred when connecting to passport grpc endpoint...")
log.Fatal().Err(err).Msg("An error occurred when connecting to passport...")
}
// Configure timed tasks

View File

@ -5,23 +5,18 @@ import (
"gorm.io/gorm"
)
var DatabaseAutoActionRange = []any{
var AutoMaintainRange = []any{
&models.Account{},
&models.Realm{},
&models.Category{},
&models.Tag{},
&models.Moment{},
&models.Article{},
&models.Comment{},
&models.Post{},
&models.Reaction{},
&models.Attachment{},
}
func RunMigration(source *gorm.DB) error {
if err := source.AutoMigrate(
append([]any{
&models.AccountMembership{},
}, DatabaseAutoActionRange...)...,
AutoMaintainRange...,
); err != nil {
return err
}

View File

@ -1,6 +1,7 @@
package grpc
import (
pcpb "git.solsynth.dev/hydrogen/paperclip/pkg/grpc/proto"
idpb "git.solsynth.dev/hydrogen/passport/pkg/grpc/proto"
"google.golang.org/grpc/credentials/insecure"
@ -8,6 +9,19 @@ import (
"google.golang.org/grpc"
)
var Attachments pcpb.AttachmentsClient
func ConnectPaperclip() error {
addr := viper.GetString("paperclip.grpc_endpoint")
if conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials())); err != nil {
return err
} else {
Attachments = pcpb.NewAttachmentsClient(conn)
}
return nil
}
var Realms idpb.RealmsClient
var Friendships idpb.FriendshipsClient
var Notify idpb.NotifyClient

View File

@ -1,7 +1,5 @@
package models
import "time"
// Account profiles basically fetched from Hydrogen.Passport
// But cache at here for better usage
// At the same time this model can make relations between local models
@ -15,19 +13,7 @@ type Account struct {
Description string `json:"description"`
EmailAddress string `json:"email_address"`
PowerLevel int `json:"power_level"`
Moments []Moment `json:"moments" gorm:"foreignKey:AuthorID"`
Articles []Article `json:"articles" gorm:"foreignKey:AuthorID"`
Attachments []Attachment `json:"attachments" gorm:"foreignKey:AuthorID"`
Posts []Post `json:"posts" gorm:"foreignKey:AuthorID"`
Reactions []Reaction `json:"reactions"`
ExternalID uint `json:"external_id"`
}
type AccountMembership struct {
ID uint `json:"id" gorm:"primaryKey"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Follower Account `json:"follower"`
Following Account `json:"following"`
FollowerID uint
FollowingID uint
}

View File

@ -1,41 +0,0 @@
package models
type Article struct {
PostBase
Title string `json:"title"`
Hashtags []Tag `json:"tags" gorm:"many2many:article_tags"`
Categories []Category `json:"categories" gorm:"many2many:article_categories"`
Reactions []Reaction `json:"reactions"`
Attachments []Attachment `json:"attachments"`
Description string `json:"description"`
Content string `json:"content"`
RealmID *uint `json:"realm_id"`
Realm *Realm `json:"realm"`
Comments []Comment `json:"comments" gorm:"foreignKey:ArticleID"`
}
func (p *Article) GetReplyTo() PostInterface {
return nil
}
func (p *Article) GetRepostTo() PostInterface {
return nil
}
func (p *Article) GetHashtags() []Tag {
return p.Hashtags
}
func (p *Article) GetCategories() []Category {
return p.Categories
}
func (p *Article) SetHashtags(tags []Tag) {
p.Hashtags = tags
}
func (p *Article) SetCategories(categories []Category) {
p.Categories = categories
}

View File

@ -1,43 +0,0 @@
package models
import (
"fmt"
"path/filepath"
"github.com/spf13/viper"
)
type AttachmentType = uint8
const (
AttachmentOthers = AttachmentType(iota)
AttachmentPhoto
AttachmentVideo
AttachmentAudio
)
type Attachment struct {
BaseModel
FileID string `json:"file_id"`
Filesize int64 `json:"filesize"`
Filename string `json:"filename"`
Mimetype string `json:"mimetype"`
Hashcode string `json:"hashcode"`
Type AttachmentType `json:"type"`
ExternalUrl string `json:"external_url"`
Author Account `json:"author"`
ArticleID *uint `json:"article_id"`
MomentID *uint `json:"moment_id"`
CommentID *uint `json:"comment_id"`
AuthorID uint `json:"author_id"`
}
func (v Attachment) GetStoragePath() string {
basepath := viper.GetString("content")
return filepath.Join(basepath, v.FileID)
}
func (v Attachment) GetAccessPath() string {
return fmt.Sprintf("/api/attachments/o/%s", v.FileID)
}

View File

@ -6,9 +6,7 @@ type Tag struct {
Alias string `json:"alias" gorm:"uniqueIndex" validate:"lowercase,alphanum,min=4,max=24"`
Name string `json:"name"`
Description string `json:"description"`
Articles []Article `json:"articles" gorm:"many2many:article_tags"`
Moments []Moment `json:"moments" gorm:"many2many:moment_tags"`
Comments []Comment `json:"comments" gorm:"many2many:comment_tags"`
Posts []Post `json:"posts" gorm:"many2many:post_tags"`
}
type Category struct {
@ -17,7 +15,5 @@ type Category struct {
Alias string `json:"alias" gorm:"uniqueIndex" validate:"lowercase,alphanum,min=4,max=24"`
Name string `json:"name"`
Description string `json:"description"`
Articles []Article `json:"articles" gorm:"many2many:article_categories"`
Moments []Moment `json:"moments" gorm:"many2many:moment_categories"`
Comments []Comment `json:"comments" gorm:"many2many:comment_categories"`
Posts []Post `json:"posts" gorm:"many2many:post_categories"`
}

View File

@ -1,38 +0,0 @@
package models
type Comment struct {
PostBase
Content string `json:"content"`
Hashtags []Tag `json:"tags" gorm:"many2many:comment_tags"`
Categories []Category `json:"categories" gorm:"many2many:comment_categories"`
Reactions []Reaction `json:"reactions"`
Attachments []Attachment `json:"attachments"`
ReplyID *uint `json:"reply_id"`
ReplyTo *Comment `json:"reply_to" gorm:"foreignKey:ReplyID"`
ArticleID *uint `json:"article_id"`
MomentID *uint `json:"moment_id"`
Article *Article `json:"article"`
Moment *Moment `json:"moment"`
}
func (p *Comment) GetReplyTo() PostInterface {
return p.ReplyTo
}
func (p *Comment) GetHashtags() []Tag {
return p.Hashtags
}
func (p *Comment) GetCategories() []Category {
return p.Categories
}
func (p *Comment) SetHashtags(tags []Tag) {
p.Hashtags = tags
}
func (p *Comment) SetCategories(categories []Category) {
p.Categories = categories
}

View File

@ -1,22 +0,0 @@
package models
type Feed struct {
BaseModel
Alias string `json:"alias"`
Title string `json:"title"`
Description string `json:"description"`
Content string `json:"content"`
ModelType string `json:"model_type"`
CommentCount int64 `json:"comment_count"`
ReactionCount int64 `json:"reaction_count"`
AuthorID uint `json:"author_id"`
RealmID *uint `json:"realm_id"`
Author Account `json:"author" gorm:"embedded"`
Attachments []Attachment `json:"attachments" gorm:"-"`
ReactionList map[string]int64 `json:"reaction_list" gorm:"-"`
}

View File

@ -1,41 +0,0 @@
package models
type Moment struct {
PostBase
Content string `json:"content"`
Hashtags []Tag `json:"tags" gorm:"many2many:moment_tags"`
Categories []Category `json:"categories" gorm:"many2many:moment_categories"`
Reactions []Reaction `json:"reactions"`
Attachments []Attachment `json:"attachments"`
RealmID *uint `json:"realm_id"`
RepostID *uint `json:"repost_id"`
Realm *Realm `json:"realm"`
RepostTo *Moment `json:"repost_to" gorm:"foreignKey:RepostID"`
Comments []Comment `json:"comments" gorm:"foreignKey:MomentID"`
}
func (p *Moment) GetRepostTo() PostInterface {
return p.RepostTo
}
func (p *Moment) GetRealm() *Realm {
return p.Realm
}
func (p *Moment) GetHashtags() []Tag {
return p.Hashtags
}
func (p *Moment) GetCategories() []Category {
return p.Categories
}
func (p *Moment) SetHashtags(tags []Tag) {
p.Hashtags = tags
}
func (p *Moment) SetCategories(categories []Category) {
p.Categories = categories
}

View File

@ -1,6 +1,7 @@
package models
import (
"gorm.io/datatypes"
"time"
)
@ -12,53 +13,30 @@ type PostReactInfo struct {
RepostCount int64 `json:"repost_count"`
}
type PostBase struct {
type Post struct {
BaseModel
Alias string `json:"alias" gorm:"uniqueIndex"`
Content string `json:"content"`
Tags []Tag `json:"tags" gorm:"many2many:post_tags"`
Categories []Category `json:"categories" gorm:"many2many:post_categories"`
Reactions []Reaction `json:"reactions"`
Replies []Post `json:"replies" gorm:"foreignKey:ReplyID"`
Attachments datatypes.JSONSlice[uint] `json:"attachments"`
ReplyID *uint `json:"reply_id"`
RepostID *uint `json:"repost_id"`
RealmID *uint `json:"realm_id"`
ReplyTo *Post `json:"reply_to" gorm:"foreignKey:ReplyID"`
RepostTo *Post `json:"repost_to" gorm:"foreignKey:RepostID"`
Realm *Realm `json:"realm"`
PublishedAt *time.Time `json:"published_at"`
AuthorID uint `json:"author_id"`
Author Account `json:"author"`
// Dynamic Calculated Values
ReplyCount int64 `json:"reply_count"`
ReactionCount int64 `json:"reaction_count"`
ReactionList map[string]int64 `json:"reaction_list" gorm:"-"`
}
func (p *PostBase) GetID() uint {
return p.ID
}
func (p *PostBase) GetReplyTo() PostInterface {
return nil
}
func (p *PostBase) GetRepostTo() PostInterface {
return nil
}
func (p *PostBase) GetAuthor() Account {
return p.Author
}
func (p *PostBase) GetRealm() *Realm {
return nil
}
func (p *PostBase) SetReactionList(list map[string]int64) {
p.ReactionList = list
}
type PostInterface interface {
GetID() uint
GetHashtags() []Tag
GetCategories() []Category
GetReplyTo() PostInterface
GetRepostTo() PostInterface
GetAuthor() Account
GetRealm() *Realm
SetHashtags([]Tag)
SetCategories([]Category)
SetReactionList(map[string]int64)
}

View File

@ -20,8 +20,6 @@ type Reaction struct {
Symbol string `json:"symbol"`
Attitude ReactionAttitude `json:"attitude"`
ArticleID *uint `json:"article_id"`
MomentID *uint `json:"moment_id"`
CommentID *uint `json:"comment_id"`
PostID *uint `json:"post_id"`
AccountID uint `json:"account_id"`
}

View File

@ -8,8 +8,7 @@ type Realm struct {
Alias string `json:"alias"`
Name string `json:"name"`
Description string `json:"description"`
Articles []Article `json:"article"`
Moments []Moment `json:"moments"`
Posts []Post `json:"posts"`
IsPublic bool `json:"is_public"`
IsCommunity bool `json:"is_community"`
ExternalID uint `json:"external_id"`

View File

@ -1,141 +0,0 @@
package server
import (
"fmt"
"strings"
"time"
"git.solsynth.dev/hydrogen/interactive/pkg/database"
"git.solsynth.dev/hydrogen/interactive/pkg/models"
"git.solsynth.dev/hydrogen/interactive/pkg/services"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
func contextArticle() *services.PostTypeContext {
return &services.PostTypeContext{
Tx: database.C,
TableName: "articles",
ColumnName: "article",
CanReply: false,
CanRepost: false,
}
}
func createArticle(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
var data struct {
Alias string `json:"alias" form:"alias"`
Title string `json:"title" form:"title" validate:"required"`
Description string `json:"description" form:"description"`
Content string `json:"content" form:"content" validate:"required"`
Hashtags []models.Tag `json:"hashtags" form:"hashtags"`
Categories []models.Category `json:"categories" form:"categories"`
Attachments []models.Attachment `json:"attachments" form:"attachments"`
PublishedAt *time.Time `json:"published_at" form:"published_at"`
RealmAlias string `json:"realm" form:"realm"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
} else if len(data.Alias) == 0 {
data.Alias = strings.ReplaceAll(uuid.NewString(), "-", "")
}
item := &models.Article{
PostBase: models.PostBase{
Alias: data.Alias,
PublishedAt: data.PublishedAt,
AuthorID: user.ID,
},
Hashtags: data.Hashtags,
Categories: data.Categories,
Attachments: data.Attachments,
Title: data.Title,
Description: data.Description,
Content: data.Content,
}
if len(data.RealmAlias) > 0 {
if realm, err := services.GetRealmWithAlias(data.RealmAlias); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else if _, err := services.GetRealmMember(realm.ExternalID, user.ExternalID); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("you aren't a part of related realm: %v", err))
} else {
item.RealmID = &realm.ID
}
}
if item, err := services.NewPost(item); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.JSON(item)
}
}
func editArticle(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
id, _ := c.ParamsInt("articleId", 0)
var data struct {
Alias string `json:"alias" form:"alias" validate:"required"`
Title string `json:"title" form:"title" validate:"required"`
Description string `json:"description" form:"description"`
Content string `json:"content" form:"content" validate:"required"`
PublishedAt *time.Time `json:"published_at" form:"published_at"`
Hashtags []models.Tag `json:"hashtags" form:"hashtags"`
Categories []models.Category `json:"categories" form:"categories"`
Attachments []models.Attachment `json:"attachments" form:"attachments"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
}
var item *models.Article
if err := database.C.Where(models.Article{
PostBase: models.PostBase{
BaseModel: models.BaseModel{ID: uint(id)},
AuthorID: user.ID,
},
}).First(&item).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
item.Alias = data.Alias
item.Title = data.Title
item.Description = data.Description
item.Content = data.Content
item.PublishedAt = data.PublishedAt
item.Hashtags = data.Hashtags
item.Categories = data.Categories
item.Attachments = data.Attachments
if item, err := services.EditPost(item); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.JSON(item)
}
}
func deleteArticle(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
id, _ := c.ParamsInt("articleId", 0)
var item *models.Article
if err := database.C.Where(models.Article{
PostBase: models.PostBase{
BaseModel: models.BaseModel{ID: uint(id)},
AuthorID: user.ID,
},
}).First(&item).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
if err := services.DeletePost(item); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.SendStatus(fiber.StatusOK)
}

View File

@ -1,61 +0,0 @@
package server
import (
"path/filepath"
"git.solsynth.dev/hydrogen/interactive/pkg/models"
"git.solsynth.dev/hydrogen/interactive/pkg/services"
"github.com/gofiber/fiber/v2"
"github.com/spf13/viper"
)
func readAttachment(c *fiber.Ctx) error {
id := c.Params("fileId")
basepath := viper.GetString("content")
return c.SendFile(filepath.Join(basepath, id), true)
}
func uploadAttachment(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
hashcode := c.FormValue("hashcode")
if len(hashcode) != 64 {
return fiber.NewError(fiber.StatusBadRequest, "please provide a SHA256 hashcode, length should be 64 characters")
}
file, err := c.FormFile("attachment")
if err != nil {
return err
}
attachment, err := services.NewAttachment(user, file, hashcode)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
if err := c.SaveFile(file, attachment.GetStoragePath()); err != nil {
return err
}
return c.JSON(fiber.Map{
"info": attachment,
"url": attachment.GetAccessPath(),
})
}
func deleteAttachment(c *fiber.Ctx) error {
id, _ := c.ParamsInt("id", 0)
user := c.Locals("principal").(models.Account)
attachment, err := services.GetAttachmentByID(uint(id))
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
} else if attachment.AuthorID != user.ID {
return fiber.NewError(fiber.StatusNotFound, "record not created by you")
}
if err := services.DeleteAttachment(attachment); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else {
return c.SendStatus(fiber.StatusOK)
}
}

View File

@ -1,196 +0,0 @@
package server
import (
"fmt"
"github.com/spf13/viper"
"strings"
"time"
"git.solsynth.dev/hydrogen/interactive/pkg/database"
"git.solsynth.dev/hydrogen/interactive/pkg/models"
"git.solsynth.dev/hydrogen/interactive/pkg/services"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
func contextComment() *services.PostTypeContext {
return &services.PostTypeContext{
Tx: database.C,
TableName: "comments",
ColumnName: "comment",
CanReply: false,
CanRepost: true,
}
}
func listComment(c *fiber.Ctx) error {
take := c.QueryInt("take", 0)
offset := c.QueryInt("offset", 0)
alias := c.Params("postId")
mx := c.Locals(postContextKey).(*services.PostTypeContext).
FilterPublishedAt(time.Now())
item, err := mx.GetViaAlias(alias)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
data, err := mx.ListComment(item.ID, take, offset)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
count, err := mx.CountComment(item.ID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.JSON(fiber.Map{
"count": count,
"data": data,
})
}
func createComment(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
var data struct {
Content string `json:"content" form:"content" validate:"required"`
PublishedAt *time.Time `json:"published_at" form:"published_at"`
Hashtags []models.Tag `json:"hashtags" form:"hashtags"`
Categories []models.Category `json:"categories" form:"categories"`
Attachments []models.Attachment `json:"attachments" form:"attachments"`
ReplyTo uint `json:"reply_to" form:"reply_to"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
}
item := &models.Comment{
PostBase: models.PostBase{
Alias: strings.ReplaceAll(uuid.NewString(), "-", ""),
PublishedAt: data.PublishedAt,
AuthorID: user.ID,
},
Hashtags: data.Hashtags,
Categories: data.Categories,
Attachments: data.Attachments,
Content: data.Content,
}
postType := c.Params("postType")
alias := c.Params("postId")
var err error
var res models.Feed
var columnName string
var tableName string
switch postType {
case "moments":
columnName = "moment"
tableName = viper.GetString("database.table_prefix") + "moments"
err = database.C.Model(&models.Moment{}).Where("alias = ?", alias).Select("id").First(&res).Error
case "articles":
columnName = "article"
tableName = viper.GetString("database.table_prefix") + "articles"
err = database.C.Model(&models.Article{}).Where("alias = ?", alias).Select("id").First(&res).Error
default:
return fiber.NewError(fiber.StatusBadRequest, "comment must belongs to a resource")
}
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("belongs to resource was not found: %v", err))
} else {
switch postType {
case "moments":
item.MomentID = &res.ID
case "articles":
item.ArticleID = &res.ID
}
}
var relatedCount int64
if data.ReplyTo > 0 {
if err := database.C.Where("id = ?", data.ReplyTo).
Model(&models.Comment{}).Count(&relatedCount).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else if relatedCount <= 0 {
return fiber.NewError(fiber.StatusNotFound, "related post was not found")
} else {
item.ReplyID = &data.ReplyTo
}
}
item, err = services.NewPost(item)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
// Notify the original poster their post is commented by someone
go services.CommentNotify(item, res, columnName, tableName)
return c.JSON(item)
}
func editComment(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
id, _ := c.ParamsInt("commentId", 0)
var data struct {
Content string `json:"content" form:"content" validate:"required"`
PublishedAt *time.Time `json:"published_at" form:"published_at"`
Hashtags []models.Tag `json:"hashtags" form:"hashtags"`
Categories []models.Category `json:"categories" form:"categories"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
}
var item *models.Comment
if err := database.C.Where(models.Comment{
PostBase: models.PostBase{
BaseModel: models.BaseModel{ID: uint(id)},
AuthorID: user.ID,
},
}).First(&item).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
item.Content = data.Content
item.PublishedAt = data.PublishedAt
item.Hashtags = data.Hashtags
item.Categories = data.Categories
if item, err := services.EditPost(item); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.JSON(item)
}
}
func deleteComment(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
id, _ := c.ParamsInt("commentId", 0)
var item *models.Comment
if err := database.C.Where(models.Comment{
PostBase: models.PostBase{
BaseModel: models.BaseModel{ID: uint(id)},
AuthorID: user.ID,
},
}).First(&item).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
if err := services.DeletePost(item); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.SendStatus(fiber.StatusOK)
}

View File

@ -1,200 +1,49 @@
package server
import (
"fmt"
"git.solsynth.dev/hydrogen/interactive/pkg/services"
"strings"
"git.solsynth.dev/hydrogen/interactive/pkg/database"
"git.solsynth.dev/hydrogen/interactive/pkg/models"
"git.solsynth.dev/hydrogen/interactive/pkg/services"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
"github.com/spf13/viper"
)
const (
queryArticle = "id, created_at, updated_at, alias, title, NULL as content, description, realm_id, author_id, 'article' as model_type"
queryMoment = "id, created_at, updated_at, alias, NULL as title, content, NULL as description, realm_id, author_id, 'moment' as model_type"
)
func listFeed(c *fiber.Ctx) error {
take := c.QueryInt("take", 0)
offset := c.QueryInt("offset", 0)
realmAlias := c.Query("realm")
realmId := c.QueryInt("realmId", 0)
if take > 20 {
take = 20
tx := database.C
if realmId > 0 {
tx = services.FilterWithRealm(tx, uint(realmId))
}
var whereConditions []string
if len(realmAlias) > 0 {
realm, err := services.GetRealmWithAlias(realmAlias)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("related realm was not found: %v", err))
}
whereConditions = append(whereConditions, fmt.Sprintf("feed.realm_id = %d", realm.ID))
}
var author models.Account
if len(c.Query("authorId")) > 0 {
var author models.Account
if err := database.C.Where(&models.Account{Name: c.Query("authorId")}).First(&author).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
} else {
whereConditions = append(whereConditions, fmt.Sprintf("feed.author_id = %d", author.ID))
}
tx = tx.Where("author_id = ?", author.ID)
}
var whereStatement string
if len(whereConditions) > 0 {
whereStatement += "WHERE " + strings.Join(whereConditions, " AND ")
if len(c.Query("category")) > 0 {
tx = services.FilterPostWithCategory(tx, c.Query("category"))
}
if len(c.Query("tag")) > 0 {
tx = services.FilterPostWithTag(tx, c.Query("tag"))
}
var result []*models.Feed
userTable := viper.GetString("database.prefix") + "accounts"
commentTable := viper.GetString("database.prefix") + "comments"
reactionTable := viper.GetString("database.prefix") + "reactions"
database.C.Raw(
fmt.Sprintf(`SELECT feed.*, author.*,
COALESCE(comment_count, 0) AS comment_count,
COALESCE(reaction_count, 0) AS reaction_count
FROM (? UNION ALL ?) AS feed
INNER JOIN %s AS author ON author_id = author.id
LEFT JOIN (SELECT article_id, moment_id, COUNT(*) AS comment_count
FROM %s
GROUP BY article_id, moment_id) AS comments
ON (feed.model_type = 'article' AND feed.id = comments.article_id) OR
(feed.model_type = 'moment' AND feed.id = comments.moment_id)
LEFT JOIN (SELECT article_id, moment_id, COUNT(*) AS reaction_count
FROM %s
GROUP BY article_id, moment_id) AS reactions
ON (feed.model_type = 'article' AND feed.id = reactions.article_id) OR
(feed.model_type = 'moment' AND feed.id = reactions.moment_id)
%s ORDER BY feed.created_at desc LIMIT ? OFFSET ?`,
userTable,
commentTable,
reactionTable,
whereStatement,
),
database.C.Select(queryArticle).Model(&models.Article{}),
database.C.Select(queryMoment).Model(&models.Moment{}),
take,
offset,
).Scan(&result)
if !c.QueryBool("noReact", false) {
var reactions []struct {
PostID uint
Symbol string
Count int64
}
revertReaction := func(dataset string) error {
itemMap := lo.SliceToMap(lo.FilterMap(result, func(item *models.Feed, index int) (*models.Feed, bool) {
return item, item.ModelType == dataset
}), func(item *models.Feed) (uint, *models.Feed) {
return item.ID, item
})
idx := lo.Map(lo.Filter(result, func(item *models.Feed, index int) bool {
return item.ModelType == dataset
}), func(item *models.Feed, index int) uint {
return item.ID
})
if err := database.C.Model(&models.Reaction{}).
Select(dataset+"_id as post_id, symbol, COUNT(id) as count").
Where(dataset+"_id IN (?)", idx).
Group("post_id, symbol").
Scan(&reactions).Error; err != nil {
count, err := services.CountPost(tx)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
list := map[uint]map[string]int64{}
for _, info := range reactions {
if _, ok := list[info.PostID]; !ok {
list[info.PostID] = make(map[string]int64)
items, err := services.ListPost(tx, take, offset)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
list[info.PostID][info.Symbol] = info.Count
}
for k, v := range list {
if post, ok := itemMap[k]; ok {
post.ReactionList = v
}
}
return nil
}
if err := revertReaction("article"); err != nil {
return err
}
if err := revertReaction("moment"); err != nil {
return err
}
}
if !c.QueryBool("noAttachment", false) {
revertAttachment := func(dataset string) error {
var attachments []struct {
models.Attachment
PostID uint `json:"post_id"`
}
itemMap := lo.SliceToMap(lo.FilterMap(result, func(item *models.Feed, index int) (*models.Feed, bool) {
return item, item.ModelType == dataset
}), func(item *models.Feed) (uint, *models.Feed) {
return item.ID, item
})
idx := lo.Map(lo.Filter(result, func(item *models.Feed, index int) bool {
return item.ModelType == dataset
}), func(item *models.Feed, index int) uint {
return item.ID
})
if err := database.C.
Model(&models.Attachment{}).
Select(dataset+"_id as post_id, *").
Where(dataset+"_id IN (?)", idx).
Scan(&attachments).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
list := map[uint][]models.Attachment{}
for _, info := range attachments {
list[info.PostID] = append(list[info.PostID], info.Attachment)
}
for k, v := range list {
if post, ok := itemMap[k]; ok {
post.Attachments = v
}
}
return nil
}
if err := revertAttachment("article"); err != nil {
return err
}
if err := revertAttachment("moment"); err != nil {
return err
}
}
var count int64
database.C.Raw(`SELECT COUNT(*) FROM (? UNION ALL ?) as feed`,
database.C.Select(queryArticle).Model(&models.Article{}),
database.C.Select(queryMoment).Model(&models.Moment{}),
).Scan(&count)
return c.JSON(fiber.Map{
"count": count,
"data": result,
"data": items,
})
}

View File

@ -1,147 +0,0 @@
package server
import (
"fmt"
"strings"
"time"
"git.solsynth.dev/hydrogen/interactive/pkg/database"
"git.solsynth.dev/hydrogen/interactive/pkg/models"
"git.solsynth.dev/hydrogen/interactive/pkg/services"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
func contextMoment() *services.PostTypeContext {
return &services.PostTypeContext{
Tx: database.C,
TableName: "moments",
ColumnName: "moment",
CanReply: false,
CanRepost: true,
}
}
func createMoment(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
var data struct {
Alias string `json:"alias" form:"alias"`
Content string `json:"content" form:"content" validate:"required,max=1024"`
Hashtags []models.Tag `json:"hashtags" form:"hashtags"`
Categories []models.Category `json:"categories" form:"categories"`
Attachments []models.Attachment `json:"attachments" form:"attachments"`
PublishedAt *time.Time `json:"published_at" form:"published_at"`
RealmAlias string `json:"realm" form:"realm"`
RepostTo uint `json:"repost_to" form:"repost_to"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
} else if len(data.Alias) == 0 {
data.Alias = strings.ReplaceAll(uuid.NewString(), "-", "")
}
item := &models.Moment{
PostBase: models.PostBase{
Alias: data.Alias,
PublishedAt: data.PublishedAt,
AuthorID: user.ID,
},
Hashtags: data.Hashtags,
Categories: data.Categories,
Attachments: data.Attachments,
Content: data.Content,
}
var relatedCount int64
if data.RepostTo > 0 {
if err := database.C.Where("id = ?", data.RepostTo).
Model(&models.Moment{}).Count(&relatedCount).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else if relatedCount <= 0 {
return fiber.NewError(fiber.StatusNotFound, "related post was not found")
} else {
item.RepostID = &data.RepostTo
}
}
if len(data.RealmAlias) > 0 {
if realm, err := services.GetRealmWithAlias(data.RealmAlias); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else if _, err := services.GetRealmMember(realm.ExternalID, user.ExternalID); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("you aren't a part of related realm: %v", err))
} else {
item.RealmID = &realm.ID
}
}
item, err := services.NewPost(item)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(item)
}
func editMoment(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
id, _ := c.ParamsInt("momentId", 0)
var data struct {
Alias string `json:"alias" form:"alias" validate:"required"`
Content string `json:"content" form:"content" validate:"required,max=1024"`
PublishedAt *time.Time `json:"published_at" form:"published_at"`
Hashtags []models.Tag `json:"hashtags" form:"hashtags"`
Categories []models.Category `json:"categories" form:"categories"`
Attachments []models.Attachment `json:"attachments" form:"attachments"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
}
var item *models.Moment
if err := database.C.Where(models.Moment{
PostBase: models.PostBase{
BaseModel: models.BaseModel{ID: uint(id)},
AuthorID: user.ID,
},
}).First(&item).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
item.Alias = data.Alias
item.Content = data.Content
item.PublishedAt = data.PublishedAt
item.Hashtags = data.Hashtags
item.Categories = data.Categories
item.Attachments = data.Attachments
if item, err := services.EditPost(item); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.JSON(item)
}
}
func deleteMoment(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
id, _ := c.ParamsInt("momentId", 0)
var item *models.Moment
if err := database.C.Where(models.Moment{
PostBase: models.PostBase{
BaseModel: models.BaseModel{ID: uint(id)},
AuthorID: user.ID,
},
}).First(&item).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
if err := services.DeletePost(item); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.SendStatus(fiber.StatusOK)
}

View File

@ -2,47 +2,28 @@ package server
import (
"fmt"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"strings"
"time"
"git.solsynth.dev/hydrogen/interactive/pkg/database"
"git.solsynth.dev/hydrogen/interactive/pkg/models"
"git.solsynth.dev/hydrogen/interactive/pkg/services"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
)
var postContextKey = "ptx"
func useDynamicContext(c *fiber.Ctx) error {
postType := c.Params("postType")
switch postType {
case "articles":
c.Locals(postContextKey, contextArticle())
case "moments":
c.Locals(postContextKey, contextMoment())
case "comments":
c.Locals(postContextKey, contextComment())
default:
return fiber.NewError(fiber.StatusBadRequest, "invalid dataset")
}
return c.Next()
}
func getPost(c *fiber.Ctx) error {
alias := c.Params("postId")
alias := c.Params("post")
mx := c.Locals(postContextKey).(*services.PostTypeContext).
FilterPublishedAt(time.Now())
item, err := mx.GetViaAlias(alias)
item, err := services.GetPostWithAlias(alias)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
item.CommentCount = mx.CountComments(item.ID)
item.ReactionCount = mx.CountReactions(item.ID)
item.ReactionList, err = mx.ListReactions(item.ID)
item.ReplyCount = services.CountPostReply(item.ID)
item.ReactionCount = services.CountPostReactions(item.ID)
item.ReactionList, err = services.ListPostReactions(item.ID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
@ -55,36 +36,32 @@ func listPost(c *fiber.Ctx) error {
offset := c.QueryInt("offset", 0)
realmId := c.QueryInt("realmId", 0)
mx := c.Locals(postContextKey).(*services.PostTypeContext).
FilterPublishedAt(time.Now()).
FilterRealm(uint(realmId)).
SortCreatedAt("desc")
tx := database.C
if realmId > 0 {
tx = services.FilterWithRealm(tx, uint(realmId))
}
var author models.Account
if len(c.Query("authorId")) > 0 {
var author models.Account
if err := database.C.Where(&models.Account{Name: c.Query("authorId")}).First(&author).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
mx = mx.FilterAuthor(author.ID)
tx = tx.Where("author_id = ?", author.ID)
}
if len(c.Query("category")) > 0 {
mx = mx.FilterWithCategory(c.Query("category"))
tx = services.FilterPostWithCategory(tx, c.Query("category"))
}
if len(c.Query("tag")) > 0 {
mx = mx.FilterWithTag(c.Query("tag"))
tx = services.FilterPostWithTag(tx, c.Query("tag"))
}
if !c.QueryBool("reply", true) {
mx = mx.FilterReply(true)
}
count, err := mx.Count()
count, err := services.CountPost(tx)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
items, err := mx.List(take, offset)
items, err := services.ListPost(tx, take, offset)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
@ -95,6 +72,142 @@ func listPost(c *fiber.Ctx) error {
})
}
func createPost(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
var data struct {
Alias string `json:"alias" form:"alias"`
Content string `json:"content" form:"content" validate:"required,max=4096"`
Tags []models.Tag `json:"tags" form:"tags"`
Categories []models.Category `json:"categories" form:"categories"`
Attachments []uint `json:"attachments" form:"attachments"`
PublishedAt *time.Time `json:"published_at" form:"published_at"`
RealmAlias string `json:"realm" form:"realm"`
ReplyTo *uint `json:"reply_to" form:"reply_to"`
RepostTo *uint `json:"repost_to" form:"repost_to"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
} else if len(data.Alias) == 0 {
data.Alias = strings.ReplaceAll(uuid.NewString(), "-", "")
}
for _, attachment := range data.Attachments {
if !services.CheckAttachmentByIDExists(attachment, "i.attachment") {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("attachment %s not found", attachment))
}
}
item := models.Post{
Alias: data.Alias,
PublishedAt: data.PublishedAt,
AuthorID: user.ID,
Tags: data.Tags,
Categories: data.Categories,
Attachments: data.Attachments,
Content: data.Content,
}
if data.ReplyTo != nil {
var replyTo models.Post
if err := database.C.Where("id = ?", data.ReplyTo).First(&replyTo).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("related post was not found: %v", err))
} else {
item.ReplyID = &replyTo.ID
}
}
if data.RepostTo != nil {
var repostTo models.Post
if err := database.C.Where("id = ?", data.RepostTo).First(&repostTo).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("related post was not found: %v", err))
} else {
item.RepostID = &repostTo.ID
}
}
if len(data.RealmAlias) > 0 {
if realm, err := services.GetRealmWithAlias(data.RealmAlias); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else if _, err := services.GetRealmMember(realm.ExternalID, user.ExternalID); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("you aren't a part of related realm: %v", err))
} else {
item.RealmID = &realm.ID
}
}
item, err := services.NewPost(user, item)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(item)
}
func editPost(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
id, _ := c.ParamsInt("postId", 0)
var data struct {
Alias string `json:"alias" form:"alias" validate:"required"`
Content string `json:"content" form:"content" validate:"required,max=1024"`
PublishedAt *time.Time `json:"published_at" form:"published_at"`
Tags []models.Tag `json:"tags" form:"tags"`
Categories []models.Category `json:"categories" form:"categories"`
Attachments []uint `json:"attachments" form:"attachments"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
}
var item models.Post
if err := database.C.Where(models.Post{
BaseModel: models.BaseModel{ID: uint(id)},
AuthorID: user.ID,
}).First(&item).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
for _, attachment := range data.Attachments {
if !services.CheckAttachmentByIDExists(attachment, "i.attachment") {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("attachment %s not found", attachment))
}
}
item.Alias = data.Alias
item.Content = data.Content
item.PublishedAt = data.PublishedAt
item.Tags = data.Tags
item.Categories = data.Categories
item.Attachments = data.Attachments
if item, err := services.EditPost(item); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.JSON(item)
}
}
func deletePost(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
id, _ := c.ParamsInt("postId", 0)
var item models.Post
if err := database.C.Where(models.Post{
BaseModel: models.BaseModel{ID: uint(id)},
AuthorID: user.ID,
}).First(&item).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
if err := services.DeletePost(item); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.SendStatus(fiber.StatusOK)
}
func reactPost(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
@ -107,45 +220,22 @@ func reactPost(c *fiber.Ctx) error {
return err
}
mx := c.Locals(postContextKey).(*services.PostTypeContext)
reaction := models.Reaction{
Symbol: data.Symbol,
Attitude: data.Attitude,
AccountID: user.ID,
}
postType := c.Params("postType")
alias := c.Params("postId")
alias := c.Params("post")
var err error
var res models.Feed
switch postType {
case "moments":
err = database.C.Model(&models.Moment{}).Where("id = ?", alias).Select("id").First(&res).Error
case "articles":
err = database.C.Model(&models.Article{}).Where("id = ?", alias).Select("id").First(&res).Error
case "comments":
err = database.C.Model(&models.Comment{}).Where("id = ?", alias).Select("id").First(&res).Error
default:
return fiber.NewError(fiber.StatusBadRequest, "comment must belongs to a resource")
}
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("belongs to resource was not found: %v", err))
var res models.Post
if err := database.C.Where("alias = ?", alias).Select("id").First(&res).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to find post to react: %v", err))
} else {
switch postType {
case "moments":
reaction.MomentID = &res.ID
case "articles":
reaction.ArticleID = &res.ID
case "comments":
reaction.CommentID = &res.ID
}
reaction.PostID = &res.ID
}
if positive, reaction, err := mx.React(reaction); err != nil {
if positive, reaction, err := services.ReactPost(reaction); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.Status(lo.Ternary(positive, fiber.StatusCreated, fiber.StatusNoContent)).JSON(reaction)

52
pkg/server/replies_api.go Normal file
View File

@ -0,0 +1,52 @@
package server
import (
"fmt"
"git.solsynth.dev/hydrogen/interactive/pkg/database"
"git.solsynth.dev/hydrogen/interactive/pkg/models"
"git.solsynth.dev/hydrogen/interactive/pkg/services"
"github.com/gofiber/fiber/v2"
)
func listReplies(c *fiber.Ctx) error {
take := c.QueryInt("take", 0)
offset := c.QueryInt("offset", 0)
tx := database.C
var post models.Post
if err := database.C.Where("alias = ?", c.Params("post")).First(&post).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to find post: %v", err))
} else {
tx = services.FilterPostReply(tx, post.ID)
}
if len(c.Query("authorId")) > 0 {
var author models.Account
if err := database.C.Where(&models.Account{Name: c.Query("authorId")}).First(&author).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
tx = tx.Where("author_id = ?", author.ID)
}
if len(c.Query("category")) > 0 {
tx = services.FilterPostWithCategory(tx, c.Query("category"))
}
if len(c.Query("tag")) > 0 {
tx = services.FilterPostWithTag(tx, c.Query("tag"))
}
count, err := services.CountPost(tx)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
items, err := services.ListPost(tx, take, offset)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(fiber.Map{
"count": count,
"data": items,
})
}

View File

@ -62,42 +62,19 @@ func NewServer() {
{
api.Get("/users/me", authMiddleware, getUserinfo)
api.Get("/users/:accountId", getOthersInfo)
api.Get("/users/:accountId/follow", authMiddleware, getAccountFollowed)
api.Post("/users/:accountId/follow", authMiddleware, doFollowAccount)
api.Get("/attachments/o/:fileId", readAttachment)
api.Post("/attachments", authMiddleware, uploadAttachment)
api.Delete("/attachments/:id", authMiddleware, deleteAttachment)
api.Get("/feed", listFeed)
posts := api.Group("/p/:postType").Use(useDynamicContext).Name("Dataset Universal API")
posts := api.Group("/posts").Name("Posts API")
{
posts.Get("/", listPost)
posts.Get("/:postId", getPost)
posts.Post("/:postId/react", authMiddleware, reactPost)
posts.Get("/:postId/comments", listComment)
posts.Post("/:postId/comments", authMiddleware, createComment)
}
posts.Get("/:post", getPost)
posts.Post("/", authMiddleware, createPost)
posts.Post("/:post/react", authMiddleware, reactPost)
posts.Put("/:postId", authMiddleware, editPost)
posts.Delete("/:postId", authMiddleware, deletePost)
moments := api.Group("/p/moments").Name("Moments API")
{
moments.Post("/", authMiddleware, createMoment)
moments.Put("/:momentId", authMiddleware, editMoment)
moments.Delete("/:momentId", authMiddleware, deleteMoment)
}
articles := api.Group("/p/articles").Name("Articles API")
{
articles.Post("/", authMiddleware, createArticle)
articles.Put("/:articleId", authMiddleware, editArticle)
articles.Delete("/:articleId", authMiddleware, deleteArticle)
}
comments := api.Group("/p/comments").Name("Comments API")
{
comments.Put("/:commentId", authMiddleware, editComment)
comments.Delete("/:commentId", authMiddleware, deleteComment)
posts.Get("/:post/replies", listReplies)
}
api.Get("/categories", listCategories)

View File

@ -3,7 +3,6 @@ package server
import (
"git.solsynth.dev/hydrogen/interactive/pkg/database"
"git.solsynth.dev/hydrogen/interactive/pkg/models"
"git.solsynth.dev/hydrogen/interactive/pkg/services"
"github.com/gofiber/fiber/v2"
)
@ -32,45 +31,3 @@ func getOthersInfo(c *fiber.Ctx) error {
return c.JSON(data)
}
func getAccountFollowed(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
accountId, _ := c.ParamsInt("accountId", 0)
var data models.Account
if err := database.C.
Where(&models.Account{BaseModel: models.BaseModel{ID: uint(accountId)}}).
First(&data).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
_, status := services.GetAccountFollowed(user, data)
return c.JSON(fiber.Map{
"is_followed": status,
})
}
func doFollowAccount(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
id, _ := c.ParamsInt("accountId", 0)
var account models.Account
if err := database.C.Where(&models.Account{
BaseModel: models.BaseModel{ID: uint(id)},
}).First(&account).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
if _, ok := services.GetAccountFollowed(user, account); ok {
if err := services.UnfollowAccount(user.ID, account.ID); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.SendStatus(fiber.StatusNoContent)
} else {
if err := services.FollowAccount(user.ID, account.ID); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.SendStatus(fiber.StatusCreated)
}
}

View File

@ -11,30 +11,6 @@ import (
"time"
)
func FollowAccount(followerId, followingId uint) error {
relationship := models.AccountMembership{
FollowerID: followerId,
FollowingID: followingId,
}
return database.C.Create(&relationship).Error
}
func UnfollowAccount(followerId, followingId uint) error {
return database.C.Where(models.AccountMembership{
FollowerID: followerId,
FollowingID: followingId,
}).Delete(&models.AccountMembership{}).Error
}
func GetAccountFollowed(user models.Account, target models.Account) (models.AccountMembership, bool) {
var relationship models.AccountMembership
err := database.C.Model(&models.AccountMembership{}).
Where(&models.AccountMembership{FollowerID: user.ID, FollowingID: target.ID}).
First(&relationship).
Error
return relationship, err == nil
}
func GetAccountFriend(userId, relatedId uint, status int) (*proto.FriendshipResponse, error) {
var user models.Account
if err := database.C.Where("id = ?", userId).First(&user).Error; err != nil {

View File

@ -1,127 +1,29 @@
package services
import (
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
"git.solsynth.dev/hydrogen/interactive/pkg/database"
"git.solsynth.dev/hydrogen/interactive/pkg/models"
"github.com/google/uuid"
"github.com/spf13/viper"
"context"
"git.solsynth.dev/hydrogen/interactive/pkg/grpc"
pcpb "git.solsynth.dev/hydrogen/paperclip/pkg/grpc/proto"
"github.com/samber/lo"
)
func GetAttachmentByID(id uint) (models.Attachment, error) {
var attachment models.Attachment
if err := database.C.Where(models.Attachment{
BaseModel: models.BaseModel{ID: id},
}).First(&attachment).Error; err != nil {
return attachment, err
}
return attachment, nil
func GetAttachmentByID(id uint) (*pcpb.Attachment, error) {
return grpc.Attachments.GetAttachment(context.Background(), &pcpb.AttachmentLookupRequest{
Id: lo.ToPtr(uint64(id)),
})
}
func GetAttachmentByUUID(fileId string) (models.Attachment, error) {
var attachment models.Attachment
if err := database.C.Where(models.Attachment{
FileID: fileId,
}).First(&attachment).Error; err != nil {
return attachment, err
}
return attachment, nil
func GetAttachmentByUUID(uuid string) (*pcpb.Attachment, error) {
return grpc.Attachments.GetAttachment(context.Background(), &pcpb.AttachmentLookupRequest{
Uuid: &uuid,
})
}
func GetAttachmentByHashcode(hashcode string) (models.Attachment, error) {
var attachment models.Attachment
if err := database.C.Where(models.Attachment{
Hashcode: hashcode,
}).First(&attachment).Error; err != nil {
return attachment, err
}
return attachment, nil
}
func CheckAttachmentByIDExists(id uint, usage string) bool {
_, err := grpc.Attachments.CheckAttachmentExists(context.Background(), &pcpb.AttachmentLookupRequest{
Id: lo.ToPtr(uint64(id)),
Usage: &usage,
})
func NewAttachment(user models.Account, header *multipart.FileHeader, hashcode string) (models.Attachment, error) {
var attachment models.Attachment
existsAttachment, err := GetAttachmentByHashcode(hashcode)
if err != nil {
// Upload the new file
attachment = models.Attachment{
FileID: uuid.NewString(),
Filesize: header.Size,
Filename: header.Filename,
Hashcode: hashcode,
Mimetype: "unknown/unknown",
Type: models.AttachmentOthers,
AuthorID: user.ID,
}
// Open file
file, err := header.Open()
if err != nil {
return attachment, err
}
defer file.Close()
// Detect mimetype
fileHeader := make([]byte, 512)
_, err = file.Read(fileHeader)
if err != nil {
return attachment, err
}
attachment.Mimetype = http.DetectContentType(fileHeader)
switch strings.Split(attachment.Mimetype, "/")[0] {
case "image":
attachment.Type = models.AttachmentPhoto
case "video":
attachment.Type = models.AttachmentVideo
case "audio":
attachment.Type = models.AttachmentAudio
default:
attachment.Type = models.AttachmentOthers
}
} else {
// Instant upload, build link with the exists file
attachment = models.Attachment{
FileID: existsAttachment.FileID,
Filesize: header.Size,
Filename: header.Filename,
Hashcode: hashcode,
Mimetype: existsAttachment.Mimetype,
Type: existsAttachment.Type,
AuthorID: user.ID,
}
}
// Save into database
err = database.C.Save(&attachment).Error
return attachment, err
}
func DeleteAttachment(item models.Attachment) error {
var dupeCount int64
if err := database.C.
Where(&models.Attachment{Hashcode: item.Hashcode}).
Model(&models.Attachment{}).
Count(&dupeCount).Error; err != nil {
dupeCount = -1
}
if err := database.C.Delete(&item).Error; err != nil {
return err
}
if dupeCount != -1 && dupeCount <= 1 {
// Safe for deletion the physics file
basepath := viper.GetString("content")
fullpath := filepath.Join(basepath, item.FileID)
os.Remove(fullpath)
}
return nil
return err == nil
}

View File

@ -11,7 +11,7 @@ func DoAutoDatabaseCleanup() {
log.Debug().Time("deadline", deadline).Msg("Now cleaning up entire database...")
var count int64
for _, model := range database.DatabaseAutoActionRange {
for _, model := range database.AutoMaintainRange {
tx := database.C.Unscoped().Delete(model, "deleted_at >= ?", deadline)
if tx.Error != nil {
log.Error().Err(tx.Error).Msg("An error occurred when running auth context cleanup...")

View File

@ -1,107 +0,0 @@
package services
import (
"fmt"
"git.solsynth.dev/hydrogen/passport/pkg/grpc/proto"
"github.com/rs/zerolog/log"
"time"
"git.solsynth.dev/hydrogen/interactive/pkg/database"
"git.solsynth.dev/hydrogen/interactive/pkg/models"
"github.com/samber/lo"
"github.com/spf13/viper"
)
func (v *PostTypeContext) ListComment(id uint, take int, offset int, noReact ...bool) ([]*models.Feed, error) {
if take > 20 {
take = 20
}
var items []*models.Feed
table := viper.GetString("database.prefix") + "comments"
userTable := viper.GetString("database.prefix") + "accounts"
if err := v.Tx.
Table(table).
Select("*, ? as model_type", "comment").
Where(v.ColumnName+"_id = ?", id).
Joins(fmt.Sprintf("INNER JOIN %s as author ON author_id = author.id", userTable)).
Limit(take).Offset(offset).Find(&items).Error; err != nil {
return items, err
}
idx := lo.Map(items, func(item *models.Feed, index int) uint {
return item.ID
})
if len(noReact) <= 0 || !noReact[0] {
var reactions []struct {
PostID uint
Symbol string
Count int64
}
if err := database.C.Model(&models.Reaction{}).
Select("comment_id as post_id, symbol, COUNT(id) as count").
Where("comment_id IN (?)", idx).
Group("post_id, symbol").
Scan(&reactions).Error; err != nil {
return items, err
}
itemMap := lo.SliceToMap(items, func(item *models.Feed) (uint, *models.Feed) {
return item.ID, item
})
list := map[uint]map[string]int64{}
for _, info := range reactions {
if _, ok := list[info.PostID]; !ok {
list[info.PostID] = make(map[string]int64)
}
list[info.PostID][info.Symbol] = info.Count
}
for k, v := range list {
if post, ok := itemMap[k]; ok {
post.ReactionList = v
}
}
}
return items, nil
}
func (v *PostTypeContext) CountComment(id uint) (int64, error) {
var count int64
if err := database.C.
Model(&models.Comment{}).
Where(v.ColumnName+"_id = ?", id).
Where("published_at <= ?", time.Now()).
Count(&count).Error; err != nil {
return count, err
}
return count, nil
}
func CommentNotify(this models.PostInterface, original models.Feed, columnName, tableName string) {
var op models.Feed
if err := database.C.
Where(columnName+"_id = ?", original.ID).
Preload("Author").
Table(tableName).
First(&op).Error; err == nil {
if op.Author.ID != this.GetAuthor().ID {
postUrl := fmt.Sprintf("https://%s/posts/%d", viper.GetString("domain"), this.GetID())
err := NotifyAccount(
op.Author,
fmt.Sprintf("%s commented you", this.GetAuthor().Name),
fmt.Sprintf("%s commented your post. Check it out!", this.GetAuthor().Name),
false,
&proto.NotifyLink{Label: "Related post", Url: postUrl},
)
if err != nil {
log.Error().Err(err).Msg("An error occurred when notifying user...")
}
}
}
}

View File

@ -1 +0,0 @@
package services

View File

@ -14,129 +14,93 @@ import (
"gorm.io/gorm"
)
type PostTypeContext struct {
Tx *gorm.DB
TableName string
ColumnName string
CanReply bool
CanRepost bool
func FilterPostWithCategory(tx *gorm.DB, alias string) *gorm.DB {
return tx.Joins("JOIN post_categories ON posts.id = post_categories.post_id").
Joins("JOIN post_categories ON post_categories.id = post_categories.category_id").
Where("post_categories.alias = ?", alias)
}
func (v *PostTypeContext) FilterWithCategory(alias string) *PostTypeContext {
name := v.ColumnName
v.Tx.Joins(fmt.Sprintf("JOIN %s_categories ON %s.id = %s_categories.%s_id", name, v.TableName, name, name)).
Joins(fmt.Sprintf("JOIN %s_categories ON %s_categories.id = %s_categories.category_id", name, name, name)).
Where(name+"_categories.alias = ?", alias)
return v
func FilterPostWithTag(tx *gorm.DB, alias string) *gorm.DB {
return tx.Joins("JOIN post_tags ON posts.id = post_tags.post_id").
Joins("JOIN post_tags ON post_tags.id = post_tags.category_id").
Where("post_tags.alias = ?", alias)
}
func (v *PostTypeContext) FilterWithTag(alias string) *PostTypeContext {
name := v.ColumnName
v.Tx.Joins(fmt.Sprintf("JOIN %s_tags ON %s.id = %s_tags.%s_id", name, v.TableName, name, name)).
Joins(fmt.Sprintf("JOIN %s_tags ON %s_tags.id = %s_tags.category_id", name, name, name)).
Where(name+"_tags.alias = ?", alias)
return v
}
func (v *PostTypeContext) FilterPublishedAt(date time.Time) *PostTypeContext {
v.Tx.Where("published_at <= ? AND published_at IS NULL", date)
return v
}
func (v *PostTypeContext) FilterRealm(id uint) *PostTypeContext {
func FilterWithRealm(tx *gorm.DB, id uint) *gorm.DB {
if id > 0 {
v.Tx = v.Tx.Where("realm_id = ?", id)
return tx.Where("realm_id = ?", id)
} else {
v.Tx = v.Tx.Where("realm_id IS NULL")
return tx.Where("realm_id IS NULL")
}
return v
}
func (v *PostTypeContext) FilterAuthor(id uint) *PostTypeContext {
v.Tx = v.Tx.Where("author_id = ?", id)
return v
}
func (v *PostTypeContext) FilterReply(condition bool) *PostTypeContext {
if condition {
v.Tx = v.Tx.Where("reply_id IS NOT NULL")
func FilterPostReply(tx *gorm.DB, replyTo ...uint) *gorm.DB {
if len(replyTo) > 0 && replyTo[0] > 0 {
return tx.Where("reply_id = ?", replyTo[0])
} else {
v.Tx = v.Tx.Where("reply_id IS NULL")
return tx.Where("reply_id IS NULL")
}
return v
}
func (v *PostTypeContext) SortCreatedAt(order string) *PostTypeContext {
v.Tx.Order(fmt.Sprintf("created_at %s", order))
return v
func FilterPostWithPublishedAt(tx *gorm.DB, date time.Time) *gorm.DB {
return tx.Where("published_at <= ? OR published_at IS NULL", date)
}
func (v *PostTypeContext) GetViaAlias(alias string) (models.Feed, error) {
var item models.Feed
table := viper.GetString("database.prefix") + v.TableName
userTable := viper.GetString("database.prefix") + "accounts"
if err := v.Tx.
Table(table).
Select("*, ? as model_type", v.ColumnName).
Joins(fmt.Sprintf("INNER JOIN %s AS author ON author_id = author.id", userTable)).
func GetPostWithAlias(alias string, ignoreLimitation ...bool) (models.Post, error) {
tx := database.C
if len(ignoreLimitation) == 0 || !ignoreLimitation[0] {
tx = FilterPostWithPublishedAt(tx, time.Now())
}
var item models.Post
if err := tx.
Where("alias = ?", alias).
Preload("Author").
Preload("ReplyTo").
Preload("ReplyTo.Author").
Preload("RepostTo").
Preload("RepostTo.Author").
First(&item).Error; err != nil {
return item, err
}
var attachments []models.Attachment
if err := database.C.
Model(&models.Attachment{}).
Where(v.ColumnName+"_id = ?", item.ID).
Scan(&attachments).Error; err != nil {
return item, nil
}
func GetPost(id uint, ignoreLimitation ...bool) (models.Post, error) {
tx := database.C
if len(ignoreLimitation) == 0 || !ignoreLimitation[0] {
tx = FilterPostWithPublishedAt(tx, time.Now())
}
var item models.Post
if err := tx.
Where("id = ?", id).
Preload("Author").
Preload("ReplyTo").
Preload("ReplyTo.Author").
Preload("RepostTo").
Preload("RepostTo.Author").
First(&item).Error; err != nil {
return item, err
} else {
item.Attachments = attachments
}
return item, nil
}
func (v *PostTypeContext) Get(id uint, noComments ...bool) (models.Feed, error) {
var item models.Feed
table := viper.GetString("database.prefix") + v.TableName
userTable := viper.GetString("database.prefix") + "accounts"
if err := v.Tx.
Table(table).
Select("*, ? as model_type", v.ColumnName).
Joins(fmt.Sprintf("INNER JOIN %s AS author ON author_id = author.id", userTable)).
Where("id = ?", id).First(&item).Error; err != nil {
return item, err
}
var attachments []models.Attachment
if err := database.C.
Model(&models.Attachment{}).
Where(v.ColumnName+"_id = ?", id).
Scan(&attachments).Error; err != nil {
return item, err
} else {
item.Attachments = attachments
}
return item, nil
}
func (v *PostTypeContext) Count() (int64, error) {
func CountPost(tx *gorm.DB) (int64, error) {
var count int64
table := viper.GetString("database.prefix") + v.TableName
if err := v.Tx.Table(table).Count(&count).Error; err != nil {
if err := tx.Model(&models.Post{}).Count(&count).Error; err != nil {
return count, err
}
return count, nil
}
func (v *PostTypeContext) CountComments(id uint) int64 {
func CountPostReply(id uint) int64 {
var count int64
if err := database.C.Model(&models.Comment{}).
Where(v.ColumnName+"_id = ?", id).
if err := database.C.Model(&models.Post{}).
Where("reply_id = ?", id).
Count(&count).Error; err != nil {
return 0
}
@ -144,10 +108,10 @@ func (v *PostTypeContext) CountComments(id uint) int64 {
return count
}
func (v *PostTypeContext) CountReactions(id uint) int64 {
func CountPostReactions(id uint) int64 {
var count int64
if err := database.C.Model(&models.Reaction{}).
Where(v.ColumnName+"_id = ?", id).
Where("post_id = ?", id).
Count(&count).Error; err != nil {
return 0
}
@ -155,7 +119,7 @@ func (v *PostTypeContext) CountReactions(id uint) int64 {
return count
}
func (v *PostTypeContext) ListReactions(id uint) (map[string]int64, error) {
func ListPostReactions(id uint) (map[string]int64, error) {
var reactions []struct {
Symbol string
Count int64
@ -163,7 +127,7 @@ func (v *PostTypeContext) ListReactions(id uint) (map[string]int64, error) {
if err := database.C.Model(&models.Reaction{}).
Select("symbol, COUNT(id) as count").
Where(v.ColumnName+"_id = ?", id).
Where("post_id = ?", id).
Group("symbol").
Scan(&reactions).Error; err != nil {
return map[string]int64{}, err
@ -178,21 +142,25 @@ func (v *PostTypeContext) ListReactions(id uint) (map[string]int64, error) {
}), nil
}
func (v *PostTypeContext) List(take int, offset int, noReact ...bool) ([]*models.Feed, error) {
func ListPost(tx *gorm.DB, take int, offset int, noReact ...bool) ([]*models.Post, error) {
if take > 20 {
take = 20
}
var items []*models.Feed
table := viper.GetString("database.prefix") + v.TableName
if err := v.Tx.
Table(table).
Select("*, ? as model_type", v.ColumnName).
Limit(take).Offset(offset).Find(&items).Error; err != nil {
var items []*models.Post
if err := tx.
Limit(take).Offset(offset).
Order("created_at DESC").
Preload("Author").
Preload("ReplyTo").
Preload("ReplyTo.Author").
Preload("RepostTo").
Preload("RepostTo.Author").
Find(&items).Error; err != nil {
return items, err
}
idx := lo.Map(items, func(item *models.Feed, index int) uint {
idx := lo.Map(items, func(item *models.Post, index int) uint {
return item.ID
})
@ -204,14 +172,14 @@ func (v *PostTypeContext) List(take int, offset int, noReact ...bool) ([]*models
}
if err := database.C.Model(&models.Reaction{}).
Select(v.ColumnName+"_id as post_id, symbol, COUNT(id) as count").
Where(v.ColumnName+"_id IN (?)", idx).
Select("post_id, symbol, COUNT(id) as count").
Where("post_id IN (?)", idx).
Group("post_id, symbol").
Scan(&reactions).Error; err != nil {
return items, err
}
itemMap := lo.SliceToMap(items, func(item *models.Feed) (uint, *models.Feed) {
itemMap := lo.SliceToMap(items, func(item *models.Post) (uint, *models.Post) {
return item.ID, item
})
@ -230,37 +198,32 @@ func (v *PostTypeContext) List(take int, offset int, noReact ...bool) ([]*models
}
}
{
var attachments []struct {
models.Attachment
PostID uint `json:"post_id"`
if len(noReact) <= 0 || !noReact[0] {
var replies []struct {
PostID uint
Count int64
}
itemMap := lo.SliceToMap(items, func(item *models.Feed) (uint, *models.Feed) {
return item.ID, item
})
idx := lo.Map(items, func(item *models.Feed, index int) uint {
return item.ID
})
if err := database.C.
Model(&models.Attachment{}).
Select(v.ColumnName+"_id as post_id, *").
Where(v.ColumnName+"_id IN (?)", idx).
Scan(&attachments).Error; err != nil {
if err := database.C.Model(&models.Post{}).
Select("id as post_id, COUNT(id) as count").
Where("reply_id IN (?)", idx).
Group("post_id").
Scan(&replies).Error; err != nil {
return items, err
}
list := map[uint][]models.Attachment{}
for _, info := range attachments {
list[info.PostID] = append(list[info.PostID], info.Attachment)
itemMap := lo.SliceToMap(items, func(item *models.Post) (uint, *models.Post) {
return item.ID, item
})
list := map[uint]int64{}
for _, info := range replies {
list[info.PostID] = info.Count
}
for k, v := range list {
if post, ok := itemMap[k]; ok {
post.Attachments = v
post.ReplyCount = v
}
}
}
@ -268,35 +231,31 @@ func (v *PostTypeContext) List(take int, offset int, noReact ...bool) ([]*models
return items, nil
}
func MapCategoriesAndTags[T models.PostInterface](item T) (T, error) {
func InitPostCategoriesAndTags(item models.Post) (models.Post, error) {
var err error
categories := item.GetCategories()
for idx, category := range categories {
categories[idx], err = GetCategory(category.Alias)
for idx, category := range item.Categories {
item.Categories[idx], err = GetCategory(category.Alias)
if err != nil {
return item, err
}
}
item.SetCategories(categories)
tags := item.GetHashtags()
for idx, tag := range tags {
tags[idx], err = GetTagOrCreate(tag.Alias, tag.Name)
for idx, tag := range item.Tags {
item.Tags[idx], err = GetTagOrCreate(tag.Alias, tag.Name)
if err != nil {
return item, err
}
}
item.SetHashtags(tags)
return item, nil
}
func NewPost[T models.PostInterface](item T) (T, error) {
item, err := MapCategoriesAndTags(item)
func NewPost(user models.Account, item models.Post) (models.Post, error) {
item, err := InitPostCategoriesAndTags(item)
if err != nil {
return item, err
}
if item.GetRealm() != nil {
_, err := GetRealmMember(item.GetRealm().ID, item.GetAuthor().ExternalID)
if item.RealmID != nil {
_, err := GetRealmMember(*item.RealmID, user.ExternalID)
if err != nil {
return item, fmt.Errorf("you aren't a part of that realm: %v", err)
}
@ -306,19 +265,20 @@ func NewPost[T models.PostInterface](item T) (T, error) {
return item, err
}
if item.GetReplyTo() != nil {
// Notify the original poster its post has been replied
if item.ReplyID != nil {
go func() {
var op models.Moment
var op models.Post
if err := database.C.
Where("id = ?", item.GetReplyTo()).
Where("id = ?", item.ReplyID).
Preload("Author").
First(&op).Error; err == nil {
if op.Author.ID != item.GetAuthor().ID {
postUrl := fmt.Sprintf("https://%s/posts/%d", viper.GetString("domain"), item.GetID())
if op.Author.ID != user.ID {
postUrl := fmt.Sprintf("https://%s/posts/%s", viper.GetString("domain"), item.Alias)
err := NotifyAccount(
op.Author,
fmt.Sprintf("%s replied you", item.GetAuthor().Name),
fmt.Sprintf("%s replied your post. Check it out!", item.GetAuthor().Name),
fmt.Sprintf("%s replied you", user.Nick),
fmt.Sprintf("%s (%s) replied your post #%s.", user.Nick, user.Name, op.Alias),
false,
&proto.NotifyLink{Label: "Related post", Url: postUrl},
)
@ -330,36 +290,11 @@ func NewPost[T models.PostInterface](item T) (T, error) {
}()
}
var subscribers []models.AccountMembership
if err := database.C.Where(&models.AccountMembership{
FollowingID: item.GetAuthor().ID,
}).Preload("Follower").Find(&subscribers).Error; err == nil && len(subscribers) > 0 {
go func() {
accounts := lo.Map(subscribers, func(item models.AccountMembership, index int) models.Account {
return item.Follower
})
for _, account := range accounts {
postUrl := fmt.Sprintf("https://%s/posts/%d", viper.GetString("domain"), item.GetID())
err := NotifyAccount(
account,
fmt.Sprintf("%s just posted a post", item.GetAuthor().Name),
"Someone you followed post a brand new post. Check it out!",
false,
&proto.NotifyLink{Label: "Related post", Url: postUrl},
)
if err != nil {
log.Error().Err(err).Msg("An error occurred when notifying user...")
}
}
}()
}
return item, nil
}
func EditPost[T models.PostInterface](item T) (T, error) {
item, err := MapCategoriesAndTags(item)
func EditPost(item models.Post) (models.Post, error) {
item, err := InitPostCategoriesAndTags(item)
if err != nil {
return item, err
}
@ -369,11 +304,11 @@ func EditPost[T models.PostInterface](item T) (T, error) {
return item, err
}
func DeletePost[T models.PostInterface](item T) error {
func DeletePost(item models.Post) error {
return database.C.Delete(&item).Error
}
func (v *PostTypeContext) React(reaction models.Reaction) (bool, models.Reaction, error) {
func ReactPost(reaction models.Reaction) (bool, models.Reaction, error) {
if err := database.C.Where(reaction).First(&reaction).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return true, reaction, database.C.Save(&reaction).Error

View File

@ -4,20 +4,22 @@ maintainer = "SmartSheep Studio"
frontend = "https://lian.solsynth.dev"
bind = "0.0.0.0:8445"
domain = "feed.smartsheep.studio"
domain = "im.solsynth.dev"
secret = "LtTjzAGFLshwXhN4ZD4nG5KlMv1MWcsvfv03TSZYnT1VhiAnLIZFTnHUwR0XhGgi"
content = "uploads"
[debug]
database = false
print_routes = false
[paperclip]
endpoint = "http://localhost:8443"
grpc_endpoint = "localhost:7443"
[passport]
client_id = "solarplaza"
client_secret = "Z9k9AFTj^p"
endpoint = "http://localhost:8444"
grpc_endpoint = "127.0.0.1:7444"
grpc_endpoint = "localhost:7444"
[mailer]
name = "Alphabot <alphabot@smartsheep.studio>"