diff --git a/go.mod b/go.mod index d1cefcf78..10d93267e 100644 --- a/go.mod +++ b/go.mod @@ -5,31 +5,53 @@ go 1.16 require ( github.com/buckket/go-blurhash v1.1.0 github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect + github.com/dsoprea/go-exif v0.0.0-20210428042052-dca55bf8ca15 // indirect + github.com/dsoprea/go-exif/v2 v2.0.0-20210428042052-dca55bf8ca15 // indirect + github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413 // indirect + github.com/dsoprea/go-jpeg-image-structure v0.0.0-20210505113650-8010c634293c // indirect + github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect + github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d // indirect + github.com/dsoprea/go-png-image-structure v0.0.0-20210428043356-45b892641b59 // indirect + github.com/dsoprea/go-utility v0.0.0-20200717064901-2fccff4aa15e // indirect + github.com/gin-contrib/cors v1.3.1 github.com/gin-contrib/sessions v0.0.3 - github.com/gin-gonic/gin v1.6.3 - github.com/go-fed/activity v1.0.0 - github.com/go-fed/httpsig v0.1.1-0.20190914113940-c2de3672e5b5 + github.com/gin-gonic/gin v1.7.1 + github.com/go-errors/errors v1.2.0 // indirect + github.com/go-fed/activity v1.0.1-0.20210426194615-e0de0863dcc1 + github.com/go-fed/httpsig v1.1.0 github.com/go-pg/pg/extra/pgdebug v0.2.0 - github.com/go-pg/pg/v10 v10.8.0 - github.com/golang/mock v1.4.4 // indirect + github.com/go-pg/pg/v10 v10.9.1 + github.com/go-playground/validator/v10 v10.6.0 // indirect + github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect + github.com/golang/mock v1.5.0 // indirect github.com/google/uuid v1.2.0 + github.com/gorilla/sessions v1.2.1 // indirect github.com/h2non/filetype v1.1.1 + github.com/json-iterator/go v1.1.11 // indirect + github.com/leodido/go-urn v1.2.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 - github.com/onsi/ginkgo v1.15.0 // indirect - github.com/onsi/gomega v1.10.5 // indirect - github.com/sirupsen/logrus v1.8.0 + github.com/onsi/gomega v1.12.0 // indirect + github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sirupsen/logrus v1.8.1 + github.com/stretchr/objx v0.3.0 // indirect github.com/stretchr/testify v1.7.0 github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 github.com/superseriousbusiness/oauth2/v4 v4.2.1-0.20210327102222-902aba1ef45f - github.com/tidwall/btree v0.4.2 // indirect - github.com/tidwall/buntdb v1.2.0 // indirect - github.com/tidwall/pretty v1.1.0 // indirect + github.com/tidwall/btree v0.5.0 // indirect + github.com/tidwall/buntdb v1.2.3 // indirect + github.com/ugorji/go v1.2.5 // indirect github.com/urfave/cli/v2 v2.3.0 + github.com/vmihailenco/msgpack/v5 v5.3.1 // indirect github.com/wagslane/go-password-validator v0.3.0 - golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b - golang.org/x/text v0.3.3 + go.opentelemetry.io/otel v0.20.0 // indirect + golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf + golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect + golang.org/x/sys v0.0.0-20210511113859-b0526f3d8744 // indirect + golang.org/x/text v0.3.6 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect - gopkg.in/yaml.v2 v2.3.0 + gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.sum b/go.sum index 7a8514c2a..078157692 100644 --- a/go.sum +++ b/go.sum @@ -20,24 +20,33 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dsoprea/go-exif v0.0.0-20210131231135-d154f10435cc h1:AuzYp98IFVOi0NU/WcZyGDQ6vAh/zkCjxGD3kt8aLzA= github.com/dsoprea/go-exif v0.0.0-20210131231135-d154f10435cc/go.mod h1:lOaOt7+UEppOgyvRy749v3do836U/hw0YVJNjoyPaEs= +github.com/dsoprea/go-exif v0.0.0-20210428042052-dca55bf8ca15 h1:uqmD+m+8q7afXhhtABSab5ZMWpy0L+Vi7p/SDDNIMbs= +github.com/dsoprea/go-exif v0.0.0-20210428042052-dca55bf8ca15/go.mod h1:lOaOt7+UEppOgyvRy749v3do836U/hw0YVJNjoyPaEs= github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E= -github.com/dsoprea/go-exif/v2 v2.0.0-20200604193436-ca8584a0e1c4 h1:Mg7pY7kxDQD2Bkvr1N+XW4BESSIQ7tTTR7Vv+Gi2CsM= github.com/dsoprea/go-exif/v2 v2.0.0-20200604193436-ca8584a0e1c4/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0= -github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb h1:gwjJjUr6FY7zAWVEueFPrcRHhd9+IK81TcItbqw2du4= +github.com/dsoprea/go-exif/v2 v2.0.0-20210428042052-dca55bf8ca15 h1:a73ubT6QCaR0G6ZfkA0i3ecR+bB3OFCa9VoKjZT8H24= +github.com/dsoprea/go-exif/v2 v2.0.0-20210428042052-dca55bf8ca15/go.mod h1:oKrjk2kb3rAR5NbtSTLUMvMSbc+k8ZosI3MaVH47noc= github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM= -github.com/dsoprea/go-jpeg-image-structure v0.0.0-20210128210355-86b1014917f2 h1:ULCSN6v0WISNbALxomGPXh4dSjRKPW+7+seYoMz8UTc= +github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413 h1:YDRiMEm32T60Kpm35YzOK9ZHgjsS1Qrid+XskNcsdp8= +github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM= github.com/dsoprea/go-jpeg-image-structure v0.0.0-20210128210355-86b1014917f2/go.mod h1:ZoOP3yUG0HD1T4IUjIFsz/2OAB2yB4YX6NSm4K+uJRg= +github.com/dsoprea/go-jpeg-image-structure v0.0.0-20210505113650-8010c634293c h1:g2vhZhMoEz2oqTPT5xV1pvOc93KXMeRsz2dSeVDG0zs= +github.com/dsoprea/go-jpeg-image-structure v0.0.0-20210505113650-8010c634293c/go.mod h1:ZoOP3yUG0HD1T4IUjIFsz/2OAB2yB4YX6NSm4K+uJRg= github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696/go.mod h1:Nm/x2ZUNRW6Fe5C3LxdY1PyZY5wmDv/s5dkPJ/VB3iA= -github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d h1:F/7L5wr/fP/SKeO5HuMlNEX9Ipyx2MbH2rV9G4zJRpk= github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8= -github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c h1:7j5aWACOzROpr+dvMtu8GnI97g9ShLWD72XIELMgn+c= +github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd h1:l+vLbuxptsC6VQyQsfD7NnEC8BZuFpz45PgY+pH8YTg= +github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8= github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E= -github.com/dsoprea/go-png-image-structure v0.0.0-20200807080309-a98d4e94ac82 h1:RdwKOEEe2ND/JmoKh6I/EQlR9idKJTDOMffPFK6vN2M= +github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d h1:dg6UMHa50VI01WuPWXPbNJpO8QSyvIF5T5n2IZiqX3A= +github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E= github.com/dsoprea/go-png-image-structure v0.0.0-20200807080309-a98d4e94ac82/go.mod h1:aDYQkL/5gfRNZkoxiLTSWU4Y8/gV/4MVsy/MU9uwTak= -github.com/dsoprea/go-utility v0.0.0-20200512094054-1abbbc781176 h1:CfXezFYb2STGOd1+n1HshvE191zVx+QX3A1nML5xxME= +github.com/dsoprea/go-png-image-structure v0.0.0-20210428043356-45b892641b59 h1:4CJr4z+gM6jmak9k6vzMWwj+cM8jYSFje+AxTDns1PA= +github.com/dsoprea/go-png-image-structure v0.0.0-20210428043356-45b892641b59/go.mod h1:aDYQkL/5gfRNZkoxiLTSWU4Y8/gV/4MVsy/MU9uwTak= github.com/dsoprea/go-utility v0.0.0-20200512094054-1abbbc781176/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8= +github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8= +github.com/dsoprea/go-utility v0.0.0-20200717064901-2fccff4aa15e h1:ojqYA1mU6LuRm8XzrVOvyfb000y59cbUcu6Wt8sFSAs= +github.com/dsoprea/go-utility v0.0.0-20200717064901-2fccff4aa15e/go.mod h1:KVK+/Hul09ujXAGq+42UBgCTnXkiJZRnLYdURGjQUwo= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= @@ -48,26 +57,30 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= +github.com/gin-contrib/cors v1.3.1 h1:doAsuITavI4IOcd0Y19U4B+O0dNWihRyX//nn4sEmgA= +github.com/gin-contrib/cors v1.3.1/go.mod h1:jjEJ4268OPZUcU7k9Pm653S7lXUGcqMADzFA61xsmDk= github.com/gin-contrib/sessions v0.0.3 h1:PoBXki+44XdJdlgDqDrY5nDVe3Wk7wDV/UCOuLP6fBI= github.com/gin-contrib/sessions v0.0.3/go.mod h1:8C/J6cad3Il1mWYYgtw0w+hqasmpvy25mPkXdOgeB9I= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= -github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= -github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/gin-gonic/gin v1.7.1 h1:qC89GU3p8TvKWMAVhEpmpB2CIb1hnqt2UdKZaP93mS8= +github.com/gin-gonic/gin v1.7.1/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= -github.com/go-errors/errors v1.0.2 h1:xMxH9j2fNg/L4hLn/4y3M0IUsn0M6Wbu/Uh9QlOfBh4= github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= -github.com/go-fed/activity v1.0.0 h1:j7w3auHZnVCjUcgA1mE+UqSOjFBhvW2Z2res3vNol+o= -github.com/go-fed/activity v1.0.0/go.mod h1:v4QoPaAzjWZ8zN2VFVGL5ep9C02mst0hQYHUpQwso4Q= -github.com/go-fed/httpsig v0.1.1-0.20190914113940-c2de3672e5b5 h1:WLvFZqoXnuVTBKA6U/1FnEHNQ0Rq0QM0rGhY8Tx6R1g= +github.com/go-errors/errors v1.2.0 h1:g5NHvR3mlTvaIa23r4xj7JAHlIhdVhOK8rEOGauEMCY= +github.com/go-errors/errors v1.2.0/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= +github.com/go-fed/activity v1.0.1-0.20210426194615-e0de0863dcc1 h1:go9MogQW0eTLwdOs/ZfNCGpwUkVcr7IMUbI3u8wYQxw= +github.com/go-fed/activity v1.0.1-0.20210426194615-e0de0863dcc1/go.mod h1:v4QoPaAzjWZ8zN2VFVGL5ep9C02mst0hQYHUpQwso4Q= github.com/go-fed/httpsig v0.1.1-0.20190914113940-c2de3672e5b5/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE= +github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= +github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= github.com/go-pg/pg/extra/pgdebug v0.2.0 h1:t62UhMiV6KYAxSWojwIJiyX06TdepkzCeIzdeb00184= github.com/go-pg/pg/extra/pgdebug v0.2.0/go.mod h1:KmW//PLshMAQunfInLv9mFIbYXuGplOY9bc6qo3CaY0= github.com/go-pg/pg/v10 v10.6.2/go.mod h1:BfgPoQnD2wXNd986RYEHzikqv9iE875PrFaZ9vXvtNM= -github.com/go-pg/pg/v10 v10.8.0 h1:7L1VmOwW/VMmPtz5K3TWWMdM68MDgRs8Yb3c3NTMNgI= -github.com/go-pg/pg/v10 v10.8.0/go.mod h1:0ZZA18+5xlUPvKjlDxoMyU79ZSuJtI+EeM2/GEd4RVo= +github.com/go-pg/pg/v10 v10.9.1 h1:kU4t84zWGGaU0Qsu49FbNtToUVrlSTkNOngW8aQmwvk= +github.com/go-pg/pg/v10 v10.9.1/go.mod h1:rgmTPgHgl5EN2CNKKoMwC7QT62t8BqsdpEkUQuiZMQs= github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU= github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= @@ -78,20 +91,23 @@ github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTM github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= -github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-playground/validator/v10 v10.6.0 h1:UGIt4xR++fD9QrBOoo/ascJfGe3AGHEB9s6COnss4Rk= +github.com/go-playground/validator/v10 v10.6.0/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg= github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b h1:khEcpUM4yFcxg4/FHQWkvVRmgijNXRfzkIDHh23ggEo= github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= -github.com/golang/geo v0.0.0-20200319012246-673a6f80352d h1:C/hKUcHT483btRbeGkrRjJz+Zbcj8audldIi9tRJDCc= github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= +github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo= +github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -104,8 +120,10 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W 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.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= 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 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= 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= @@ -113,8 +131,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw 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.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -128,8 +146,9 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51 github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= -github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU= github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/h2non/filetype v1.1.1 h1:xvOwnXKAckvtLWsN398qS9QhlxlnVXBjXBydK2/UFB4= @@ -141,8 +160,9 @@ github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJS github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= @@ -157,10 +177,9 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= -github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/magefile/mage v1.10.0 h1:3HiXzCUY12kh9bIuyXShaVe529fJfyqoVM42o/uom2g= -github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= @@ -177,39 +196,42 @@ github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOA github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.15.0 h1:1V1NfVQR87RtWAgp1lv9JZJ5Jap+XFGKPi00andXGi4= -github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= +github.com/onsi/ginkgo v1.16.2 h1:HFB2fbVIlhIfCfOW81bZFbiC/RvnpXSdhbF2/DJr134= +github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= -github.com/onsi/gomega v1.10.5 h1:7n6FEkpFmfCoo2t+YYqXH0evK+a9ICQz0xcAy9dYcaQ= -github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= +github.com/onsi/gomega v1.12.0 h1:p4oGGk2M2UJc0wWN4lHFvIB71lxsh0T/UiKCCgFADY8= +github.com/onsi/gomega v1.12.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438 h1:jnz/4VenymvySjE+Ez511s0pqVzkUOmr1fwCVytNNWk= github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b h1:aUNXCGgukb4gtY99imuIeoh8Vr0GSwAlYxPAhqZrpFc= +github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.8.0 h1:nfhvjKcUMhBMVqbKHJlk5RPrrfYr/NMo3692g0dwfWU= -github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= +github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -222,26 +244,25 @@ github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203/go github.com/superseriousbusiness/oauth2/v4 v4.2.1-0.20210327102222-902aba1ef45f h1:0YcjA/ieDuDFHJPg5w2hk3r5kIWNvEyl7GsoArxdI3s= github.com/superseriousbusiness/oauth2/v4 v4.2.1-0.20210327102222-902aba1ef45f/go.mod h1:8p0a/BEN9hhsGzE3tPaFFlIZgxAaLyLN5KY0bPg9ZBc= github.com/tidwall/btree v0.0.0-20191029221954-400434d76274/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8= -github.com/tidwall/btree v0.3.0/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8= -github.com/tidwall/btree v0.4.2 h1:aLwwJlG+InuFzdAPuBf9YCAR1LvSQ9zhC5aorFPlIPs= github.com/tidwall/btree v0.4.2/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8= +github.com/tidwall/btree v0.5.0 h1:IBfCtOj4uOMQcodv3wzYVo0zPqSJObm71mE039/dlXY= +github.com/tidwall/btree v0.5.0/go.mod h1:TzIRzen6yHbibdSfK6t8QimqbUnoxUSrZfeW7Uob0q4= github.com/tidwall/buntdb v1.1.2/go.mod h1:xAzi36Hir4FarpSHyfuZ6JzPJdjRZ8QlLZSntE2mqlI= -github.com/tidwall/buntdb v1.2.0 h1:8KOzf5Gg97DoCMSOgcwZjnM0FfROtq0fcZkPW54oGKU= -github.com/tidwall/buntdb v1.2.0/go.mod h1:XLza/dhlwzO6dc5o/KWor4kfZSt3BP8QV+77ZMKfI58= +github.com/tidwall/buntdb v1.2.3 h1:AoGVe4yrhKmnEPHrPrW5EUOATHOCIk4VtFvd8xn/ZtU= +github.com/tidwall/buntdb v1.2.3/go.mod h1:+i/gBwYOHWG19wLgwMXFLkl00twh9+VWkkaOhuNQ4PA= github.com/tidwall/gjson v1.3.4/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= -github.com/tidwall/gjson v1.6.7/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI= -github.com/tidwall/gjson v1.6.8 h1:CTmXMClGYPAmln7652e69B7OLXfTi5ABcPPwjIWUv7w= -github.com/tidwall/gjson v1.6.8/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI= +github.com/tidwall/gjson v1.7.4 h1:19cchw8FOxkG5mdLRkGf9jqIqEyqdZhPqW60XfyFxk8= +github.com/tidwall/gjson v1.7.4/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk= github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M= -github.com/tidwall/grect v0.1.0 h1:ICcKWD5uu5A5fmxApGIa0QRvfGnSWKRd07POT08CQSA= -github.com/tidwall/grect v0.1.0/go.mod h1:sa5O42oP6jWfTShL9ka6Sgmg3TgIK649veZe05B7+J8= +github.com/tidwall/grect v0.1.1 h1:+kMEkxhoqB7rniVXzMEIA66XwU07STgINqxh+qVIndY= +github.com/tidwall/grect v0.1.1/go.mod h1:CzvbGiFbWUwiJ1JohXLb28McpyBsI00TK9Y6pDWLGRQ= +github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8= github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE= github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.0.1/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.1.0 h1:K3hMW5epkdAVwibsQEfR/7Zj0Qgt4DxtNumTq/VloO8= github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8= @@ -252,10 +273,12 @@ github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaym github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= -github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go v1.2.5 h1:NozRHfUeEta89taVkyfsDVSy2f7v89Frft4pjnWuGuc= +github.com/ugorji/go v1.2.5/go.mod h1:gat2tIT8KJG8TVI8yv77nEO/KYT6dV7JE1gfUa8Xuls= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go/codec v1.2.5 h1:8WobZKAk18Msm2CothY2jnztY56YVY8kF1oQrj21iis= +github.com/ugorji/go/codec v1.2.5/go.mod h1:QPxoTbPKSEAlAHPYt02++xp/en9B/wUdwFCz+hj5caA= github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -267,8 +290,9 @@ github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6cz github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ= github.com/vmihailenco/msgpack/v4 v4.3.11/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1/go.mod h1:xlngVLeyQ/Qi05oQxhQ+oTuqa03RjMwMfk/7/TCs+QI= -github.com/vmihailenco/msgpack/v5 v5.2.0 h1:ZhIAtVUP1mme8GIlpiAnmTzjSWMexA/uNF2We85DR0w= -github.com/vmihailenco/msgpack/v5 v5.2.0/go.mod h1:fEM7KuHcnm0GvDCztRpw9hV0PuoO2ciTismP6vjggcM= +github.com/vmihailenco/msgpack/v5 v5.3.0/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/msgpack/v5 v5.3.1 h1:0i85a4dsZh8mC//wmyyTEzidDLPQfQAxZIOLtafGbFY= +github.com/vmihailenco/msgpack/v5 v5.3.1/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= @@ -291,23 +315,27 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDf github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/otel v0.13.0/go.mod h1:dlSNewoRYikTkotEnxdmuBHgzT+k/idJSfDv/FxEnOY= -go.opentelemetry.io/otel v0.18.0 h1:d5Of7+Zw4ANFOJB+TIn2K3QWsgS2Ht7OU9DqZHI6qu8= -go.opentelemetry.io/otel v0.18.0/go.mod h1:PT5zQj4lTsR1YeARt8YNKcFb88/c2IKoSABK9mX0r78= -go.opentelemetry.io/otel/metric v0.18.0 h1:yuZCmY9e1ZTaMlZXLrrbAPmYW6tW1A5ozOZeOYGaTaY= -go.opentelemetry.io/otel/metric v0.18.0/go.mod h1:kEH2QtzAyBy3xDVQfGZKIcok4ZZFvd5xyKPfPcuK6pE= -go.opentelemetry.io/otel/oteltest v0.18.0 h1:FbKDFm/LnQDOHuGjED+fy3s5YMVg0z019GJ9Er66hYo= -go.opentelemetry.io/otel/oteltest v0.18.0/go.mod h1:NyierCU3/G8DLTva7KRzGii2fdxdR89zXKH1bNWY7Bo= -go.opentelemetry.io/otel/trace v0.18.0 h1:ilCfc/fptVKaDMK1vWk0elxpolurJbEgey9J6g6s+wk= -go.opentelemetry.io/otel/trace v0.18.0/go.mod h1:FzdUu3BPwZSZebfQ1vl5/tAa8LyMLXSJN57AXIt/iDk= +go.opentelemetry.io/otel v0.19.0/go.mod h1:j9bF567N9EfomkSidSfmMwIwIBuP37AMAIzVW85OxSg= +go.opentelemetry.io/otel v0.20.0 h1:eaP0Fqu7SXHwvjiqDq83zImeehOHX8doTvU9AwXON8g= +go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= +go.opentelemetry.io/otel/metric v0.19.0/go.mod h1:8f9fglJPRnXuskQmKpnad31lcLJ2VmNNqIsx/uIwBSc= +go.opentelemetry.io/otel/metric v0.20.0 h1:4kzhXFP+btKm4jwxpjIqjs41A7MakRFUS86bqLHTIw8= +go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= +go.opentelemetry.io/otel/oteltest v0.19.0/go.mod h1:tI4yxwh8U21v7JD6R3BcA/2+RBoTKFexE/PJ/nSO7IA= +go.opentelemetry.io/otel/oteltest v0.20.0 h1:HiITxCawalo5vQzdHfKeZurV8x7ljcqAgiWzF6Vaeaw= +go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= +go.opentelemetry.io/otel/trace v0.19.0/go.mod h1:4IXiNextNOpPnRlI4ryK69mn5iC84bjBWZQA5DXz/qg= +go.opentelemetry.io/otel/trace v0.20.0 h1:1DL6EXUdcg95gukhuRRvLDO/4X5THh/5dIV52lqtnbw= +go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b h1:wSOdpTq0/eI46Ez/LkDwIsAKA71YP2SRKBODiRWM0as= -golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf h1:B2n+Zi5QeYRDAEodEu72OS36gmTWjgpXr2+cWcBW90o= +golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 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= @@ -333,10 +361,10 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I= +golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -361,20 +389,22 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305034016-7844c3c200c3 h1:RdE7htvBru4I4VZQofQjCZk5W9+aLNlSF5n0zgVwm8s= -golang.org/x/sys v0.0.0-20210305034016-7844c3c200c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210511113859-b0526f3d8744 h1:yhBbb4IRs2HS9PPlAg6DMC6mUOKexJBNsLf4Z+6En1Q= +golang.org/x/sys v0.0.0-20210511113859-b0526f3d8744/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/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-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-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.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -401,8 +431,10 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi 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 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= 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 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -419,11 +451,13 @@ gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= mellium.im/sasl v0.2.1 h1:nspKSRg7/SyO0cRGY71OkfHab8tf9kCts6a6oTDut0w= diff --git a/internal/api/client/auth/auth_test.go b/internal/api/client/auth/auth_test.go index 7ec788a0e..48d2a2508 100644 --- a/internal/api/client/auth/auth_test.go +++ b/internal/api/client/auth/auth_test.go @@ -28,6 +28,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/db/pg" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/oauth" "golang.org/x/crypto/bcrypt" @@ -103,7 +104,7 @@ func (suite *AuthTestSuite) SetupTest() { log := logrus.New() log.SetLevel(logrus.TraceLevel) - db, err := db.NewPostgresService(context.Background(), suite.config, log) + db, err := pg.NewPostgresService(context.Background(), suite.config, log) if err != nil { logrus.Panicf("error creating database connection: %s", err) } diff --git a/internal/api/client/auth/middleware.go b/internal/api/client/auth/middleware.go index 8313b7c28..2a63cbdb6 100644 --- a/internal/api/client/auth/middleware.go +++ b/internal/api/client/auth/middleware.go @@ -33,7 +33,7 @@ func (m *Module) OauthTokenMiddleware(c *gin.Context) { l := m.log.WithField("func", "OauthTokenMiddleware") l.Trace("entering OauthTokenMiddleware") - ti, err := m.server.ValidationBearerToken(c.Request) + ti, err := m.server.ValidationBearerToken(c.Copy().Request) if err != nil { l.Tracef("could not validate token: %s", err) return @@ -74,4 +74,5 @@ func (m *Module) OauthTokenMiddleware(c *gin.Context) { c.Set(oauth.SessionAuthorizedApplication, app) l.Tracef("set gin context %s to %+v", oauth.SessionAuthorizedApplication, app) } + c.Next() } diff --git a/internal/api/client/auth/token.go b/internal/api/client/auth/token.go index c531a3009..798a88d19 100644 --- a/internal/api/client/auth/token.go +++ b/internal/api/client/auth/token.go @@ -20,16 +20,46 @@ package auth import ( "net/http" + "net/url" "github.com/gin-gonic/gin" ) +type tokenBody struct { + ClientID *string `form:"client_id" json:"client_id" xml:"client_id"` + ClientSecret *string `form:"client_secret" json:"client_secret" xml:"client_secret"` + Code *string `form:"code" json:"code" xml:"code"` + GrantType *string `form:"grant_type" json:"grant_type" xml:"grant_type"` + RedirectURI *string `form:"redirect_uri" json:"redirect_uri" xml:"redirect_uri"` +} + // TokenPOSTHandler should be served as a POST at https://example.org/oauth/token // The idea here is to serve an oauth access token to a user, which can be used for authorizing against non-public APIs. // See https://docs.joinmastodon.org/methods/apps/oauth/#obtain-a-token func (m *Module) TokenPOSTHandler(c *gin.Context) { l := m.log.WithField("func", "TokenPOSTHandler") l.Trace("entered TokenPOSTHandler") + + form := &tokenBody{} + if err := c.ShouldBind(form); err == nil { + c.Request.Form = url.Values{} + if form.ClientID != nil { + c.Request.Form.Set("client_id", *form.ClientID) + } + if form.ClientSecret != nil { + c.Request.Form.Set("client_secret", *form.ClientSecret) + } + if form.Code != nil { + c.Request.Form.Set("code", *form.Code) + } + if form.GrantType != nil { + c.Request.Form.Set("grant_type", *form.GrantType) + } + if form.RedirectURI != nil { + c.Request.Form.Set("redirect_uri", *form.RedirectURI) + } + } + if err := m.server.HandleTokenRequest(c.Writer, c.Request); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } diff --git a/internal/api/client/followrequest/accept.go b/internal/api/client/followrequest/accept.go new file mode 100644 index 000000000..45dc1a2af --- /dev/null +++ b/internal/api/client/followrequest/accept.go @@ -0,0 +1,57 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package followrequest + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +// FollowRequestAcceptPOSTHandler deals with follow request accepting. It should be served at +// /api/v1/follow_requests/:id/authorize +func (m *Module) FollowRequestAcceptPOSTHandler(c *gin.Context) { + l := m.log.WithField("func", "statusCreatePOSTHandler") + authed, err := oauth.Authed(c, true, true, true, true) + if err != nil { + l.Debugf("couldn't auth: %s", err) + c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + return + } + + if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { + l.Debugf("couldn't auth: %s", err) + c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"}) + return + } + + originAccountID := c.Param(IDKey) + if originAccountID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "no follow request origin account id provided"}) + return + } + + if errWithCode := m.processor.FollowRequestAccept(authed, originAccountID); errWithCode != nil { + l.Debug(errWithCode.Error()) + c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + return + } + c.Status(http.StatusOK) +} diff --git a/internal/db/pg_test.go b/internal/api/client/followrequest/deny.go similarity index 74% rename from internal/db/pg_test.go rename to internal/api/client/followrequest/deny.go index a54784022..c1a9e4dbf 100644 --- a/internal/db/pg_test.go +++ b/internal/api/client/followrequest/deny.go @@ -16,6 +16,12 @@ along with this program. If not, see . */ -package db_test +package followrequest -// TODO: write tests for postgres +import "github.com/gin-gonic/gin" + +// FollowRequestDenyPOSTHandler deals with follow request rejection. It should be served at +// /api/v1/follow_requests/:id/reject +func (m *Module) FollowRequestDenyPOSTHandler(c *gin.Context) { + +} diff --git a/internal/api/client/followrequest/followrequest.go b/internal/api/client/followrequest/followrequest.go new file mode 100644 index 000000000..8be957009 --- /dev/null +++ b/internal/api/client/followrequest/followrequest.go @@ -0,0 +1,68 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package followrequest + +import ( + "net/http" + + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/message" + "github.com/superseriousbusiness/gotosocial/internal/router" +) + +const ( + // IDKey is for status UUIDs + IDKey = "id" + // BasePath is the base path for serving the follow request API + BasePath = "/api/v1/follow_requests" + // BasePathWithID is just the base path with the ID key in it. + // Use this anywhere you need to know the ID of the follow request being queried. + BasePathWithID = BasePath + "/:" + IDKey + + // AcceptPath is used for accepting follow requests + AcceptPath = BasePathWithID + "/authorize" + // DenyPath is used for denying follow requests + DenyPath = BasePathWithID + "/reject" +) + +// Module implements the ClientAPIModule interface for every related to interacting with follow requests +type Module struct { + config *config.Config + processor message.Processor + log *logrus.Logger +} + +// New returns a new follow request module +func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.ClientModule { + return &Module{ + config: config, + processor: processor, + log: log, + } +} + +// Route attaches all routes from this module to the given router +func (m *Module) Route(r router.Router) error { + r.AttachHandler(http.MethodGet, BasePath, m.FollowRequestGETHandler) + r.AttachHandler(http.MethodPost, AcceptPath, m.FollowRequestAcceptPOSTHandler) + r.AttachHandler(http.MethodPost, DenyPath, m.FollowRequestDenyPOSTHandler) + return nil +} diff --git a/internal/api/client/followrequest/get.go b/internal/api/client/followrequest/get.go new file mode 100644 index 000000000..3f02ee02a --- /dev/null +++ b/internal/api/client/followrequest/get.go @@ -0,0 +1,51 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package followrequest + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +// FollowRequestGETHandler allows clients to get a list of their incoming follow requests. +func (m *Module) FollowRequestGETHandler(c *gin.Context) { + l := m.log.WithField("func", "statusCreatePOSTHandler") + authed, err := oauth.Authed(c, true, true, true, true) + if err != nil { + l.Debugf("couldn't auth: %s", err) + c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + return + } + + if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { + l.Debugf("couldn't auth: %s", err) + c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"}) + return + } + + accts, errWithCode := m.processor.FollowRequestsGet(authed) + if errWithCode != nil { + c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + return + } + + c.JSON(http.StatusOK, accts) +} diff --git a/internal/api/client/instance/instance.go b/internal/api/client/instance/instance.go index ed7c18718..ba54480a5 100644 --- a/internal/api/client/instance/instance.go +++ b/internal/api/client/instance/instance.go @@ -11,7 +11,7 @@ import ( ) const ( - // InstanceInformationPath + // InstanceInformationPath is for serving instance info requests InstanceInformationPath = "api/v1/instance" ) diff --git a/internal/api/client/instance/instanceget.go b/internal/api/client/instance/instanceget.go index f8e82c096..6ae419a83 100644 --- a/internal/api/client/instance/instanceget.go +++ b/internal/api/client/instance/instanceget.go @@ -6,6 +6,7 @@ import ( "github.com/gin-gonic/gin" ) +// InstanceInformationGETHandler is for serving instance information at /api/v1/instance func (m *Module) InstanceInformationGETHandler(c *gin.Context) { l := m.log.WithField("func", "InstanceInformationGETHandler") diff --git a/internal/api/client/media/media.go b/internal/api/client/media/media.go index e45eec7ea..f68a73c2c 100644 --- a/internal/api/client/media/media.go +++ b/internal/api/client/media/media.go @@ -33,8 +33,10 @@ import ( // BasePath is the base API path for making media requests const BasePath = "/api/v1/media" + // IDKey is the key for media attachment IDs const IDKey = "id" + // BasePathWithID corresponds to a media attachment with the given ID const BasePathWithID = BasePath + "/:" + IDKey diff --git a/internal/api/client/media/mediacreate.go b/internal/api/client/media/mediacreate.go index c0b4a80d7..9f4702b6b 100644 --- a/internal/api/client/media/mediacreate.go +++ b/internal/api/client/media/mediacreate.go @@ -35,30 +35,32 @@ func (m *Module) MediaCreatePOSTHandler(c *gin.Context) { authed, err := oauth.Authed(c, true, true, true, true) // posting new media is serious business so we want *everything* if err != nil { l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) return } // extract the media create form from the request context l.Tracef("parsing request form: %s", c.Request.Form) - var form model.AttachmentRequest + form := &model.AttachmentRequest{} if err := c.ShouldBind(&form); err != nil { - l.Debugf("could not parse form from request: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"}) + l.Debugf("error parsing form: %s", err) + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("could not parse form: %s", err)}) return } // Give the fields on the request form a first pass to make sure the request is superficially valid. l.Tracef("validating form %+v", form) - if err := validateCreateMedia(&form, m.config.MediaConfig); err != nil { + if err := validateCreateMedia(form, m.config.MediaConfig); err != nil { l.Debugf("error validating form: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) return } - mastoAttachment, err := m.processor.MediaCreate(authed, &form) + l.Debug("calling processor media create func") + mastoAttachment, err := m.processor.MediaCreate(authed, form) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + l.Debugf("error creating attachment: %s", err) + c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) return } @@ -67,7 +69,7 @@ func (m *Module) MediaCreatePOSTHandler(c *gin.Context) { func validateCreateMedia(form *model.AttachmentRequest, config *config.MediaConfig) error { // check there actually is a file attached and it's not size 0 - if form.File == nil || form.File.Size == 0 { + if form.File == nil { return errors.New("no attachment given") } diff --git a/internal/api/client/media/mediaget.go b/internal/api/client/media/mediaget.go index 31c40a5aa..7acb475ce 100644 --- a/internal/api/client/media/mediaget.go +++ b/internal/api/client/media/mediaget.go @@ -43,7 +43,7 @@ func (m *Module) MediaGETHandler(c *gin.Context) { attachment, errWithCode := m.processor.MediaGet(authed, attachmentID) if errWithCode != nil { - c.JSON(errWithCode.Code(),gin.H{"error": errWithCode.Safe()}) + c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) return } diff --git a/internal/api/model/application.go b/internal/api/model/application.go index a796c88ea..fe9fada03 100644 --- a/internal/api/model/application.go +++ b/internal/api/model/application.go @@ -43,13 +43,13 @@ type Application struct { // And here: https://docs.joinmastodon.org/client/token/ type ApplicationCreateRequest struct { // A name for your application - ClientName string `form:"client_name" binding:"required"` + ClientName string `form:"client_name" json:"client_name" xml:"client_name" binding:"required"` // Where the user should be redirected after authorization. // To display the authorization code to the user instead of redirecting // to a web page, use urn:ietf:wg:oauth:2.0:oob in this parameter. - RedirectURIs string `form:"redirect_uris" binding:"required"` + RedirectURIs string `form:"redirect_uris" json:"redirect_uris" xml:"redirect_uris" binding:"required"` // Space separated list of scopes. If none is provided, defaults to read. - Scopes string `form:"scopes"` + Scopes string `form:"scopes" json:"scopes" xml:"scopes"` // A URL to the homepage of your app - Website string `form:"website"` + Website string `form:"website" json:"website" xml:"website"` } diff --git a/internal/api/model/attachment.go b/internal/api/model/attachment.go index c5dbb0cba..ed53757eb 100644 --- a/internal/api/model/attachment.go +++ b/internal/api/model/attachment.go @@ -24,15 +24,15 @@ import "mime/multipart" // See: https://docs.joinmastodon.org/methods/statuses/media/ type AttachmentRequest struct { File *multipart.FileHeader `form:"file" binding:"required"` - Description string `form:"description" json:"description" xml:"description"` - Focus string `form:"focus" json:"focus" xml:"focus"` + Description string `form:"description"` + Focus string `form:"focus"` } -// AttachmentRequest represents the form data parameters submitted by a client during a media update/PUT request. +// AttachmentUpdateRequest represents the form data parameters submitted by a client during a media update/PUT request. // See: https://docs.joinmastodon.org/methods/statuses/media/ type AttachmentUpdateRequest struct { - Description *string `form:"description" json:"description" xml:"description"` - Focus *string `form:"focus" json:"focus" xml:"focus"` + Description *string `form:"description" json:"description" xml:"description"` + Focus *string `form:"focus" json:"focus" xml:"focus"` } // Attachment represents the object returned to a client after a successful media upload request. @@ -63,7 +63,7 @@ type Attachment struct { // See https://docs.joinmastodon.org/methods/statuses/media/#focal-points points for more. Meta MediaMeta `json:"meta,omitempty"` // Alternate text that describes what is in the media attachment, to be used for the visually impaired or when media attachments do not load. - Description string `json:"description"` + Description string `json:"description,omitempty"` // A hash computed by the BlurHash algorithm, for generating colorful preview thumbnails when media has not been downloaded yet. // See https://github.com/woltapp/blurhash Blurhash string `json:"blurhash,omitempty"` diff --git a/internal/api/model/status.go b/internal/api/model/status.go index 54d021e29..2cb22aa0d 100644 --- a/internal/api/model/status.go +++ b/internal/api/model/status.go @@ -119,11 +119,15 @@ const ( VisibilityDirect Visibility = "direct" ) +// AdvancedStatusCreateForm wraps the mastodon status create form along with the GTS advanced +// visibility settings. type AdvancedStatusCreateForm struct { StatusCreateRequest AdvancedVisibilityFlagsForm } +// AdvancedVisibilityFlagsForm allows a few more advanced flags to be set on new statuses, in addition +// to the standard mastodon-compatible ones. type AdvancedVisibilityFlagsForm struct { // The gotosocial visibility model VisibilityAdvanced *string `form:"visibility_advanced"` diff --git a/internal/api/s2s/user/inboxpost.go b/internal/api/s2s/user/inboxpost.go new file mode 100644 index 000000000..60b74ab70 --- /dev/null +++ b/internal/api/s2s/user/inboxpost.go @@ -0,0 +1,58 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package user + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/message" +) + +// InboxPOSTHandler deals with incoming POST requests to an actor's inbox. +// Eg., POST to https://example.org/users/whatever/inbox. +func (m *Module) InboxPOSTHandler(c *gin.Context) { + l := m.log.WithFields(logrus.Fields{ + "func": "InboxPOSTHandler", + "url": c.Request.RequestURI, + }) + + requestedUsername := c.Param(UsernameKey) + if requestedUsername == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) + return + } + + posted, err := m.processor.InboxPost(c.Request.Context(), c.Writer, c.Request) + if err != nil { + if withCode, ok := err.(message.ErrorWithCode); ok { + l.Debug(withCode.Error()) + c.JSON(withCode.Code(), withCode.Safe()) + return + } + l.Debug(err) + c.JSON(http.StatusBadRequest, gin.H{"error": "unable to process request"}) + return + } + + if !posted { + c.JSON(http.StatusBadRequest, gin.H{"error": "unable to process request"}) + } +} diff --git a/internal/api/s2s/user/user.go b/internal/api/s2s/user/user.go index 693fac7c3..a6116247d 100644 --- a/internal/api/s2s/user/user.go +++ b/internal/api/s2s/user/user.go @@ -38,6 +38,8 @@ const ( // Use this anywhere you need to know the username of the user being queried. // Eg https://example.org/users/:username UsersBasePathWithUsername = UsersBasePath + "/:" + UsernameKey + // UsersInboxPath is for serving POST requests to a user's inbox with the given username key. + UsersInboxPath = UsersBasePathWithUsername + "/" + util.InboxPath ) // ActivityPubAcceptHeaders represents the Accept headers mentioned here: @@ -66,5 +68,6 @@ func New(config *config.Config, processor message.Processor, log *logrus.Logger) // Route satisfies the RESTAPIModule interface func (m *Module) Route(s router.Router) error { s.AttachHandler(http.MethodGet, UsersBasePathWithUsername, m.UsersGETHandler) + s.AttachHandler(http.MethodPost, UsersInboxPath, m.InboxPOSTHandler) return nil } diff --git a/internal/api/s2s/webfinger/webfinger.go b/internal/api/s2s/webfinger/webfinger.go index c11d3fb61..168fe1e76 100644 --- a/internal/api/s2s/webfinger/webfinger.go +++ b/internal/api/s2s/webfinger/webfinger.go @@ -29,7 +29,7 @@ import ( ) const ( - // The base path for serving webfinger lookup requests + // WebfingerBasePath is the base path for serving webfinger lookup requests WebfingerBasePath = ".well-known/webfinger" ) diff --git a/internal/api/security/extraheaders.go b/internal/api/security/extraheaders.go new file mode 100644 index 000000000..dfcddfbe1 --- /dev/null +++ b/internal/api/security/extraheaders.go @@ -0,0 +1,8 @@ +package security + +import "github.com/gin-gonic/gin" + +// ExtraHeaders adds any additional required headers to the response +func (m *Module) ExtraHeaders(c *gin.Context) { + c.Header("Server", "Mastodon") +} diff --git a/internal/api/security/security.go b/internal/api/security/security.go index c80b568b3..eaae8471e 100644 --- a/internal/api/security/security.go +++ b/internal/api/security/security.go @@ -42,5 +42,6 @@ func New(config *config.Config, log *logrus.Logger) api.ClientModule { // Route attaches security middleware to the given router func (m *Module) Route(s router.Router) error { s.AttachMiddleware(m.FlocBlock) + s.AttachMiddleware(m.ExtraHeaders) return nil } diff --git a/internal/db/db.go b/internal/db/db.go index b281dd8d7..a354ddee8 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -26,7 +26,10 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) -const DBTypePostgres string = "POSTGRES" +const ( + // DBTypePostgres represents an underlying POSTGRES database type. + DBTypePostgres string = "POSTGRES" +) // ErrNoEntries is to be returned from the DB interface when no entries are found for a given query. type ErrNoEntries struct{} @@ -112,6 +115,10 @@ type DB interface { HANDY SHORTCUTS */ + // AcceptFollowRequest moves a follow request in the database from the follow_requests table to the follows table. + // In other words, it should create the follow, and delete the existing follow request. + AcceptFollowRequest(originAccountID string, targetAccountID string) error + // CreateInstanceAccount creates an account in the database with the same username as the instance host value. // Ie., if the instance is hosted at 'example.org' the instance user will have a username of 'example.org'. // This is needed for things like serving files that belong to the instance and not an individual user/account. @@ -148,6 +155,11 @@ type DB interface { // In case of no entries, a 'no entries' error will be returned GetFollowersByAccountID(accountID string, followers *[]gtsmodel.Follow) error + // GetFavesByAccountID is a shortcut for the common action of fetching a list of faves made by the given accountID. + // The given slice 'faves' will be set to the result of the query, whatever it is. + // In case of no entries, a 'no entries' error will be returned + GetFavesByAccountID(accountID string, faves *[]gtsmodel.StatusFave) error + // GetStatusesByAccountID is a shortcut for the common action of fetching a list of statuses produced by accountID. // The given slice 'statuses' will be set to the result of the query, whatever it is. // In case of no entries, a 'no entries' error will be returned diff --git a/internal/db/pg.go b/internal/db/pg/pg.go similarity index 94% rename from internal/db/pg.go rename to internal/db/pg/pg.go index c0fbcc9e0..f8c2fdbe8 100644 --- a/internal/db/pg.go +++ b/internal/db/pg/pg.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package db +package pg import ( "context" @@ -37,6 +37,8 @@ import ( "github.com/google/uuid" "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/federation" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/util" "golang.org/x/crypto/bcrypt" @@ -53,7 +55,7 @@ type postgresService struct { // NewPostgresService returns a postgresService derived from the provided config, which implements the go-fed DB interface. // Under the hood, it uses https://github.com/go-pg/pg to create and maintain a database connection. -func NewPostgresService(ctx context.Context, c *config.Config, log *logrus.Logger) (DB, error) { +func NewPostgresService(ctx context.Context, c *config.Config, log *logrus.Logger) (db.DB, error) { opts, err := derivePGOptions(c) if err != nil { return nil, fmt.Errorf("could not create postgres service: %s", err) @@ -95,7 +97,7 @@ func NewPostgresService(ctx context.Context, c *config.Config, log *logrus.Logge cancel: cancel, } - federatingDB := NewFederatingDB(ps, c, log) + federatingDB := federation.NewFederatingDB(ps, c, log) ps.federationDB = federatingDB // we can confidently return this useable postgres service now @@ -109,8 +111,8 @@ func NewPostgresService(ctx context.Context, c *config.Config, log *logrus.Logge // derivePGOptions takes an application config and returns either a ready-to-use *pg.Options // with sensible defaults, or an error if it's not satisfied by the provided config. func derivePGOptions(c *config.Config) (*pg.Options, error) { - if strings.ToUpper(c.DBConfig.Type) != DBTypePostgres { - return nil, fmt.Errorf("expected db type of %s but got %s", DBTypePostgres, c.DBConfig.Type) + if strings.ToUpper(c.DBConfig.Type) != db.DBTypePostgres { + return nil, fmt.Errorf("expected db type of %s but got %s", db.DBTypePostgres, c.DBConfig.Type) } // validate port @@ -219,7 +221,7 @@ func (ps *postgresService) CreateSchema(ctx context.Context) error { func (ps *postgresService) GetByID(id string, i interface{}) error { if err := ps.conn.Model(i).Where("id = ?", id).Select(); err != nil { if err == pg.ErrNoRows { - return ErrNoEntries{} + return db.ErrNoEntries{} } return err @@ -230,7 +232,7 @@ func (ps *postgresService) GetByID(id string, i interface{}) error { func (ps *postgresService) GetWhere(key string, value interface{}, i interface{}) error { if err := ps.conn.Model(i).Where("? = ?", pg.Safe(key), value).Select(); err != nil { if err == pg.ErrNoRows { - return ErrNoEntries{} + return db.ErrNoEntries{} } return err } @@ -244,7 +246,7 @@ func (ps *postgresService) GetWhere(key string, value interface{}, i interface{} func (ps *postgresService) GetAll(i interface{}) error { if err := ps.conn.Model(i).Select(); err != nil { if err == pg.ErrNoRows { - return ErrNoEntries{} + return db.ErrNoEntries{} } return err } @@ -259,7 +261,7 @@ func (ps *postgresService) Put(i interface{}) error { func (ps *postgresService) Upsert(i interface{}, conflictColumn string) error { if _, err := ps.conn.Model(i).OnConflict(fmt.Sprintf("(%s) DO UPDATE", conflictColumn)).Insert(); err != nil { if err == pg.ErrNoRows { - return ErrNoEntries{} + return db.ErrNoEntries{} } return err } @@ -269,7 +271,7 @@ func (ps *postgresService) Upsert(i interface{}, conflictColumn string) error { func (ps *postgresService) UpdateByID(id string, i interface{}) error { if _, err := ps.conn.Model(i).Where("id = ?", id).OnConflict("(id) DO UPDATE").Insert(); err != nil { if err == pg.ErrNoRows { - return ErrNoEntries{} + return db.ErrNoEntries{} } return err } @@ -284,7 +286,7 @@ func (ps *postgresService) UpdateOneByID(id string, key string, value interface{ func (ps *postgresService) DeleteByID(id string, i interface{}) error { if _, err := ps.conn.Model(i).Where("id = ?", id).Delete(); err != nil { if err == pg.ErrNoRows { - return ErrNoEntries{} + return db.ErrNoEntries{} } return err } @@ -294,7 +296,7 @@ func (ps *postgresService) DeleteByID(id string, i interface{}) error { func (ps *postgresService) DeleteWhere(key string, value interface{}, i interface{}) error { if _, err := ps.conn.Model(i).Where("? = ?", pg.Safe(key), value).Delete(); err != nil { if err == pg.ErrNoRows { - return ErrNoEntries{} + return db.ErrNoEntries{} } return err } @@ -305,6 +307,32 @@ func (ps *postgresService) DeleteWhere(key string, value interface{}, i interfac HANDY SHORTCUTS */ +func (ps *postgresService) AcceptFollowRequest(originAccountID string, targetAccountID string) error { + fr := >smodel.FollowRequest{} + if err := ps.conn.Model(fr).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Select(); err != nil { + if err == pg.ErrMultiRows { + return db.ErrNoEntries{} + } + return err + } + + follow := >smodel.Follow{ + AccountID: originAccountID, + TargetAccountID: targetAccountID, + URI: fr.URI, + } + + if _, err := ps.conn.Model(follow).Insert(); err != nil { + return err + } + + if _, err := ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Delete(); err != nil { + return err + } + + return nil +} + func (ps *postgresService) CreateInstanceAccount() error { username := ps.config.Host key, err := rsa.GenerateKey(rand.Reader, 2048) @@ -365,13 +393,13 @@ func (ps *postgresService) GetAccountByUserID(userID string, account *gtsmodel.A } if err := ps.conn.Model(user).Where("id = ?", userID).Select(); err != nil { if err == pg.ErrNoRows { - return ErrNoEntries{} + return db.ErrNoEntries{} } return err } if err := ps.conn.Model(account).Where("id = ?", user.AccountID).Select(); err != nil { if err == pg.ErrNoRows { - return ErrNoEntries{} + return db.ErrNoEntries{} } return err } @@ -381,7 +409,7 @@ func (ps *postgresService) GetAccountByUserID(userID string, account *gtsmodel.A func (ps *postgresService) GetLocalAccountByUsername(username string, account *gtsmodel.Account) error { if err := ps.conn.Model(account).Where("username = ?", username).Where("? IS NULL", pg.Ident("domain")).Select(); err != nil { if err == pg.ErrNoRows { - return ErrNoEntries{} + return db.ErrNoEntries{} } return err } @@ -391,7 +419,7 @@ func (ps *postgresService) GetLocalAccountByUsername(username string, account *g func (ps *postgresService) GetFollowRequestsForAccountID(accountID string, followRequests *[]gtsmodel.FollowRequest) error { if err := ps.conn.Model(followRequests).Where("target_account_id = ?", accountID).Select(); err != nil { if err == pg.ErrNoRows { - return ErrNoEntries{} + return nil } return err } @@ -401,7 +429,7 @@ func (ps *postgresService) GetFollowRequestsForAccountID(accountID string, follo func (ps *postgresService) GetFollowingByAccountID(accountID string, following *[]gtsmodel.Follow) error { if err := ps.conn.Model(following).Where("account_id = ?", accountID).Select(); err != nil { if err == pg.ErrNoRows { - return ErrNoEntries{} + return nil } return err } @@ -411,7 +439,17 @@ func (ps *postgresService) GetFollowingByAccountID(accountID string, following * func (ps *postgresService) GetFollowersByAccountID(accountID string, followers *[]gtsmodel.Follow) error { if err := ps.conn.Model(followers).Where("target_account_id = ?", accountID).Select(); err != nil { if err == pg.ErrNoRows { - return ErrNoEntries{} + return nil + } + return err + } + return nil +} + +func (ps *postgresService) GetFavesByAccountID(accountID string, faves *[]gtsmodel.StatusFave) error { + if err := ps.conn.Model(faves).Where("account_id = ?", accountID).Select(); err != nil { + if err == pg.ErrNoRows { + return nil } return err } @@ -421,7 +459,7 @@ func (ps *postgresService) GetFollowersByAccountID(accountID string, followers * func (ps *postgresService) GetStatusesByAccountID(accountID string, statuses *[]gtsmodel.Status) error { if err := ps.conn.Model(statuses).Where("account_id = ?", accountID).Select(); err != nil { if err == pg.ErrNoRows { - return ErrNoEntries{} + return db.ErrNoEntries{} } return err } @@ -438,7 +476,7 @@ func (ps *postgresService) GetStatusesByTimeDescending(accountID string, statuse } if err := q.Select(); err != nil { if err == pg.ErrNoRows { - return ErrNoEntries{} + return db.ErrNoEntries{} } return err } @@ -448,7 +486,7 @@ func (ps *postgresService) GetStatusesByTimeDescending(accountID string, statuse func (ps *postgresService) GetLastStatusForAccountID(accountID string, status *gtsmodel.Status) error { if err := ps.conn.Model(status).Order("created_at DESC").Limit(1).Where("account_id = ?", accountID).Select(); err != nil { if err == pg.ErrNoRows { - return ErrNoEntries{} + return db.ErrNoEntries{} } return err } @@ -574,18 +612,18 @@ func (ps *postgresService) GetHeaderForAccountID(header *gtsmodel.MediaAttachmen acct := >smodel.Account{} if err := ps.conn.Model(acct).Where("id = ?", accountID).Select(); err != nil { if err == pg.ErrNoRows { - return ErrNoEntries{} + return db.ErrNoEntries{} } return err } if acct.HeaderMediaAttachmentID == "" { - return ErrNoEntries{} + return db.ErrNoEntries{} } if err := ps.conn.Model(header).Where("id = ?", acct.HeaderMediaAttachmentID).Select(); err != nil { if err == pg.ErrNoRows { - return ErrNoEntries{} + return db.ErrNoEntries{} } return err } @@ -596,18 +634,18 @@ func (ps *postgresService) GetAvatarForAccountID(avatar *gtsmodel.MediaAttachmen acct := >smodel.Account{} if err := ps.conn.Model(acct).Where("id = ?", accountID).Select(); err != nil { if err == pg.ErrNoRows { - return ErrNoEntries{} + return db.ErrNoEntries{} } return err } if acct.AvatarMediaAttachmentID == "" { - return ErrNoEntries{} + return db.ErrNoEntries{} } if err := ps.conn.Model(avatar).Where("id = ?", acct.AvatarMediaAttachmentID).Select(); err != nil { if err == pg.ErrNoRows { - return ErrNoEntries{} + return db.ErrNoEntries{} } return err } @@ -645,7 +683,7 @@ func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAc if err := ps.conn.Model(targetUser).Where("account_id = ?", targetAccount.ID).Select(); err != nil { l.Debug("target user could not be selected") if err == pg.ErrNoRows { - return false, ErrNoEntries{} + return false, db.ErrNoEntries{} } return false, err } diff --git a/internal/federation/clock.go b/internal/federation/clock.go index f0d6f5e84..cc67f8b73 100644 --- a/internal/federation/clock.go +++ b/internal/federation/clock.go @@ -37,6 +37,7 @@ func (c *Clock) Now() time.Time { return time.Now() } +// NewClock returns a simple pub.Clock for use in federation interfaces. func NewClock() pub.Clock { return &Clock{} } diff --git a/internal/federation/commonbehavior.go b/internal/federation/commonbehavior.go index 9274e78b4..8ed6fd2cb 100644 --- a/internal/federation/commonbehavior.go +++ b/internal/federation/commonbehavior.go @@ -57,7 +57,7 @@ import ( // authenticated must be true and error nil. The request will continue // to be processed. func (f *federator) AuthenticateGetInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) { - // IMPLEMENTATION NOTE: For GoToSocial, we serve outboxes and inboxes through + // IMPLEMENTATION NOTE: For GoToSocial, we serve GETS to outboxes and inboxes through // the CLIENT API, not through the federation API, so we just do nothing here. return nil, false, nil } @@ -82,7 +82,7 @@ func (f *federator) AuthenticateGetInbox(ctx context.Context, w http.ResponseWri // authenticated must be true and error nil. The request will continue // to be processed. func (f *federator) AuthenticateGetOutbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) { - // IMPLEMENTATION NOTE: For GoToSocial, we serve outboxes and inboxes through + // IMPLEMENTATION NOTE: For GoToSocial, we serve GETS to outboxes and inboxes through // the CLIENT API, not through the federation API, so we just do nothing here. return nil, false, nil } @@ -96,7 +96,7 @@ func (f *federator) AuthenticateGetOutbox(ctx context.Context, w http.ResponseWr // Always called, regardless whether the Federated Protocol or Social // API is enabled. func (f *federator) GetOutbox(ctx context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) { - // IMPLEMENTATION NOTE: For GoToSocial, we serve outboxes and inboxes through + // IMPLEMENTATION NOTE: For GoToSocial, we serve GETS to outboxes and inboxes through // the CLIENT API, not through the federation API, so we just do nothing here. return nil, nil } diff --git a/internal/db/federating_db.go b/internal/federation/federating_db.go similarity index 58% rename from internal/db/federating_db.go rename to internal/federation/federating_db.go index ab66b19de..4ea0412e7 100644 --- a/internal/db/federating_db.go +++ b/internal/federation/federating_db.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package db +package federation import ( "context" @@ -26,28 +26,35 @@ import ( "sync" "github.com/go-fed/activity/pub" + "github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams/vocab" + "github.com/google/uuid" "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/typeutils" "github.com/superseriousbusiness/gotosocial/internal/util" ) // FederatingDB uses the underlying DB interface to implement the go-fed pub.Database interface. // It doesn't care what the underlying implementation of the DB interface is, as long as it works. type federatingDB struct { - locks *sync.Map - db DB - config *config.Config - log *logrus.Logger + locks *sync.Map + db db.DB + config *config.Config + log *logrus.Logger + typeConverter typeutils.TypeConverter } -func NewFederatingDB(db DB, config *config.Config, log *logrus.Logger) pub.Database { +// NewFederatingDB returns a pub.Database interface using the given database, config, and logger. +func NewFederatingDB(db db.DB, config *config.Config, log *logrus.Logger) pub.Database { return &federatingDB{ - locks: new(sync.Map), - db: db, - config: config, - log: log, + locks: new(sync.Map), + db: db, + config: config, + log: log, + typeConverter: typeutils.NewConverter(config, db), } } @@ -104,30 +111,42 @@ func (f *federatingDB) Unlock(c context.Context, id *url.URL) error { // // The library makes this call only after acquiring a lock first. func (f *federatingDB) InboxContains(c context.Context, inbox, id *url.URL) (contains bool, err error) { + l := f.log.WithFields( + logrus.Fields{ + "func": "InboxContains", + "id": id.String(), + }, + ) + l.Debugf("entering INBOXCONTAINS function with for inbox %s and id %s", inbox.String(), id.String()) if !util.IsInboxPath(inbox) { return false, fmt.Errorf("%s is not an inbox URI", inbox.String()) } - if !util.IsStatusesPath(id) { - return false, fmt.Errorf("%s is not a status URI", id.String()) + activityI := c.Value(util.APActivity) + if activityI == nil { + return false, fmt.Errorf("no activity was set for id %s", id.String()) } - _, statusID, err := util.ParseStatusesPath(inbox) - if err != nil { - return false, fmt.Errorf("status URI %s was not parseable: %s", id.String(), err) + activity, ok := activityI.(pub.Activity) + if !ok || activity == nil { + return false, fmt.Errorf("could not parse contextual activity for id %s", id.String()) } - if err := f.db.GetByID(statusID, >smodel.Status{}); err != nil { - if _, ok := err.(ErrNoEntries); ok { - // we don't have it - return false, nil - } - // actual error - return false, fmt.Errorf("error getting status from db: %s", err) - } + l.Debugf("activity type %s for id %s", activity.GetTypeName(), id.String()) - // we must have it - return true, nil + return false, nil + + // if err := f.db.GetByID(statusID, >smodel.Status{}); err != nil { + // if _, ok := err.(db.ErrNoEntries); ok { + // // we don't have it + // return false, nil + // } + // // actual error + // return false, fmt.Errorf("error getting status from db: %s", err) + // } + + // // we must have it + // return true, nil } // GetInbox returns the first ordered collection page of the outbox at @@ -135,7 +154,13 @@ func (f *federatingDB) InboxContains(c context.Context, inbox, id *url.URL) (con // // The library makes this call only after acquiring a lock first. func (f *federatingDB) GetInbox(c context.Context, inboxIRI *url.URL) (inbox vocab.ActivityStreamsOrderedCollectionPage, err error) { - return nil, nil + l := f.log.WithFields( + logrus.Fields{ + "func": "GetInbox", + }, + ) + l.Debugf("entering GETINBOX function with inboxIRI %s", inboxIRI.String()) + return streams.NewActivityStreamsOrderedCollectionPage(), nil } // SetInbox saves the inbox value given from GetInbox, with new items @@ -144,6 +169,12 @@ func (f *federatingDB) GetInbox(c context.Context, inboxIRI *url.URL) (inbox voc // // The library makes this call only after acquiring a lock first. func (f *federatingDB) SetInbox(c context.Context, inbox vocab.ActivityStreamsOrderedCollectionPage) error { + l := f.log.WithFields( + logrus.Fields{ + "func": "SetInbox", + }, + ) + l.Debug("entering SETINBOX function") return nil } @@ -151,12 +182,21 @@ func (f *federatingDB) SetInbox(c context.Context, inbox vocab.ActivityStreamsOr // the database has an entry for the IRI. // The library makes this call only after acquiring a lock first. func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) { + l := f.log.WithFields( + logrus.Fields{ + "func": "Owns", + "id": id.String(), + }, + ) + l.Debugf("entering OWNS function with id %s", id.String()) + // if the id host isn't this instance host, we don't own this IRI if id.Host != f.config.Host { + l.Debugf("we DO NOT own activity because the host is %s not %s", id.Host, f.config.Host) return false, nil } - // apparently we own it, so what *is* it? + // apparently it belongs to this host, so what *is* it? // check if it's a status, eg /users/example_username/statuses/SOME_UUID_OF_A_STATUS if util.IsStatusesPath(id) { @@ -165,13 +205,14 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) { return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err) } if err := f.db.GetWhere("uri", uid, >smodel.Status{}); err != nil { - if _, ok := err.(ErrNoEntries); ok { + if _, ok := err.(db.ErrNoEntries); ok { // there are no entries for this status return false, nil } // an actual error happened return false, fmt.Errorf("database error fetching status with id %s: %s", uid, err) } + l.Debug("we DO own this") return true, nil } @@ -182,13 +223,14 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) { return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err) } if err := f.db.GetLocalAccountByUsername(username, >smodel.Account{}); err != nil { - if _, ok := err.(ErrNoEntries); ok { + if _, ok := err.(db.ErrNoEntries); ok { // there are no entries for this username return false, nil } // an actual error happened return false, fmt.Errorf("database error fetching account with username %s: %s", username, err) } + l.Debug("we DO own this") return true, nil } @@ -199,12 +241,20 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) { // // The library makes this call only after acquiring a lock first. func (f *federatingDB) ActorForOutbox(c context.Context, outboxIRI *url.URL) (actorIRI *url.URL, err error) { + l := f.log.WithFields( + logrus.Fields{ + "func": "ActorForOutbox", + "inboxIRI": outboxIRI.String(), + }, + ) + l.Debugf("entering ACTORFOROUTBOX function with outboxIRI %s", outboxIRI.String()) + if !util.IsOutboxPath(outboxIRI) { return nil, fmt.Errorf("%s is not an outbox URI", outboxIRI.String()) } acct := >smodel.Account{} if err := f.db.GetWhere("outbox_uri", outboxIRI.String(), acct); err != nil { - if _, ok := err.(ErrNoEntries); ok { + if _, ok := err.(db.ErrNoEntries); ok { return nil, fmt.Errorf("no actor found that corresponds to outbox %s", outboxIRI.String()) } return nil, fmt.Errorf("db error searching for actor with outbox %s", outboxIRI.String()) @@ -216,12 +266,20 @@ func (f *federatingDB) ActorForOutbox(c context.Context, outboxIRI *url.URL) (ac // // The library makes this call only after acquiring a lock first. func (f *federatingDB) ActorForInbox(c context.Context, inboxIRI *url.URL) (actorIRI *url.URL, err error) { + l := f.log.WithFields( + logrus.Fields{ + "func": "ActorForInbox", + "inboxIRI": inboxIRI.String(), + }, + ) + l.Debugf("entering ACTORFORINBOX function with inboxIRI %s", inboxIRI.String()) + if !util.IsInboxPath(inboxIRI) { return nil, fmt.Errorf("%s is not an inbox URI", inboxIRI.String()) } acct := >smodel.Account{} if err := f.db.GetWhere("inbox_uri", inboxIRI.String(), acct); err != nil { - if _, ok := err.(ErrNoEntries); ok { + if _, ok := err.(db.ErrNoEntries); ok { return nil, fmt.Errorf("no actor found that corresponds to inbox %s", inboxIRI.String()) } return nil, fmt.Errorf("db error searching for actor with inbox %s", inboxIRI.String()) @@ -234,12 +292,20 @@ func (f *federatingDB) ActorForInbox(c context.Context, inboxIRI *url.URL) (acto // // The library makes this call only after acquiring a lock first. func (f *federatingDB) OutboxForInbox(c context.Context, inboxIRI *url.URL) (outboxIRI *url.URL, err error) { + l := f.log.WithFields( + logrus.Fields{ + "func": "OutboxForInbox", + "inboxIRI": inboxIRI.String(), + }, + ) + l.Debugf("entering OUTBOXFORINBOX function with inboxIRI %s", inboxIRI.String()) + if !util.IsInboxPath(inboxIRI) { return nil, fmt.Errorf("%s is not an inbox URI", inboxIRI.String()) } acct := >smodel.Account{} if err := f.db.GetWhere("inbox_uri", inboxIRI.String(), acct); err != nil { - if _, ok := err.(ErrNoEntries); ok { + if _, ok := err.(db.ErrNoEntries); ok { return nil, fmt.Errorf("no actor found that corresponds to inbox %s", inboxIRI.String()) } return nil, fmt.Errorf("db error searching for actor with inbox %s", inboxIRI.String()) @@ -252,6 +318,14 @@ func (f *federatingDB) OutboxForInbox(c context.Context, inboxIRI *url.URL) (out // // The library makes this call only after acquiring a lock first. func (f *federatingDB) Exists(c context.Context, id *url.URL) (exists bool, err error) { + l := f.log.WithFields( + logrus.Fields{ + "func": "Exists", + "id": id.String(), + }, + ) + l.Debugf("entering EXISTS function with id %s", id.String()) + return false, nil } @@ -259,6 +333,22 @@ func (f *federatingDB) Exists(c context.Context, id *url.URL) (exists bool, err // // The library makes this call only after acquiring a lock first. func (f *federatingDB) Get(c context.Context, id *url.URL) (value vocab.Type, err error) { + l := f.log.WithFields( + logrus.Fields{ + "func": "Get", + "id": id.String(), + }, + ) + l.Debug("entering GET function") + + if util.IsUserPath(id) { + acct := >smodel.Account{} + if err := f.db.GetWhere("uri", id.String(), acct); err != nil { + return nil, err + } + return f.typeConverter.AccountToAS(acct) + } + return nil, nil } @@ -275,6 +365,49 @@ func (f *federatingDB) Get(c context.Context, id *url.URL) (value vocab.Type, er // Under certain conditions and network activities, Create may be called // multiple times for the same ActivityStreams object. func (f *federatingDB) Create(c context.Context, asType vocab.Type) error { + l := f.log.WithFields( + logrus.Fields{ + "func": "Create", + "asType": asType.GetTypeName(), + }, + ) + l.Debugf("received CREATE asType %+v", asType) + + switch gtsmodel.ActivityStreamsActivity(asType.GetTypeName()) { + case gtsmodel.ActivityStreamsCreate: + create, ok := asType.(vocab.ActivityStreamsCreate) + if !ok { + return errors.New("could not convert type to create") + } + object := create.GetActivityStreamsObject() + for objectIter := object.Begin(); objectIter != object.End(); objectIter = objectIter.Next() { + switch gtsmodel.ActivityStreamsObject(objectIter.GetType().GetTypeName()) { + case gtsmodel.ActivityStreamsNote: + note := objectIter.GetActivityStreamsNote() + status, err := f.typeConverter.ASStatusToStatus(note) + if err != nil { + return fmt.Errorf("error converting note to status: %s", err) + } + if err := f.db.Put(status); err != nil { + return fmt.Errorf("database error inserting status: %s", err) + } + } + } + case gtsmodel.ActivityStreamsFollow: + follow, ok := asType.(vocab.ActivityStreamsFollow) + if !ok { + return errors.New("could not convert type to follow") + } + + followRequest, err := f.typeConverter.ASFollowToFollowRequest(follow) + if err != nil { + return fmt.Errorf("could not convert Follow to follow request: %s", err) + } + + if err := f.db.Put(followRequest); err != nil { + return fmt.Errorf("database error inserting follow request: %s", err) + } + } return nil } @@ -288,6 +421,13 @@ func (f *federatingDB) Create(c context.Context, asType vocab.Type) error { // // The library makes this call only after acquiring a lock first. func (f *federatingDB) Update(c context.Context, asType vocab.Type) error { + l := f.log.WithFields( + logrus.Fields{ + "func": "Update", + "asType": asType.GetTypeName(), + }, + ) + l.Debugf("received UPDATE asType %+v", asType) return nil } @@ -298,6 +438,13 @@ func (f *federatingDB) Update(c context.Context, asType vocab.Type) error { // // The library makes this call only after acquiring a lock first. func (f *federatingDB) Delete(c context.Context, id *url.URL) error { + l := f.log.WithFields( + logrus.Fields{ + "func": "Delete", + "id": id.String(), + }, + ) + l.Debugf("received DELETE id %s", id.String()) return nil } @@ -306,6 +453,13 @@ func (f *federatingDB) Delete(c context.Context, id *url.URL) error { // // The library makes this call only after acquiring a lock first. func (f *federatingDB) GetOutbox(c context.Context, outboxIRI *url.URL) (inbox vocab.ActivityStreamsOrderedCollectionPage, err error) { + l := f.log.WithFields( + logrus.Fields{ + "func": "GetOutbox", + }, + ) + l.Debug("entering GETOUTBOX function") + return nil, nil } @@ -315,6 +469,13 @@ func (f *federatingDB) GetOutbox(c context.Context, outboxIRI *url.URL) (inbox v // // The library makes this call only after acquiring a lock first. func (f *federatingDB) SetOutbox(c context.Context, outbox vocab.ActivityStreamsOrderedCollectionPage) error { + l := f.log.WithFields( + logrus.Fields{ + "func": "SetOutbox", + }, + ) + l.Debug("entering SETOUTBOX function") + return nil } @@ -325,7 +486,15 @@ func (f *federatingDB) SetOutbox(c context.Context, outbox vocab.ActivityStreams // The go-fed library will handle setting the 'id' property on the // activity or object provided with the value returned. func (f *federatingDB) NewID(c context.Context, t vocab.Type) (id *url.URL, err error) { - return nil, nil + l := f.log.WithFields( + logrus.Fields{ + "func": "NewID", + "asType": t.GetTypeName(), + }, + ) + l.Debugf("received NEWID request for asType %+v", t) + + return url.Parse(fmt.Sprintf("%s://%s/", f.config.Protocol, uuid.NewString())) } // Followers obtains the Followers Collection for an actor with the @@ -335,7 +504,39 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (id *url.URL, err // // The library makes this call only after acquiring a lock first. func (f *federatingDB) Followers(c context.Context, actorIRI *url.URL) (followers vocab.ActivityStreamsCollection, err error) { - return nil, nil + l := f.log.WithFields( + logrus.Fields{ + "func": "Followers", + "actorIRI": actorIRI.String(), + }, + ) + l.Debugf("entering FOLLOWERS function with actorIRI %s", actorIRI.String()) + + acct := >smodel.Account{} + if err := f.db.GetWhere("uri", actorIRI.String(), acct); err != nil { + return nil, fmt.Errorf("db error getting account with uri %s: %s", actorIRI.String(), err) + } + + acctFollowers := []gtsmodel.Follow{} + if err := f.db.GetFollowersByAccountID(acct.ID, &acctFollowers); err != nil { + return nil, fmt.Errorf("db error getting followers for account id %s: %s", acct.ID, err) + } + + followers = streams.NewActivityStreamsCollection() + items := streams.NewActivityStreamsItemsProperty() + for _, follow := range acctFollowers { + gtsFollower := >smodel.Account{} + if err := f.db.GetByID(follow.AccountID, gtsFollower); err != nil { + return nil, fmt.Errorf("db error getting account id %s: %s", follow.AccountID, err) + } + uri, err := url.Parse(gtsFollower.URI) + if err != nil { + return nil, fmt.Errorf("error parsing %s as url: %s", gtsFollower.URI, err) + } + items.AppendIRI(uri) + } + followers.SetActivityStreamsItems(items) + return } // Following obtains the Following Collection for an actor with the @@ -344,8 +545,40 @@ func (f *federatingDB) Followers(c context.Context, actorIRI *url.URL) (follower // If modified, the library will then call Update. // // The library makes this call only after acquiring a lock first. -func (f *federatingDB) Following(c context.Context, actorIRI *url.URL) (followers vocab.ActivityStreamsCollection, err error) { - return nil, nil +func (f *federatingDB) Following(c context.Context, actorIRI *url.URL) (following vocab.ActivityStreamsCollection, err error) { + l := f.log.WithFields( + logrus.Fields{ + "func": "Following", + "actorIRI": actorIRI.String(), + }, + ) + l.Debugf("entering FOLLOWING function with actorIRI %s", actorIRI.String()) + + acct := >smodel.Account{} + if err := f.db.GetWhere("uri", actorIRI.String(), acct); err != nil { + return nil, fmt.Errorf("db error getting account with uri %s: %s", actorIRI.String(), err) + } + + acctFollowing := []gtsmodel.Follow{} + if err := f.db.GetFollowingByAccountID(acct.ID, &acctFollowing); err != nil { + return nil, fmt.Errorf("db error getting following for account id %s: %s", acct.ID, err) + } + + following = streams.NewActivityStreamsCollection() + items := streams.NewActivityStreamsItemsProperty() + for _, follow := range acctFollowing { + gtsFollowing := >smodel.Account{} + if err := f.db.GetByID(follow.AccountID, gtsFollowing); err != nil { + return nil, fmt.Errorf("db error getting account id %s: %s", follow.AccountID, err) + } + uri, err := url.Parse(gtsFollowing.URI) + if err != nil { + return nil, fmt.Errorf("error parsing %s as url: %s", gtsFollowing.URI, err) + } + items.AppendIRI(uri) + } + following.SetActivityStreamsItems(items) + return } // Liked obtains the Liked Collection for an actor with the @@ -354,6 +587,13 @@ func (f *federatingDB) Following(c context.Context, actorIRI *url.URL) (follower // If modified, the library will then call Update. // // The library makes this call only after acquiring a lock first. -func (f *federatingDB) Liked(c context.Context, actorIRI *url.URL) (followers vocab.ActivityStreamsCollection, err error) { +func (f *federatingDB) Liked(c context.Context, actorIRI *url.URL) (liked vocab.ActivityStreamsCollection, err error) { + l := f.log.WithFields( + logrus.Fields{ + "func": "Liked", + "actorIRI": actorIRI.String(), + }, + ) + l.Debugf("entering LIKED function with actorIRI %s", actorIRI.String()) return nil, nil } diff --git a/internal/db/federating_db_test.go b/internal/federation/federating_db_test.go similarity index 97% rename from internal/db/federating_db_test.go rename to internal/federation/federating_db_test.go index 529d2efd0..b4695b55b 100644 --- a/internal/db/federating_db_test.go +++ b/internal/federation/federating_db_test.go @@ -16,6 +16,6 @@ along with this program. If not, see . */ -package db +package federation // TODO: write tests for pgfed diff --git a/internal/federation/federatingactor.go b/internal/federation/federatingactor.go index f105d9125..65a16efaf 100644 --- a/internal/federation/federatingactor.go +++ b/internal/federation/federatingactor.go @@ -77,6 +77,13 @@ func (f *federatingActor) PostInbox(c context.Context, w http.ResponseWriter, r return f.actor.PostInbox(c, w, r) } +// PostInboxScheme is similar to PostInbox, except clients are able to +// specify which protocol scheme to handle the incoming request and the +// data stored within the application (HTTP, HTTPS, etc). +func (f *federatingActor) PostInboxScheme(c context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) { + return f.actor.PostInboxScheme(c, w, r, scheme) +} + // GetInbox returns true if the request was handled as an ActivityPub // GET to an actor's inbox. If false, the request was not an ActivityPub // request and may still be handled by the caller in another way, such @@ -118,6 +125,13 @@ func (f *federatingActor) PostOutbox(c context.Context, w http.ResponseWriter, r return f.actor.PostOutbox(c, w, r) } +// PostOutboxScheme is similar to PostOutbox, except clients are able to +// specify which protocol scheme to handle the incoming request and the +// data stored within the application (HTTP, HTTPS, etc). +func (f *federatingActor) PostOutboxScheme(c context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) { + return f.actor.PostOutboxScheme(c, w, r, scheme) +} + // GetOutbox returns true if the request was handled as an ActivityPub // GET to an actor's outbox. If false, the request was not an // ActivityPub request. diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go index 1764eb791..0d2a8d9dd 100644 --- a/internal/federation/federatingprotocol.go +++ b/internal/federation/federatingprotocol.go @@ -72,8 +72,49 @@ func (f *federator) PostInboxRequestBodyHook(ctx context.Context, r *http.Reques return nil, err } - ctxWithActivity := context.WithValue(ctx, util.APActivity, activity) - return ctxWithActivity, nil + // derefence the actor of the activity already + // var requestingActorIRI *url.URL + // actorProp := activity.GetActivityStreamsActor() + // if actorProp != nil { + // for i := actorProp.Begin(); i != actorProp.End(); i = i.Next() { + // if i.IsIRI() { + // requestingActorIRI = i.GetIRI() + // break + // } + // } + // } + // if requestingActorIRI != nil { + + // requestedAccountI := ctx.Value(util.APAccount) + // requestedAccount, ok := requestedAccountI.(*gtsmodel.Account) + // if !ok { + // return nil, errors.New("requested account was not set on request context") + // } + + // requestingActor := >smodel.Account{} + // if err := f.db.GetWhere("uri", requestingActorIRI.String(), requestingActor); err != nil { + // // there's been a proper error so return it + // if _, ok := err.(db.ErrNoEntries); !ok { + // return nil, fmt.Errorf("error getting requesting actor with id %s: %s", requestingActorIRI.String(), err) + // } + + // // we don't know this account (yet) so let's dereference it right now + // person, err := f.DereferenceRemoteAccount(requestedAccount.Username, publicKeyOwnerURI) + // if err != nil { + // return ctx, false, fmt.Errorf("error dereferencing account with public key id %s: %s", publicKeyOwnerURI.String(), err) + // } + + // a, err := f.typeConverter.ASRepresentationToAccount(person) + // if err != nil { + // return ctx, false, fmt.Errorf("error converting person with public key id %s to account: %s", publicKeyOwnerURI.String(), err) + // } + // requestingAccount = a + // } + // } + + // set the activity on the context for use later on + + return context.WithValue(ctx, util.APActivity, activity), nil } // AuthenticatePostInbox delegates the authentication of a POST to an @@ -100,14 +141,22 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr }) l.Trace("received request to authenticate") - requestedAccountI := ctx.Value(util.APAccount) - if requestedAccountI == nil { - return ctx, false, errors.New("requested account not set in context") + if !util.IsInboxPath(r.URL) { + return nil, false, fmt.Errorf("path %s was not an inbox path", r.URL.String()) } - requestedAccount, ok := requestedAccountI.(*gtsmodel.Account) - if !ok || requestedAccount == nil { - return ctx, false, errors.New("requested account not parsebale from context") + username, err := util.ParseInboxPath(r.URL) + if err != nil { + return nil, false, fmt.Errorf("could not parse path %s: %s", r.URL.String(), err) + } + + if username == "" { + return nil, false, errors.New("username was empty") + } + + requestedAccount := >smodel.Account{} + if err := f.db.GetLocalAccountByUsername(username, requestedAccount); err != nil { + return nil, false, fmt.Errorf("could not fetch requested account with username %s: %s", username, err) } publicKeyOwnerURI, err := f.AuthenticateFederatedRequest(requestedAccount.Username, r) @@ -124,7 +173,6 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr } // we don't know this account (yet) so let's dereference it right now - // TODO: slow-fed person, err := f.DereferenceRemoteAccount(requestedAccount.Username, publicKeyOwnerURI) if err != nil { return ctx, false, fmt.Errorf("error dereferencing account with public key id %s: %s", publicKeyOwnerURI.String(), err) @@ -134,12 +182,17 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr if err != nil { return ctx, false, fmt.Errorf("error converting person with public key id %s to account: %s", publicKeyOwnerURI.String(), err) } + + if err := f.db.Put(a); err != nil { + l.Errorf("error inserting dereferenced remote account: %s", err) + } + requestingAccount = a } - contextWithRequestingAccount := context.WithValue(ctx, util.APRequestingAccount, requestingAccount) - - return contextWithRequestingAccount, true, nil + withRequester := context.WithValue(ctx, util.APRequestingAccount, requestingAccount) + withRequested := context.WithValue(withRequester, util.APAccount, requestedAccount) + return withRequested, true, nil } // Blocked should determine whether to permit a set of actors given by @@ -156,8 +209,40 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr // Finally, if the authentication and authorization succeeds, then // blocked must be false and error nil. The request will continue // to be processed. +// +// TODO: implement domain block checking here as well func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, error) { - // TODO + l := f.log.WithFields(logrus.Fields{ + "func": "Blocked", + }) + l.Debugf("entering BLOCKED function with IRI list: %+v", actorIRIs) + + requestedAccountI := ctx.Value(util.APAccount) + requestedAccount, ok := requestedAccountI.(*gtsmodel.Account) + if !ok { + f.log.Errorf("requested account not set on request context") + return false, errors.New("requested account not set on request context, so couldn't determine blocks") + } + + for _, uri := range actorIRIs { + a := >smodel.Account{} + if err := f.db.GetWhere("uri", uri.String(), a); err != nil { + _, ok := err.(db.ErrNoEntries) + if ok { + // we don't have an entry for this account so it's not blocked + // TODO: allow a different default to be set for this behavior + continue + } + return false, fmt.Errorf("error getting account with uri %s: %s", uri.String(), err) + } + blocked, err := f.db.Blocked(requestedAccount.ID, a.ID) + if err != nil { + return false, fmt.Errorf("error checking account blocks: %s", err) + } + if blocked { + return true, nil + } + } return false, nil } @@ -180,9 +265,40 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er // // Applications are not expected to handle every single ActivityStreams // type and extension. The unhandled ones are passed to DefaultCallback. -func (f *federator) FederatingCallbacks(ctx context.Context) (pub.FederatingWrappedCallbacks, []interface{}, error) { - // TODO - return pub.FederatingWrappedCallbacks{}, nil, nil +func (f *federator) FederatingCallbacks(ctx context.Context) (wrapped pub.FederatingWrappedCallbacks, other []interface{}, err error) { + l := f.log.WithFields(logrus.Fields{ + "func": "FederatingCallbacks", + }) + + targetAcctI := ctx.Value(util.APAccount) + if targetAcctI == nil { + l.Error("target account wasn't set on context") + } + targetAcct, ok := targetAcctI.(*gtsmodel.Account) + if !ok { + l.Error("target account was set on context but couldn't be parsed") + } + + var onFollow pub.OnFollowBehavior = pub.OnFollowAutomaticallyAccept + if targetAcct.Locked { + onFollow = pub.OnFollowDoNothing + } + + wrapped = pub.FederatingWrappedCallbacks{ + // Follow handles additional side effects for the Follow ActivityStreams + // type, specific to the application using go-fed. + // + // The wrapping function can have one of several default behaviors, + // depending on the value of the OnFollow setting. + Follow: func(context.Context, vocab.ActivityStreamsFollow) error { + return nil + }, + // OnFollow determines what action to take for this particular callback + // if a Follow Activity is handled. + OnFollow: onFollow, + } + + return } // DefaultCallback is called for types that go-fed can deserialize but @@ -207,7 +323,7 @@ func (f *federator) DefaultCallback(ctx context.Context, activity pub.Activity) // Zero or negative numbers indicate infinite recursion. func (f *federator) MaxInboxForwardingRecursionDepth(ctx context.Context) int { // TODO - return 0 + return 4 } // MaxDeliveryRecursionDepth determines how deep to search within @@ -217,7 +333,7 @@ func (f *federator) MaxInboxForwardingRecursionDepth(ctx context.Context) int { // Zero or negative numbers indicate infinite recursion. func (f *federator) MaxDeliveryRecursionDepth(ctx context.Context) int { // TODO - return 0 + return 4 } // FilterForwarding allows the implementation to apply business logic @@ -241,7 +357,7 @@ func (f *federator) FilterForwarding(ctx context.Context, potentialRecipients [] // Always called, regardless whether the Federated Protocol or Social // API is enabled. func (f *federator) GetInbox(ctx context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) { - // IMPLEMENTATION NOTE: For GoToSocial, we serve outboxes and inboxes through + // IMPLEMENTATION NOTE: For GoToSocial, we serve GETS to outboxes and inboxes through // the CLIENT API, not through the federation API, so we just do nothing here. return nil, nil } diff --git a/internal/gotosocial/actions.go b/internal/gotosocial/actions.go index 5790456dd..fb83a4231 100644 --- a/internal/gotosocial/actions.go +++ b/internal/gotosocial/actions.go @@ -34,6 +34,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/api/client/app" "github.com/superseriousbusiness/gotosocial/internal/api/client/auth" "github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver" + "github.com/superseriousbusiness/gotosocial/internal/api/client/followrequest" "github.com/superseriousbusiness/gotosocial/internal/api/client/instance" mediaModule "github.com/superseriousbusiness/gotosocial/internal/api/client/media" "github.com/superseriousbusiness/gotosocial/internal/api/client/status" @@ -41,7 +42,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/api/s2s/webfinger" "github.com/superseriousbusiness/gotosocial/internal/api/security" "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/db/pg" "github.com/superseriousbusiness/gotosocial/internal/federation" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/media" @@ -78,7 +79,7 @@ var models []interface{} = []interface{}{ // Run creates and starts a gotosocial server var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error { - dbService, err := db.NewPostgresService(ctx, c, log) + dbService, err := pg.NewPostgresService(ctx, c, log) if err != nil { return fmt.Errorf("error creating dbservice: %s", err) } @@ -111,6 +112,7 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr accountModule := account.New(c, processor, log) instanceModule := instance.New(c, processor, log) appsModule := app.New(c, processor, log) + followRequestsModule := followrequest.New(c, processor, log) webfingerModule := webfinger.New(c, processor, log) usersModule := user.New(c, processor, log) mm := mediaModule.New(c, processor, log) @@ -128,6 +130,7 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr accountModule, instanceModule, appsModule, + followRequestsModule, mm, fileServerModule, adminModule, diff --git a/internal/gtsmodel/account.go b/internal/gtsmodel/account.go index 1903216f8..56c401e62 100644 --- a/internal/gtsmodel/account.go +++ b/internal/gtsmodel/account.go @@ -76,15 +76,15 @@ type Account struct { */ // Does this account need an approval for new followers? - Locked bool `pg:",default:true"` + Locked bool `pg:",default:'true'"` // Should this account be shown in the instance's profile directory? Discoverable bool // Default post privacy for this account Privacy Visibility // Set posts from this account to sensitive by default? - Sensitive bool `pg:",default:false"` + Sensitive bool `pg:",default:'false'"` // What language does this account post in? - Language string `pg:",default:en"` + Language string `pg:",default:'en'"` /* ACTIVITYPUB THINGS diff --git a/internal/gtsmodel/mention.go b/internal/gtsmodel/mention.go index 18eb11082..8e56a1b36 100644 --- a/internal/gtsmodel/mention.go +++ b/internal/gtsmodel/mention.go @@ -30,10 +30,22 @@ type Mention struct { CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // When was this mention last updated? UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` - // Who created this mention? + // What's the internal account ID of the originator of the mention? OriginAccountID string `pg:",notnull"` - // Who does this mention target? + // What's the AP URI of the originator of the mention? + OriginAccountURI string `pg:",notnull"` + // What's the internal account ID of the mention target? TargetAccountID string `pg:",notnull"` // Prevent this mention from generating a notification? Silent bool + // NameString is for putting in the namestring of the mentioned user + // before the mention is dereferenced. Should be in a form along the lines of: + // @whatever_username@example.org + // + // This will not be put in the database, it's just for convenience. + NameString string `pg:"-"` + // MentionedAccountURI is the AP ID (uri) of the user mentioned. + // + // This will not be put in the database, it's just for convenience. + MentionedAccountURI string `pg:"-"` } diff --git a/internal/gtsmodel/status.go b/internal/gtsmodel/status.go index 55ae91599..8693bce30 100644 --- a/internal/gtsmodel/status.go +++ b/internal/gtsmodel/status.go @@ -71,12 +71,14 @@ type Status struct { Text string /* - NON-DATABASE FIELDS + INTERNAL MODEL NON-DATABASE FIELDS These are for convenience while passing the status around internally, but these fields should *never* be put in the db. */ + // Account that created this status + GTSAccount *Account `pg:"-"` // Mentions created in this status GTSMentions []*Mention `pg:"-"` // Hashtags used in this status @@ -93,6 +95,20 @@ type Status struct { GTSBoostedStatus *Status `pg:"-"` // Account of the boosted status GTSBoostedAccount *Account `pg:"-"` + + /* + AP NON-DATABASE FIELDS + + These are for convenience while passing the status around internally, + but these fields should *never* be put in the db. + */ + + // AP URI of the status being replied to. + // Useful when that status doesn't exist in the database yet and we still need to dereference it. + APReplyToStatusURI string `pg:"-"` + // The AP URI of the owner/creator of the status. + // Useful when that account doesn't exist in the database yet and we still need to dereference it. + APStatusOwnerURI string `pg:"-"` } // Visibility represents the visibility granularity of a status. diff --git a/internal/gtsmodel/tag.go b/internal/gtsmodel/tag.go index 83c471958..c1b0429d6 100644 --- a/internal/gtsmodel/tag.go +++ b/internal/gtsmodel/tag.go @@ -24,6 +24,8 @@ import "time" type Tag struct { // id of this tag in the database ID string `pg:",unique,type:uuid,default:gen_random_uuid(),pk,notnull"` + // Href of this tag, eg https://example.org/tags/somehashtag + URL string // name of this tag -- the tag without the hash part Name string `pg:",unique,pk,notnull"` // Which account ID is the first one we saw using this tag? diff --git a/internal/media/media_test.go b/internal/media/media_test.go index 8045295d2..03dcdc21d 100644 --- a/internal/media/media_test.go +++ b/internal/media/media_test.go @@ -29,6 +29,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/db/pg" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/storage" ) @@ -78,7 +79,7 @@ func (suite *MediaTestSuite) SetupSuite() { } suite.config = c // use an actual database for this, because it's just easier than mocking one out - database, err := db.NewPostgresService(context.Background(), c, log) + database, err := pg.NewPostgresService(context.Background(), c, log) if err != nil { suite.FailNow(err.Error()) } diff --git a/internal/message/fediprocess.go b/internal/message/fediprocess.go index dad1e848c..133e7dbaa 100644 --- a/internal/message/fediprocess.go +++ b/internal/message/fediprocess.go @@ -1,6 +1,7 @@ package message import ( + "context" "fmt" "net/http" @@ -8,6 +9,7 @@ import ( apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/util" ) // authenticateAndDereferenceFediRequest authenticates the HTTP signature of an incoming federation request, using the given @@ -130,3 +132,9 @@ func (p *processor) GetWebfingerAccount(requestedUsername string, request *http. }, }, nil } + +func (p *processor) InboxPost(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) { + contextWithChannel := context.WithValue(ctx, util.APFromFederatorChanKey, p.fromFederator) + posted, err := p.federator.FederatingActor().PostInbox(contextWithChannel, w, r) + return posted, err +} diff --git a/internal/message/frprocess.go b/internal/message/frprocess.go new file mode 100644 index 000000000..c96b83dec --- /dev/null +++ b/internal/message/frprocess.go @@ -0,0 +1,42 @@ +package message + +import ( + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +func (p *processor) FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, ErrorWithCode) { + frs := []gtsmodel.FollowRequest{} + if err := p.db.GetFollowRequestsForAccountID(auth.Account.ID, &frs); err != nil { + if _, ok := err.(db.ErrNoEntries); !ok { + return nil, NewErrorInternalError(err) + } + } + + accts := []apimodel.Account{} + for _, fr := range frs { + acct := >smodel.Account{} + if err := p.db.GetByID(fr.AccountID, acct); err != nil { + return nil, NewErrorInternalError(err) + } + mastoAcct, err := p.tc.AccountToMastoPublic(acct) + if err != nil { + return nil, NewErrorInternalError(err) + } + accts = append(accts, *mastoAcct) + } + return accts, nil +} + +func (p *processor) FollowRequestAccept(auth *oauth.Auth, accountID string) ErrorWithCode { + if err := p.db.AcceptFollowRequest(accountID, auth.Account.ID); err != nil { + return NewErrorNotFound(err) + } + return nil +} + +func (p *processor) FollowRequestDeny(auth *oauth.Auth) ErrorWithCode { + return nil +} diff --git a/internal/message/processor.go b/internal/message/processor.go index c8d58a346..7fc850e37 100644 --- a/internal/message/processor.go +++ b/internal/message/processor.go @@ -19,7 +19,11 @@ package message import ( + "context" + "errors" + "fmt" "net/http" + "net/url" "github.com/sirupsen/logrus" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" @@ -77,6 +81,11 @@ type Processor interface { // FileGet handles the fetching of a media attachment file via the fileserver. FileGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) + // FollowRequestsGet handles the getting of the authed account's incoming follow requests + FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, ErrorWithCode) + // FollowRequestAccept handles the acceptance of a follow request from the given account ID + FollowRequestAccept(auth *oauth.Auth, accountID string) ErrorWithCode + // InstanceGet retrieves instance information for serving at api/v1/instance InstanceGet(domain string) (*apimodel.Instance, ErrorWithCode) @@ -116,6 +125,18 @@ type Processor interface { // GetWebfingerAccount handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups. GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, ErrorWithCode) + + // InboxPost handles POST requests to a user's inbox for new activitypub messages. + // + // InboxPost returns true if the request was handled as an ActivityPub POST to an actor's inbox. + // If false, the request was not an ActivityPub request and may still be handled by the caller in another way, such as serving a web page. + // + // If the error is nil, then the ResponseWriter's headers and response has already been written. If a non-nil error is returned, then no response has been written. + // + // If the Actor was constructed with the Federated Protocol enabled, side effects will occur. + // + // If the Federated Protocol is not enabled, writes the http.StatusMethodNotAllowed status code in the response. No side effects occur. + InboxPost(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) } // processor just implements the Processor interface @@ -181,6 +202,9 @@ func (p *processor) Start() error { p.log.Infof("received message TO client API: %+v", clientMsg) case clientMsg := <-p.fromClientAPI: p.log.Infof("received message FROM client API: %+v", clientMsg) + if err := p.processFromClientAPI(clientMsg); err != nil { + p.log.Error(err) + } case federatorMsg := <-p.toFederator: p.log.Infof("received message TO federator: %+v", federatorMsg) case federatorMsg := <-p.fromFederator: @@ -227,3 +251,54 @@ type FromFederator struct { APActivityType gtsmodel.ActivityStreamsActivity Activity interface{} } + +func (p *processor) processFromClientAPI(clientMsg FromClientAPI) error { + switch clientMsg.APObjectType { + case gtsmodel.ActivityStreamsNote: + status, ok := clientMsg.Activity.(*gtsmodel.Status) + if !ok { + return errors.New("note was not parseable as *gtsmodel.Status") + } + + if err := p.notifyStatus(status); err != nil { + return err + } + + if status.VisibilityAdvanced.Federated { + return p.federateStatus(status) + } + return nil + } + return fmt.Errorf("message type unprocessable: %+v", clientMsg) +} + +func (p *processor) federateStatus(status *gtsmodel.Status) error { + // derive the sending account -- it might be attached to the status already + sendingAcct := >smodel.Account{} + if status.GTSAccount != nil { + sendingAcct = status.GTSAccount + } else { + // it wasn't attached so get it from the db instead + if err := p.db.GetByID(status.AccountID, sendingAcct); err != nil { + return err + } + } + + outboxURI, err := url.Parse(sendingAcct.OutboxURI) + if err != nil { + return err + } + + // convert the status to AS format Note + note, err := p.tc.StatusToAS(status) + if err != nil { + return err + } + + _, err = p.federator.FederatingActor().Send(context.Background(), outboxURI, note) + return err +} + +func (p *processor) notifyStatus(status *gtsmodel.Status) error { + return nil +} diff --git a/internal/message/processorutil.go b/internal/message/processorutil.go index c928eec1a..233a18ad8 100644 --- a/internal/message/processorutil.go +++ b/internal/message/processorutil.go @@ -179,7 +179,7 @@ func (p *processor) processLanguage(form *apimodel.AdvancedStatusCreateForm, acc func (p *processor) processMentions(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error { menchies := []string{} - gtsMenchies, err := p.db.MentionStringsToMentions(util.DeriveMentions(form.Status), accountID, status.ID) + gtsMenchies, err := p.db.MentionStringsToMentions(util.DeriveMentionsFromStatus(form.Status), accountID, status.ID) if err != nil { return fmt.Errorf("error generating mentions from status: %s", err) } @@ -198,7 +198,7 @@ func (p *processor) processMentions(form *apimodel.AdvancedStatusCreateForm, acc func (p *processor) processTags(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error { tags := []string{} - gtsTags, err := p.db.TagStringsToTags(util.DeriveHashtags(form.Status), accountID, status.ID) + gtsTags, err := p.db.TagStringsToTags(util.DeriveHashtagsFromStatus(form.Status), accountID, status.ID) if err != nil { return fmt.Errorf("error generating hashtags from status: %s", err) } @@ -217,7 +217,7 @@ func (p *processor) processTags(form *apimodel.AdvancedStatusCreateForm, account func (p *processor) processEmojis(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error { emojis := []string{} - gtsEmojis, err := p.db.EmojiStringsToEmojis(util.DeriveEmojis(form.Status), accountID, status.ID) + gtsEmojis, err := p.db.EmojiStringsToEmojis(util.DeriveEmojisFromStatus(form.Status), accountID, status.ID) if err != nil { return fmt.Errorf("error generating emojis from status: %s", err) } diff --git a/internal/message/statusprocess.go b/internal/message/statusprocess.go index bd4329082..d9d115aec 100644 --- a/internal/message/statusprocess.go +++ b/internal/message/statusprocess.go @@ -81,6 +81,13 @@ func (p *processor) StatusCreate(auth *oauth.Auth, form *apimodel.AdvancedStatus } } + // put the new status in the appropriate channel for async processing + p.fromClientAPI <- FromClientAPI{ + APObjectType: newStatus.ActivityStreamsType, + APActivityType: gtsmodel.ActivityStreamsCreate, + Activity: newStatus, + } + // return the frontend representation of the new status to the submitter return p.tc.StatusToMasto(newStatus, auth.Account, auth.Account, nil, newStatus.GTSReplyToAccount, nil) } diff --git a/internal/oauth/clientstore_test.go b/internal/oauth/clientstore_test.go index b77163e48..58c5148b2 100644 --- a/internal/oauth/clientstore_test.go +++ b/internal/oauth/clientstore_test.go @@ -25,6 +25,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/db/pg" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/oauth2/v4/models" ) @@ -62,7 +63,7 @@ func (suite *PgClientStoreTestSuite) SetupTest() { Database: "postgres", ApplicationName: "gotosocial", } - db, err := db.NewPostgresService(context.Background(), c, log) + db, err := pg.NewPostgresService(context.Background(), c, log) if err != nil { logrus.Panicf("error creating database connection: %s", err) } diff --git a/internal/router/router.go b/internal/router/router.go index cdd079634..eed85771f 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -27,6 +27,7 @@ import ( "path/filepath" "time" + "github.com/gin-contrib/cors" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/memstore" "github.com/gin-gonic/gin" @@ -123,6 +124,14 @@ func New(config *config.Config, logger *logrus.Logger) (Router, error) { // create the actual engine here -- this is the core request routing handler for gts engine := gin.Default() + engine.Use(cors.New(cors.Config{ + AllowAllOrigins: true, + AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, + AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization"}, + AllowCredentials: false, + MaxAge: 12 * time.Hour, + })) + engine.MaxMultipartMemory = 8 << 20 // 8 MiB // create a new session store middleware store, err := sessionStore() @@ -143,10 +152,10 @@ func New(config *config.Config, logger *logrus.Logger) (Router, error) { // create the actual http server here s := &http.Server{ Handler: engine, - ReadTimeout: 1 * time.Second, - WriteTimeout: 1 * time.Second, + ReadTimeout: 60 * time.Second, + WriteTimeout: 5 * time.Second, IdleTimeout: 30 * time.Second, - ReadHeaderTimeout: 2 * time.Second, + ReadHeaderTimeout: 30 * time.Second, } var m *autocert.Manager diff --git a/internal/transport/controller.go b/internal/transport/controller.go index 2ee23f141..72f41b335 100644 --- a/internal/transport/controller.go +++ b/internal/transport/controller.go @@ -54,15 +54,15 @@ func NewController(config *config.Config, clock pub.Clock, client pub.HttpClient func (c *controller) NewTransport(pubKeyID string, privkey crypto.PrivateKey) (pub.Transport, error) { prefs := []httpsig.Algorithm{httpsig.RSA_SHA256, httpsig.RSA_SHA512} digestAlgo := httpsig.DigestSha256 - getHeaders := []string{"(request-target)", "date", "accept"} - postHeaders := []string{"(request-target)", "date", "accept", "digest"} + getHeaders := []string{"(request-target)", "host", "date"} + postHeaders := []string{"(request-target)", "host", "date", "digest"} - getSigner, _, err := httpsig.NewSigner(prefs, digestAlgo, getHeaders, httpsig.Signature) + getSigner, _, err := httpsig.NewSigner(prefs, digestAlgo, getHeaders, httpsig.Signature, 120) if err != nil { return nil, fmt.Errorf("error creating get signer: %s", err) } - postSigner, _, err := httpsig.NewSigner(prefs, digestAlgo, postHeaders, httpsig.Signature) + postSigner, _, err := httpsig.NewSigner(prefs, digestAlgo, postHeaders, httpsig.Signature, 120) if err != nil { return nil, fmt.Errorf("error creating post signer: %s", err) } diff --git a/internal/typeutils/accountable.go b/internal/typeutils/accountable.go deleted file mode 100644 index ba5c4aa2a..000000000 --- a/internal/typeutils/accountable.go +++ /dev/null @@ -1,101 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ - -package typeutils - -import "github.com/go-fed/activity/streams/vocab" - -// Accountable represents the minimum activitypub interface for representing an 'account'. -// This interface is fulfilled by: Person, Application, Organization, Service, and Group -type Accountable interface { - withJSONLDId - withGetTypeName - withPreferredUsername - withIcon - withDisplayName - withImage - withSummary - withDiscoverable - withURL - withPublicKey - withInbox - withOutbox - withFollowing - withFollowers - withFeatured -} - -type withJSONLDId interface { - GetJSONLDId() vocab.JSONLDIdProperty -} - -type withGetTypeName interface { - GetTypeName() string -} - -type withPreferredUsername interface { - GetActivityStreamsPreferredUsername() vocab.ActivityStreamsPreferredUsernameProperty -} - -type withIcon interface { - GetActivityStreamsIcon() vocab.ActivityStreamsIconProperty -} - -type withDisplayName interface { - GetActivityStreamsName() vocab.ActivityStreamsNameProperty -} - -type withImage interface { - GetActivityStreamsImage() vocab.ActivityStreamsImageProperty -} - -type withSummary interface { - GetActivityStreamsSummary() vocab.ActivityStreamsSummaryProperty -} - -type withDiscoverable interface { - GetTootDiscoverable() vocab.TootDiscoverableProperty -} - -type withURL interface { - GetActivityStreamsUrl() vocab.ActivityStreamsUrlProperty -} - -type withPublicKey interface { - GetW3IDSecurityV1PublicKey() vocab.W3IDSecurityV1PublicKeyProperty -} - -type withInbox interface { - GetActivityStreamsInbox() vocab.ActivityStreamsInboxProperty -} - -type withOutbox interface { - GetActivityStreamsOutbox() vocab.ActivityStreamsOutboxProperty -} - -type withFollowing interface { - GetActivityStreamsFollowing() vocab.ActivityStreamsFollowingProperty -} - -type withFollowers interface { - GetActivityStreamsFollowers() vocab.ActivityStreamsFollowersProperty -} - -type withFeatured interface { - GetTootFeatured() vocab.TootFeaturedProperty -} diff --git a/internal/typeutils/asextractionutil.go b/internal/typeutils/asextractionutil.go index 8d39be3ec..4ee3347bd 100644 --- a/internal/typeutils/asextractionutil.go +++ b/internal/typeutils/asextractionutil.go @@ -25,8 +25,12 @@ import ( "errors" "fmt" "net/url" + "strings" + "time" "github.com/go-fed/activity/pub" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/util" ) func extractPreferredUsername(i withPreferredUsername) (string, error) { @@ -40,22 +44,89 @@ func extractPreferredUsername(i withPreferredUsername) (string, error) { return u.GetXMLSchemaString(), nil } -func extractName(i withDisplayName) (string, error) { +func extractName(i withName) (string, error) { nameProp := i.GetActivityStreamsName() if nameProp == nil { return "", errors.New("activityStreamsName not found") } // take the first name string we can find - for nameIter := nameProp.Begin(); nameIter != nameProp.End(); nameIter = nameIter.Next() { - if nameIter.IsXMLSchemaString() && nameIter.GetXMLSchemaString() != "" { - return nameIter.GetXMLSchemaString(), nil + for iter := nameProp.Begin(); iter != nameProp.End(); iter = iter.Next() { + if iter.IsXMLSchemaString() && iter.GetXMLSchemaString() != "" { + return iter.GetXMLSchemaString(), nil } } return "", errors.New("activityStreamsName not found") } +func extractInReplyToURI(i withInReplyTo) (*url.URL, error) { + inReplyToProp := i.GetActivityStreamsInReplyTo() + for iter := inReplyToProp.Begin(); iter != inReplyToProp.End(); iter = iter.Next() { + if iter.IsIRI() { + if iter.GetIRI() != nil { + return iter.GetIRI(), nil + } + } + } + return nil, errors.New("couldn't find iri for in reply to") +} + +func extractTos(i withTo) ([]*url.URL, error) { + to := []*url.URL{} + toProp := i.GetActivityStreamsTo() + for iter := toProp.Begin(); iter != toProp.End(); iter = iter.Next() { + if iter.IsIRI() { + if iter.GetIRI() != nil { + to = append(to, iter.GetIRI()) + } + } + } + return to, nil +} + +func extractCCs(i withCC) ([]*url.URL, error) { + cc := []*url.URL{} + ccProp := i.GetActivityStreamsCc() + for iter := ccProp.Begin(); iter != ccProp.End(); iter = iter.Next() { + if iter.IsIRI() { + if iter.GetIRI() != nil { + cc = append(cc, iter.GetIRI()) + } + } + } + return cc, nil +} + +func extractAttributedTo(i withAttributedTo) (*url.URL, error) { + attributedToProp := i.GetActivityStreamsAttributedTo() + for iter := attributedToProp.Begin(); iter != attributedToProp.End(); iter = iter.Next() { + if iter.IsIRI() { + if iter.GetIRI() != nil { + return iter.GetIRI(), nil + } + } + } + return nil, errors.New("couldn't find iri for attributed to") +} + +func extractPublished(i withPublished) (time.Time, error) { + publishedProp := i.GetActivityStreamsPublished() + if publishedProp == nil { + return time.Time{}, errors.New("published prop was nil") + } + + if !publishedProp.IsXMLSchemaDateTime() { + return time.Time{}, errors.New("published prop was not date time") + } + + t := publishedProp.Get() + if t.IsZero() { + return time.Time{}, errors.New("published time was zero") + } + return t, nil +} + // extractIconURL extracts a URL to a supported image file from something like: // "icon": { // "mediaType": "image/jpeg", @@ -72,12 +143,12 @@ func extractIconURL(i withIcon) (*url.URL, error) { // here in order to find the first one that meets these criteria: // 1. is an image // 2. has a URL so we can grab it - for iconIter := iconProp.Begin(); iconIter != iconProp.End(); iconIter = iconIter.Next() { + for iter := iconProp.Begin(); iter != iconProp.End(); iter = iter.Next() { // 1. is an image - if !iconIter.IsActivityStreamsImage() { + if !iter.IsActivityStreamsImage() { continue } - imageValue := iconIter.GetActivityStreamsImage() + imageValue := iter.GetActivityStreamsImage() if imageValue == nil { continue } @@ -108,12 +179,12 @@ func extractImageURL(i withImage) (*url.URL, error) { // here in order to find the first one that meets these criteria: // 1. is an image // 2. has a URL so we can grab it - for imageIter := imageProp.Begin(); imageIter != imageProp.End(); imageIter = imageIter.Next() { + for iter := imageProp.Begin(); iter != imageProp.End(); iter = iter.Next() { // 1. is an image - if !imageIter.IsActivityStreamsImage() { + if !iter.IsActivityStreamsImage() { continue } - imageValue := imageIter.GetActivityStreamsImage() + imageValue := iter.GetActivityStreamsImage() if imageValue == nil { continue } @@ -134,9 +205,9 @@ func extractSummary(i withSummary) (string, error) { return "", errors.New("summary property was nil") } - for summaryIter := summaryProp.Begin(); summaryIter != summaryProp.End(); summaryIter = summaryIter.Next() { - if summaryIter.IsXMLSchemaString() && summaryIter.GetXMLSchemaString() != "" { - return summaryIter.GetXMLSchemaString(), nil + for iter := summaryProp.Begin(); iter != summaryProp.End(); iter = iter.Next() { + if iter.IsXMLSchemaString() && iter.GetXMLSchemaString() != "" { + return iter.GetXMLSchemaString(), nil } } @@ -156,9 +227,9 @@ func extractURL(i withURL) (*url.URL, error) { return nil, errors.New("url property was nil") } - for urlIter := urlProp.Begin(); urlIter != urlProp.End(); urlIter = urlIter.Next() { - if urlIter.IsIRI() && urlIter.GetIRI() != nil { - return urlIter.GetIRI(), nil + for iter := urlProp.Begin(); iter != urlProp.End(); iter = iter.Next() { + if iter.IsIRI() && iter.GetIRI() != nil { + return iter.GetIRI(), nil } } @@ -171,8 +242,8 @@ func extractPublicKeyForOwner(i withPublicKey, forOwner *url.URL) (*rsa.PublicKe return nil, nil, errors.New("public key property was nil") } - for publicKeyIter := publicKeyProp.Begin(); publicKeyIter != publicKeyProp.End(); publicKeyIter = publicKeyIter.Next() { - pkey := publicKeyIter.Get() + for iter := publicKeyProp.Begin(); iter != publicKeyProp.End(); iter = iter.Next() { + pkey := iter.Get() if pkey == nil { continue } @@ -214,3 +285,263 @@ func extractPublicKeyForOwner(i withPublicKey, forOwner *url.URL) (*rsa.PublicKe } return nil, nil, errors.New("couldn't find public key") } + +func extractContent(i withContent) (string, error) { + contentProperty := i.GetActivityStreamsContent() + if contentProperty == nil { + return "", errors.New("content property was nil") + } + for iter := contentProperty.Begin(); iter != contentProperty.End(); iter = iter.Next() { + if iter.IsXMLSchemaString() && iter.GetXMLSchemaString() != "" { + return iter.GetXMLSchemaString(), nil + } + } + return "", errors.New("no content found") +} + +func extractAttachments(i withAttachment) ([]*gtsmodel.MediaAttachment, error) { + attachments := []*gtsmodel.MediaAttachment{} + + attachmentProp := i.GetActivityStreamsAttachment() + for iter := attachmentProp.Begin(); iter != attachmentProp.End(); iter = iter.Next() { + attachmentable, ok := iter.(Attachmentable) + if !ok { + continue + } + attachment, err := extractAttachment(attachmentable) + if err != nil { + continue + } + attachments = append(attachments, attachment) + } + return attachments, nil +} + +func extractAttachment(i Attachmentable) (*gtsmodel.MediaAttachment, error) { + attachment := >smodel.MediaAttachment{ + File: gtsmodel.File{}, + } + + attachmentURL, err := extractURL(i) + if err != nil { + return nil, err + } + attachment.RemoteURL = attachmentURL.String() + + mediaType := i.GetActivityStreamsMediaType() + if mediaType == nil { + return nil, errors.New("no media type") + } + if mediaType.Get() == "" { + return nil, errors.New("no media type") + } + attachment.File.ContentType = mediaType.Get() + attachment.Type = gtsmodel.FileTypeImage + + name, err := extractName(i) + if err == nil { + attachment.Description = name + } + + blurhash, err := extractBlurhash(i) + if err == nil { + attachment.Blurhash = blurhash + } + + return attachment, nil +} + +func extractBlurhash(i withBlurhash) (string, error) { + if i.GetTootBlurhashProperty() == nil { + return "", errors.New("blurhash property was nil") + } + if i.GetTootBlurhashProperty().Get() == "" { + return "", errors.New("empty blurhash string") + } + return i.GetTootBlurhashProperty().Get(), nil +} + +func extractHashtags(i withTag) ([]*gtsmodel.Tag, error) { + tags := []*gtsmodel.Tag{} + + tagsProp := i.GetActivityStreamsTag() + for iter := tagsProp.Begin(); iter != tagsProp.End(); iter = iter.Next() { + t := iter.GetType() + if t == nil { + continue + } + + if t.GetTypeName() != "Hashtag" { + continue + } + + hashtaggable, ok := t.(Hashtaggable) + if !ok { + continue + } + + tag, err := extractHashtag(hashtaggable) + if err != nil { + continue + } + + tags = append(tags, tag) + } + return tags, nil +} + +func extractHashtag(i Hashtaggable) (*gtsmodel.Tag, error) { + tag := >smodel.Tag{} + + hrefProp := i.GetActivityStreamsHref() + if hrefProp == nil || !hrefProp.IsIRI() { + return nil, errors.New("no href prop") + } + tag.URL = hrefProp.GetIRI().String() + + name, err := extractName(i) + if err != nil { + return nil, err + } + tag.Name = strings.TrimPrefix(name, "#") + + return tag, nil +} + +func extractEmojis(i withTag) ([]*gtsmodel.Emoji, error) { + emojis := []*gtsmodel.Emoji{} + tagsProp := i.GetActivityStreamsTag() + for iter := tagsProp.Begin(); iter != tagsProp.End(); iter = iter.Next() { + t := iter.GetType() + if t == nil { + continue + } + + if t.GetTypeName() != "Emoji" { + continue + } + + emojiable, ok := t.(Emojiable) + if !ok { + continue + } + + emoji, err := extractEmoji(emojiable) + if err != nil { + continue + } + + emojis = append(emojis, emoji) + } + return emojis, nil +} + +func extractEmoji(i Emojiable) (*gtsmodel.Emoji, error) { + emoji := >smodel.Emoji{} + + idProp := i.GetJSONLDId() + if idProp == nil || !idProp.IsIRI() { + return nil, errors.New("no id for emoji") + } + uri := idProp.GetIRI() + emoji.URI = uri.String() + emoji.Domain = uri.Host + + name, err := extractName(i) + if err != nil { + return nil, err + } + emoji.Shortcode = strings.Trim(name, ":") + + if i.GetActivityStreamsIcon() == nil { + return nil, errors.New("no icon for emoji") + } + imageURL, err := extractIconURL(i) + if err != nil { + return nil, errors.New("no url for emoji image") + } + emoji.ImageRemoteURL = imageURL.String() + + return emoji, nil +} + +func extractMentions(i withTag) ([]*gtsmodel.Mention, error) { + mentions := []*gtsmodel.Mention{} + tagsProp := i.GetActivityStreamsTag() + for iter := tagsProp.Begin(); iter != tagsProp.End(); iter = iter.Next() { + t := iter.GetType() + if t == nil { + continue + } + + if t.GetTypeName() != "Mention" { + continue + } + + mentionable, ok := t.(Mentionable) + if !ok { + continue + } + + mention, err := extractMention(mentionable) + if err != nil { + continue + } + + mentions = append(mentions, mention) + } + return mentions, nil +} + +func extractMention(i Mentionable) (*gtsmodel.Mention, error) { + mention := >smodel.Mention{} + + mentionString, err := extractName(i) + if err != nil { + return nil, err + } + + // just make sure the mention string is valid so we can handle it properly later on... + username, domain, err := util.ExtractMentionParts(mentionString) + if err != nil { + return nil, err + } + if username == "" || domain == "" { + return nil, errors.New("username or domain was empty") + } + mention.NameString = mentionString + + // the href prop should be the AP URI of a user we know, eg https://example.org/users/whatever_user + hrefProp := i.GetActivityStreamsHref() + if hrefProp == nil || !hrefProp.IsIRI() { + return nil, errors.New("no href prop") + } + mention.MentionedAccountURI = hrefProp.GetIRI().String() + return mention, nil +} + +func extractActor(i withActor) (*url.URL, error) { + actorProp := i.GetActivityStreamsActor() + if actorProp == nil { + return nil, errors.New("actor property was nil") + } + for iter := actorProp.Begin(); iter != actorProp.End(); iter = iter.Next() { + if iter.IsIRI() && iter.GetIRI() != nil { + return iter.GetIRI(), nil + } + } + return nil, errors.New("no iri found for actor prop") +} + +func extractObject(i withObject) (*url.URL, error) { + objectProp := i.GetActivityStreamsObject() + if objectProp == nil { + return nil, errors.New("object property was nil") + } + for iter := objectProp.Begin(); iter != objectProp.End(); iter = iter.Next() { + if iter.IsIRI() && iter.GetIRI() != nil { + return iter.GetIRI(), nil + } + } + return nil, errors.New("no iri found for object prop") +} diff --git a/internal/typeutils/asinterfaces.go b/internal/typeutils/asinterfaces.go new file mode 100644 index 000000000..970ed2ecf --- /dev/null +++ b/internal/typeutils/asinterfaces.go @@ -0,0 +1,237 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package typeutils + +import "github.com/go-fed/activity/streams/vocab" + +// Accountable represents the minimum activitypub interface for representing an 'account'. +// This interface is fulfilled by: Person, Application, Organization, Service, and Group +type Accountable interface { + withJSONLDId + withTypeName + + withPreferredUsername + withIcon + withName + withImage + withSummary + withDiscoverable + withURL + withPublicKey + withInbox + withOutbox + withFollowing + withFollowers + withFeatured +} + +// Statusable represents the minimum activitypub interface for representing a 'status'. +// This interface is fulfilled by: Article, Document, Image, Video, Note, Page, Event, Place, Mention, Profile +type Statusable interface { + withJSONLDId + withTypeName + + withSummary + withInReplyTo + withPublished + withURL + withAttributedTo + withTo + withCC + withSensitive + withConversation + withContent + withAttachment + withTag + withReplies +} + +// Attachmentable represents the minimum activitypub interface for representing a 'mediaAttachment'. +// This interface is fulfilled by: Audio, Document, Image, Video +type Attachmentable interface { + withTypeName + withMediaType + withURL + withName + withBlurhash + withFocalPoint +} + +// Hashtaggable represents the minimum activitypub interface for representing a 'hashtag' tag. +type Hashtaggable interface { + withTypeName + withHref + withName +} + +// Emojiable represents the minimum interface for an 'emoji' tag. +type Emojiable interface { + withJSONLDId + withTypeName + withName + withUpdated + withIcon +} + +// Mentionable represents the minimum interface for a 'mention' tag. +type Mentionable interface { + withName + withHref +} + +// Followable represents the minimum interface for an activitystreams 'follow' activity. +type Followable interface { + withJSONLDId + withTypeName + + withActor + withObject +} + +type withJSONLDId interface { + GetJSONLDId() vocab.JSONLDIdProperty +} + +type withTypeName interface { + GetTypeName() string +} + +type withPreferredUsername interface { + GetActivityStreamsPreferredUsername() vocab.ActivityStreamsPreferredUsernameProperty +} + +type withIcon interface { + GetActivityStreamsIcon() vocab.ActivityStreamsIconProperty +} + +type withName interface { + GetActivityStreamsName() vocab.ActivityStreamsNameProperty +} + +type withImage interface { + GetActivityStreamsImage() vocab.ActivityStreamsImageProperty +} + +type withSummary interface { + GetActivityStreamsSummary() vocab.ActivityStreamsSummaryProperty +} + +type withDiscoverable interface { + GetTootDiscoverable() vocab.TootDiscoverableProperty +} + +type withURL interface { + GetActivityStreamsUrl() vocab.ActivityStreamsUrlProperty +} + +type withPublicKey interface { + GetW3IDSecurityV1PublicKey() vocab.W3IDSecurityV1PublicKeyProperty +} + +type withInbox interface { + GetActivityStreamsInbox() vocab.ActivityStreamsInboxProperty +} + +type withOutbox interface { + GetActivityStreamsOutbox() vocab.ActivityStreamsOutboxProperty +} + +type withFollowing interface { + GetActivityStreamsFollowing() vocab.ActivityStreamsFollowingProperty +} + +type withFollowers interface { + GetActivityStreamsFollowers() vocab.ActivityStreamsFollowersProperty +} + +type withFeatured interface { + GetTootFeatured() vocab.TootFeaturedProperty +} + +type withAttributedTo interface { + GetActivityStreamsAttributedTo() vocab.ActivityStreamsAttributedToProperty +} + +type withAttachment interface { + GetActivityStreamsAttachment() vocab.ActivityStreamsAttachmentProperty +} + +type withTo interface { + GetActivityStreamsTo() vocab.ActivityStreamsToProperty +} + +type withInReplyTo interface { + GetActivityStreamsInReplyTo() vocab.ActivityStreamsInReplyToProperty +} + +type withCC interface { + GetActivityStreamsCc() vocab.ActivityStreamsCcProperty +} + +type withSensitive interface { + // TODO +} + +type withConversation interface { + // TODO +} + +type withContent interface { + GetActivityStreamsContent() vocab.ActivityStreamsContentProperty +} + +type withPublished interface { + GetActivityStreamsPublished() vocab.ActivityStreamsPublishedProperty +} + +type withTag interface { + GetActivityStreamsTag() vocab.ActivityStreamsTagProperty +} + +type withReplies interface { + GetActivityStreamsReplies() vocab.ActivityStreamsRepliesProperty +} + +type withMediaType interface { + GetActivityStreamsMediaType() vocab.ActivityStreamsMediaTypeProperty +} + +type withBlurhash interface { + GetTootBlurhashProperty() vocab.TootBlurhashProperty +} + +type withFocalPoint interface { + // TODO +} + +type withHref interface { + GetActivityStreamsHref() vocab.ActivityStreamsHrefProperty +} + +type withUpdated interface { + GetActivityStreamsUpdated() vocab.ActivityStreamsUpdatedProperty +} + +type withActor interface { + GetActivityStreamsActor() vocab.ActivityStreamsActorProperty +} + +type withObject interface { + GetActivityStreamsObject() vocab.ActivityStreamsObjectProperty +} diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index 7842411ea..7f0a4c1a4 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -21,6 +21,8 @@ package typeutils import ( "errors" "fmt" + "net/url" + "strings" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" @@ -157,3 +159,202 @@ func (c *converter) ASRepresentationToAccount(accountable Accountable) (*gtsmode return acct, nil } + +func (c *converter) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, error) { + status := >smodel.Status{} + + // uri at which this status is reachable + uriProp := statusable.GetJSONLDId() + if uriProp == nil || !uriProp.IsIRI() { + return nil, errors.New("no id property found, or id was not an iri") + } + status.URI = uriProp.GetIRI().String() + + // web url for viewing this status + if statusURL, err := extractURL(statusable); err == nil { + status.URL = statusURL.String() + } + + // the html-formatted content of this status + if content, err := extractContent(statusable); err == nil { + status.Content = content + } + + // attachments to dereference and fetch later on (we don't do that here) + if attachments, err := extractAttachments(statusable); err == nil { + status.GTSMediaAttachments = attachments + } + + // hashtags to dereference later on + if hashtags, err := extractHashtags(statusable); err == nil { + status.GTSTags = hashtags + } + + // emojis to dereference and fetch later on + if emojis, err := extractEmojis(statusable); err == nil { + status.GTSEmojis = emojis + } + + // mentions to dereference later on + if mentions, err := extractMentions(statusable); err == nil { + status.GTSMentions = mentions + } + + // cw string for this status + if cw, err := extractSummary(statusable); err == nil { + status.ContentWarning = cw + } + + // when was this status created? + published, err := extractPublished(statusable) + if err == nil { + status.CreatedAt = published + } + + // which account posted this status? + // if we don't know the account yet we can dereference it later + attributedTo, err := extractAttributedTo(statusable) + if err != nil { + return nil, errors.New("attributedTo was empty") + } + status.APStatusOwnerURI = attributedTo.String() + + statusOwner := >smodel.Account{} + if err := c.db.GetWhere("uri", attributedTo.String(), statusOwner); err != nil { + return nil, fmt.Errorf("couldn't get status owner from db: %s", err) + } + status.AccountID = statusOwner.ID + status.GTSAccount = statusOwner + + // check if there's a post that this is a reply to + inReplyToURI, err := extractInReplyToURI(statusable) + if err == nil { + // something is set so we can at least set this field on the + // status and dereference using this later if we need to + status.APReplyToStatusURI = inReplyToURI.String() + + // now we can check if we have the replied-to status in our db already + inReplyToStatus := >smodel.Status{} + if err := c.db.GetWhere("uri", inReplyToURI.String(), inReplyToStatus); err == nil { + // we have the status in our database already + // so we can set these fields here and then... + status.InReplyToID = inReplyToStatus.ID + status.InReplyToAccountID = inReplyToStatus.AccountID + status.GTSReplyToStatus = inReplyToStatus + + // ... check if we've seen the account already + inReplyToAccount := >smodel.Account{} + if err := c.db.GetByID(inReplyToStatus.AccountID, inReplyToAccount); err == nil { + status.GTSReplyToAccount = inReplyToAccount + } + } + } + + // visibility entry for this status + var visibility gtsmodel.Visibility + + to, err := extractTos(statusable) + if err != nil { + return nil, fmt.Errorf("error extracting TO values: %s", err) + } + + cc, err := extractCCs(statusable) + if err != nil { + return nil, fmt.Errorf("error extracting CC values: %s", err) + } + + if len(to) == 0 && len(cc) == 0 { + return nil, errors.New("message wasn't TO or CC anyone") + } + + // for visibility derivation, we start by assuming most restrictive, and work our way to least restrictive + + // if it's a DM then it's addressed to SPECIFIC ACCOUNTS and not followers or public + if len(to) != 0 && len(cc) == 0 { + visibility = gtsmodel.VisibilityDirect + } + + // if it's just got followers in TO and it's not also CC'ed to public, it's followers only + if isFollowers(to, statusOwner.FollowersURI) { + visibility = gtsmodel.VisibilityFollowersOnly + } + + // if it's CC'ed to public, it's public or unlocked + // mentioned SPECIFIC ACCOUNTS also get added to CC'es if it's not a direct message + if isPublic(to) { + visibility = gtsmodel.VisibilityPublic + } + + // we should have a visibility by now + if visibility == "" { + return nil, errors.New("couldn't derive visibility") + } + status.Visibility = visibility + + // advanced visibility for this status + // TODO: a lot of work to be done here -- a new type needs to be created for this in go-fed/activity using ASTOOL + + // sensitive + // TODO: this is a bool + + // language + // we might be able to extract this from the contentMap field + + // ActivityStreamsType + status.ActivityStreamsType = gtsmodel.ActivityStreamsObject(statusable.GetTypeName()) + + return status, nil +} + +func (c *converter) ASFollowToFollowRequest(followable Followable) (*gtsmodel.FollowRequest, error) { + + idProp := followable.GetJSONLDId() + if idProp == nil || !idProp.IsIRI() { + return nil, errors.New("no id property set on follow, or was not an iri") + } + uri := idProp.GetIRI().String() + + origin, err := extractActor(followable) + if err != nil { + return nil, errors.New("error extracting actor property from follow") + } + originAccount := >smodel.Account{} + if err := c.db.GetWhere("uri", origin.String(), originAccount); err != nil { + return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) + } + + target, err := extractObject(followable) + if err != nil { + return nil, errors.New("error extracting object property from follow") + } + targetAccount := >smodel.Account{} + if err := c.db.GetWhere("uri", target.String(), targetAccount); err != nil { + return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) + } + + followRequest := >smodel.FollowRequest{ + URI: uri, + AccountID: originAccount.ID, + TargetAccountID: targetAccount.ID, + } + + return followRequest, nil +} + +func isPublic(tos []*url.URL) bool { + for _, entry := range tos { + if strings.EqualFold(entry.String(), "https://www.w3.org/ns/activitystreams#Public") { + return true + } + } + return false +} + +func isFollowers(ccs []*url.URL, followersURI string) bool { + for _, entry := range ccs { + if strings.EqualFold(entry.String(), followersURI) { + return true + } + } + return false +} diff --git a/internal/typeutils/astointernal_test.go b/internal/typeutils/astointernal_test.go index 1cd66a0ab..f1287e027 100644 --- a/internal/typeutils/astointernal_test.go +++ b/internal/typeutils/astointernal_test.go @@ -25,6 +25,7 @@ import ( "testing" "github.com/go-fed/activity/streams" + "github.com/go-fed/activity/streams/vocab" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/typeutils" @@ -36,6 +37,182 @@ type ASToInternalTestSuite struct { } const ( + statusWithMentionsActivityJson = `{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + { + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#", + "votersCount": "toot:votersCount" + } + ], + "id": "https://ondergrond.org/users/dumpsterqueer/statuses/106221634728637552/activity", + "type": "Create", + "actor": "https://ondergrond.org/users/dumpsterqueer", + "published": "2021-05-12T09:58:38Z", + "to": [ + "https://ondergrond.org/users/dumpsterqueer/followers" + ], + "cc": [ + "https://www.w3.org/ns/activitystreams#Public", + "https://social.pixie.town/users/f0x" + ], + "object": { + "id": "https://ondergrond.org/users/dumpsterqueer/statuses/106221634728637552", + "type": "Note", + "summary": null, + "inReplyTo": "https://social.pixie.town/users/f0x/statuses/106221628567855262", + "published": "2021-05-12T09:58:38Z", + "url": "https://ondergrond.org/@dumpsterqueer/106221634728637552", + "attributedTo": "https://ondergrond.org/users/dumpsterqueer", + "to": [ + "https://ondergrond.org/users/dumpsterqueer/followers" + ], + "cc": [ + "https://www.w3.org/ns/activitystreams#Public", + "https://social.pixie.town/users/f0x" + ], + "sensitive": false, + "atomUri": "https://ondergrond.org/users/dumpsterqueer/statuses/106221634728637552", + "inReplyToAtomUri": "https://social.pixie.town/users/f0x/statuses/106221628567855262", + "conversation": "tag:ondergrond.org,2021-05-12:objectId=1132361:objectType=Conversation", + "content": "

@f0x nice there it is:

https://social.pixie.town/users/f0x/statuses/106221628567855262/activity

", + "contentMap": { + "en": "

@f0x nice there it is:

https://social.pixie.town/users/f0x/statuses/106221628567855262/activity

" + }, + "attachment": [], + "tag": [ + { + "type": "Mention", + "href": "https://social.pixie.town/users/f0x", + "name": "@f0x@pixie.town" + } + ], + "replies": { + "id": "https://ondergrond.org/users/dumpsterqueer/statuses/106221634728637552/replies", + "type": "Collection", + "first": { + "type": "CollectionPage", + "next": "https://ondergrond.org/users/dumpsterqueer/statuses/106221634728637552/replies?only_other_accounts=true&page=true", + "partOf": "https://ondergrond.org/users/dumpsterqueer/statuses/106221634728637552/replies", + "items": [] + } + } + } + }` + statusWithEmojisAndTagsAsActivityJson = `{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + { + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#", + "votersCount": "toot:votersCount", + "Hashtag": "as:Hashtag", + "Emoji": "toot:Emoji", + "focalPoint": { + "@container": "@list", + "@id": "toot:focalPoint" + } + } + ], + "id": "https://ondergrond.org/users/dumpsterqueer/statuses/106221567884565704/activity", + "type": "Create", + "actor": "https://ondergrond.org/users/dumpsterqueer", + "published": "2021-05-12T09:41:38Z", + "to": [ + "https://ondergrond.org/users/dumpsterqueer/followers" + ], + "cc": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": { + "id": "https://ondergrond.org/users/dumpsterqueer/statuses/106221567884565704", + "type": "Note", + "summary": null, + "inReplyTo": null, + "published": "2021-05-12T09:41:38Z", + "url": "https://ondergrond.org/@dumpsterqueer/106221567884565704", + "attributedTo": "https://ondergrond.org/users/dumpsterqueer", + "to": [ + "https://ondergrond.org/users/dumpsterqueer/followers" + ], + "cc": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "sensitive": false, + "atomUri": "https://ondergrond.org/users/dumpsterqueer/statuses/106221567884565704", + "inReplyToAtomUri": null, + "conversation": "tag:ondergrond.org,2021-05-12:objectId=1132361:objectType=Conversation", + "content": "

just testing activitypub representations of #tags and #emoji :party_parrot: :amaze: :blobsunglasses:

don't mind me....

", + "contentMap": { + "en": "

just testing activitypub representations of #tags and #emoji :party_parrot: :amaze: :blobsunglasses:

don't mind me....

" + }, + "attachment": [], + "tag": [ + { + "type": "Hashtag", + "href": "https://ondergrond.org/tags/tags", + "name": "#tags" + }, + { + "type": "Hashtag", + "href": "https://ondergrond.org/tags/emoji", + "name": "#emoji" + }, + { + "id": "https://ondergrond.org/emojis/2390", + "type": "Emoji", + "name": ":party_parrot:", + "updated": "2020-11-06T13:42:11Z", + "icon": { + "type": "Image", + "mediaType": "image/gif", + "url": "https://ondergrond.org/system/custom_emojis/images/000/002/390/original/ef133aac7ab23341.gif" + } + }, + { + "id": "https://ondergrond.org/emojis/2395", + "type": "Emoji", + "name": ":amaze:", + "updated": "2020-09-26T12:29:56Z", + "icon": { + "type": "Image", + "mediaType": "image/png", + "url": "https://ondergrond.org/system/custom_emojis/images/000/002/395/original/2c7d9345e57367ed.png" + } + }, + { + "id": "https://ondergrond.org/emojis/764", + "type": "Emoji", + "name": ":blobsunglasses:", + "updated": "2020-09-26T12:13:23Z", + "icon": { + "type": "Image", + "mediaType": "image/png", + "url": "https://ondergrond.org/system/custom_emojis/images/000/000/764/original/3f8eef9de773c90d.png" + } + } + ], + "replies": { + "id": "https://ondergrond.org/users/dumpsterqueer/statuses/106221567884565704/replies", + "type": "Collection", + "first": { + "type": "CollectionPage", + "next": "https://ondergrond.org/users/dumpsterqueer/statuses/106221567884565704/replies?only_other_accounts=true&page=true", + "partOf": "https://ondergrond.org/users/dumpsterqueer/statuses/106221567884565704/replies", + "items": [] + } + } + } + }` gargronAsActivityJson = `{ "@context": [ "https://www.w3.org/ns/activitystreams", @@ -197,6 +374,62 @@ func (suite *ASToInternalTestSuite) TestParseGargron() { // TODO: write assertions here, rn we're just eyeballing the output } +func (suite *ASToInternalTestSuite) TestParseStatus() { + m := make(map[string]interface{}) + err := json.Unmarshal([]byte(statusWithEmojisAndTagsAsActivityJson), &m) + assert.NoError(suite.T(), err) + + t, err := streams.ToType(context.Background(), m) + assert.NoError(suite.T(), err) + + create, ok := t.(vocab.ActivityStreamsCreate) + assert.True(suite.T(), ok) + + obj := create.GetActivityStreamsObject() + assert.NotNil(suite.T(), obj) + + first := obj.Begin() + assert.NotNil(suite.T(), first) + + rep, ok := first.GetType().(typeutils.Statusable) + assert.True(suite.T(), ok) + + status, err := suite.typeconverter.ASStatusToStatus(rep) + assert.NoError(suite.T(), err) + + assert.Len(suite.T(), status.GTSEmojis, 3) + // assert.Len(suite.T(), status.GTSTags, 2) TODO: implement this first so that it can pick up tags +} + +func (suite *ASToInternalTestSuite) TestParseStatusWithMention() { + m := make(map[string]interface{}) + err := json.Unmarshal([]byte(statusWithMentionsActivityJson), &m) + assert.NoError(suite.T(), err) + + t, err := streams.ToType(context.Background(), m) + assert.NoError(suite.T(), err) + + create, ok := t.(vocab.ActivityStreamsCreate) + assert.True(suite.T(), ok) + + obj := create.GetActivityStreamsObject() + assert.NotNil(suite.T(), obj) + + first := obj.Begin() + assert.NotNil(suite.T(), first) + + rep, ok := first.GetType().(typeutils.Statusable) + assert.True(suite.T(), ok) + + status, err := suite.typeconverter.ASStatusToStatus(rep) + assert.NoError(suite.T(), err) + + fmt.Printf("%+v", status) + + assert.Len(suite.T(), status.GTSMentions, 1) + fmt.Println(status.GTSMentions[0]) +} + func (suite *ASToInternalTestSuite) TearDownTest() { testrig.StandardDBTeardown(suite.db) } diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go index f269fa182..8f310c921 100644 --- a/internal/typeutils/converter.go +++ b/internal/typeutils/converter.go @@ -90,6 +90,10 @@ type TypeConverter interface { // ASPersonToAccount converts a remote account/person/application representation into a gts model account ASRepresentationToAccount(accountable Accountable) (*gtsmodel.Account, error) + // ASStatus converts a remote activitystreams 'status' representation into a gts model status. + ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, error) + // ASFollowToFollowRequest converts a remote activitystreams `follow` representation into gts model follow request. + ASFollowToFollowRequest(followable Followable) (*gtsmodel.FollowRequest, error) /* INTERNAL (gts) MODEL TO ACTIVITYSTREAMS MODEL diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go index 73c121155..0216dea5e 100644 --- a/internal/typeutils/internaltoas.go +++ b/internal/typeutils/internaltoas.go @@ -200,15 +200,15 @@ func (c *converter) AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerso // icon // Used as profile avatar. if a.AvatarMediaAttachmentID != "" { - iconProperty := streams.NewActivityStreamsIconProperty() - - iconImage := streams.NewActivityStreamsImage() - avatar := >smodel.MediaAttachment{} if err := c.db.GetByID(a.AvatarMediaAttachmentID, avatar); err != nil { return nil, err } + iconProperty := streams.NewActivityStreamsIconProperty() + + iconImage := streams.NewActivityStreamsImage() + mediaType := streams.NewActivityStreamsMediaTypeProperty() mediaType.Set(avatar.File.ContentType) iconImage.SetActivityStreamsMediaType(mediaType) @@ -228,15 +228,15 @@ func (c *converter) AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerso // image // Used as profile header. if a.HeaderMediaAttachmentID != "" { - headerProperty := streams.NewActivityStreamsImageProperty() - - headerImage := streams.NewActivityStreamsImage() - header := >smodel.MediaAttachment{} if err := c.db.GetByID(a.HeaderMediaAttachmentID, header); err != nil { return nil, err } + headerProperty := streams.NewActivityStreamsImageProperty() + + headerImage := streams.NewActivityStreamsImage() + mediaType := streams.NewActivityStreamsMediaTypeProperty() mediaType.Set(header.File.ContentType) headerImage.SetActivityStreamsMediaType(mediaType) diff --git a/internal/util/regexes.go b/internal/util/regexes.go index a59bd678a..adab8c87f 100644 --- a/internal/util/regexes.go +++ b/internal/util/regexes.go @@ -35,6 +35,11 @@ const ( ) var ( + mentionNameRegexString = `^@([a-zA-Z0-9_]+)(?:@([a-zA-Z0-9_\-\.]+)?)$` + // mention name regex captures the username and domain part from a mention string + // such as @whatever_user@example.org, returning whatever_user and example.org (without the @ symbols) + mentionNameRegex = regexp.MustCompile(mentionNameRegexString) + // mention regex can be played around with here: https://regex101.com/r/qwM9D3/1 mentionFinderRegexString = `(?: |^|\W)(@[a-zA-Z0-9_]+(?:@[a-zA-Z0-9_\-\.]+)?)(?: |\n)` mentionFinderRegex = regexp.MustCompile(mentionFinderRegexString) diff --git a/internal/util/statustools.go b/internal/util/statustools.go index 5591f185a..2c74749e5 100644 --- a/internal/util/statustools.go +++ b/internal/util/statustools.go @@ -19,17 +19,18 @@ package util import ( + "fmt" "strings" ) -// DeriveMentions takes a plaintext (ie., not html-formatted) status, +// DeriveMentionsFromStatus takes a plaintext (ie., not html-formatted) status, // and applies a regex to it to return a deduplicated list of accounts // mentioned in that status. // // It will look for fully-qualified account names in the form "@user@example.org". // or the form "@username" for local users. // The case of the returned mentions will be lowered, for consistency. -func DeriveMentions(status string) []string { +func DeriveMentionsFromStatus(status string) []string { mentionedAccounts := []string{} for _, m := range mentionFinderRegex.FindAllStringSubmatch(status, -1) { mentionedAccounts = append(mentionedAccounts, m[1]) @@ -37,11 +38,11 @@ func DeriveMentions(status string) []string { return lower(unique(mentionedAccounts)) } -// DeriveHashtags takes a plaintext (ie., not html-formatted) status, +// DeriveHashtagsFromStatus takes a plaintext (ie., not html-formatted) status, // and applies a regex to it to return a deduplicated list of hashtags // used in that status, without the leading #. The case of the returned // tags will be lowered, for consistency. -func DeriveHashtags(status string) []string { +func DeriveHashtagsFromStatus(status string) []string { tags := []string{} for _, m := range hashtagFinderRegex.FindAllStringSubmatch(status, -1) { tags = append(tags, m[1]) @@ -49,11 +50,11 @@ func DeriveHashtags(status string) []string { return lower(unique(tags)) } -// DeriveEmojis takes a plaintext (ie., not html-formatted) status, +// DeriveEmojisFromStatus takes a plaintext (ie., not html-formatted) status, // and applies a regex to it to return a deduplicated list of emojis // used in that status, without the surround ::. The case of the returned // emojis will be lowered, for consistency. -func DeriveEmojis(status string) []string { +func DeriveEmojisFromStatus(status string) []string { emojis := []string{} for _, m := range emojiFinderRegex.FindAllStringSubmatch(status, -1) { emojis = append(emojis, m[1]) @@ -61,6 +62,21 @@ func DeriveEmojis(status string) []string { return lower(unique(emojis)) } +// ExtractMentionParts extracts the username test_user and the domain example.org +// from a mention string like @test_user@example.org. +// +// If nothing is matched, it will return an error. +func ExtractMentionParts(mention string) (username, domain string, err error) { + matches := mentionNameRegex.FindStringSubmatch(mention) + if matches == nil || len(matches) != 3 { + err = fmt.Errorf("could't match mention %s", mention) + return + } + username = matches[1] + domain = matches[2] + return +} + // unique returns a deduplicated version of a given string slice. func unique(s []string) []string { keys := make(map[string]bool) diff --git a/internal/util/statustools_test.go b/internal/util/statustools_test.go index 7c9af2cbd..2a12c7690 100644 --- a/internal/util/statustools_test.go +++ b/internal/util/statustools_test.go @@ -42,7 +42,7 @@ func (suite *StatusTestSuite) TestDeriveMentionsOK() { here is a duplicate mention: @hello@test.lgbt ` - menchies := util.DeriveMentions(statusText) + menchies := util.DeriveMentionsFromStatus(statusText) assert.Len(suite.T(), menchies, 4) assert.Equal(suite.T(), "@dumpsterqueer@example.org", menchies[0]) assert.Equal(suite.T(), "@someone_else@testing.best-horse.com", menchies[1]) @@ -52,7 +52,7 @@ func (suite *StatusTestSuite) TestDeriveMentionsOK() { func (suite *StatusTestSuite) TestDeriveMentionsEmpty() { statusText := `` - menchies := util.DeriveMentions(statusText) + menchies := util.DeriveMentionsFromStatus(statusText) assert.Len(suite.T(), menchies, 0) } @@ -67,7 +67,7 @@ func (suite *StatusTestSuite) TestDeriveHashtagsOK() { #111111 thisalsoshouldn'twork#### ##` - tags := util.DeriveHashtags(statusText) + tags := util.DeriveHashtagsFromStatus(statusText) assert.Len(suite.T(), tags, 5) assert.Equal(suite.T(), "testing123", tags[0]) assert.Equal(suite.T(), "also", tags[1]) @@ -90,7 +90,7 @@ Here's some normal text with an :emoji: at the end :underscores_ok_too: ` - tags := util.DeriveEmojis(statusText) + tags := util.DeriveEmojisFromStatus(statusText) assert.Len(suite.T(), tags, 7) assert.Equal(suite.T(), "test", tags[0]) assert.Equal(suite.T(), "another", tags[1]) diff --git a/internal/util/uri.go b/internal/util/uri.go index 538df9210..edcfc5c02 100644 --- a/internal/util/uri.go +++ b/internal/util/uri.go @@ -58,9 +58,15 @@ const ( // APAccount can be used the set and retrieve the account being interacted with APAccount APContextKey = "account" // APRequestingAccount can be used to set and retrieve the account of an incoming federation request. + // This will often be the actor of the instance that's posting the request. APRequestingAccount APContextKey = "requestingAccount" + // APRequestingActorIRI can be used to set and retrieve the actor of an incoming federation request. + // This will usually be the owner of whatever activity is being posted. + APRequestingActorIRI APContextKey = "requestingActorIRI" // APRequestingPublicKeyID can be used to set and retrieve the public key ID of an incoming federation request. APRequestingPublicKeyID APContextKey = "requestingPublicKeyID" + // APFromFederatorChanKey can be used to pass a pointer to the fromFederator channel into the federator for use in callbacks. + APFromFederatorChanKey APContextKey = "fromFederatorChan" ) type ginContextKey struct{} diff --git a/testrig/db.go b/testrig/db.go index 4d22ab3c8..0b4920191 100644 --- a/testrig/db.go +++ b/testrig/db.go @@ -23,6 +23,7 @@ import ( "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/db/pg" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -54,7 +55,7 @@ func NewTestDB() db.DB { config := NewTestConfig() l := logrus.New() l.SetLevel(logrus.TraceLevel) - testDB, err := db.NewPostgresService(context.Background(), config, l) + testDB, err := pg.NewPostgresService(context.Background(), config, l) if err != nil { panic(err) } diff --git a/testrig/federator.go b/testrig/federator.go index 63ad520db..c2d86fd21 100644 --- a/testrig/federator.go +++ b/testrig/federator.go @@ -24,6 +24,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/transport" ) +// NewTestFederator returns a federator with the given database and (mock!!) transport controller. func NewTestFederator(db db.DB, tc transport.Controller) federation.Federator { return federation.NewFederator(db, tc, NewTestConfig(), NewTestLog(), NewTestTypeConverter(db)) } diff --git a/testrig/testmodels.go b/testrig/testmodels.go index 6d7390729..90d7f63b6 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -1037,6 +1037,7 @@ func NewTestFaves() map[string]*gtsmodel.StatusFave { } } +// ActivityWithSignature wraps a pub.Activity along with its signature headers, for testing. type ActivityWithSignature struct { Activity pub.Activity SignatureHeader string @@ -1076,11 +1077,11 @@ func NewTestActivities(accounts map[string]*gtsmodel.Account) map[string]Activit // NewTestFediPeople returns a bunch of activity pub Person representations for testing converters and so on. func NewTestFediPeople() map[string]typeutils.Accountable { - new_person_1priv, err := rsa.GenerateKey(rand.Reader, 2048) + newPerson1Priv, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { panic(err) } - new_person_1pub := &new_person_1priv.PublicKey + newPerson1Pub := &newPerson1Priv.PublicKey return map[string]typeutils.Accountable{ "new_person_1": newPerson( @@ -1096,7 +1097,7 @@ func NewTestFediPeople() map[string]typeutils.Accountable { URLMustParse("https://unknown-instance.com/@brand_new_person"), true, URLMustParse("https://unknown-instance.com/users/brand_new_person#main-key"), - new_person_1pub, + newPerson1Pub, URLMustParse("https://unknown-instance.com/media/some_avatar_filename.jpeg"), "image/jpeg", URLMustParse("https://unknown-instance.com/media/some_header_filename.jpeg"), @@ -1105,6 +1106,7 @@ func NewTestFediPeople() map[string]typeutils.Accountable { } } +// NewTestDereferenceRequests returns a map of incoming dereference requests, with their signatures. func NewTestDereferenceRequests(accounts map[string]*gtsmodel.Account) map[string]ActivityWithSignature { sig, digest, date := getSignatureForDereference(accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, URLMustParse(accounts["local_account_1"].URI)) return map[string]ActivityWithSignature{