gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit cc48294c31a76e94fa879ad0d8d5dbd7e94c651b
parent 742f985d5b0620ad14015f9a2df9940edc254bf4
Author: Tobi Smethurst <31960611+tsmethurst@users.noreply.github.com>
Date:   Sat, 15 May 2021 11:58:11 +0200

Inbox post (#22)


    Inbox POST from federated servers now working for statuses and follow requests.
    Follow request client API added.
    Start work on federating outgoing messages.
    Other fixes and changes/tidying up.

Diffstat:
Mgo.mod | 50++++++++++++++++++++++++++++++++++++--------------
Mgo.sum | 186+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Minternal/api/client/auth/auth_test.go | 3++-
Minternal/api/client/auth/middleware.go | 3++-
Minternal/api/client/auth/token.go | 30++++++++++++++++++++++++++++++
Ainternal/api/client/followrequest/accept.go | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/api/client/followrequest/deny.go | 27+++++++++++++++++++++++++++
Ainternal/api/client/followrequest/followrequest.go | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/api/client/followrequest/get.go | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Minternal/api/client/instance/instance.go | 2+-
Minternal/api/client/instance/instanceget.go | 1+
Minternal/api/client/media/media.go | 2++
Minternal/api/client/media/mediacreate.go | 20+++++++++++---------
Minternal/api/client/media/mediaget.go | 2+-
Minternal/api/model/application.go | 8++++----
Minternal/api/model/attachment.go | 12++++++------
Minternal/api/model/status.go | 4++++
Ainternal/api/s2s/user/inboxpost.go | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minternal/api/s2s/user/user.go | 3+++
Minternal/api/s2s/webfinger/webfinger.go | 2+-
Ainternal/api/security/extraheaders.go | 8++++++++
Minternal/api/security/security.go | 1+
Minternal/db/db.go | 14+++++++++++++-
Dinternal/db/federating_db.go | 359-------------------------------------------------------------------------------
Dinternal/db/federating_db_test.go | 21---------------------
Dinternal/db/pg.go | 1089-------------------------------------------------------------------------------
Ainternal/db/pg/pg.go | 1127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dinternal/db/pg_test.go | 21---------------------
Minternal/federation/clock.go | 1+
Minternal/federation/commonbehavior.go | 6+++---
Ainternal/federation/federating_db.go | 599+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/federation/federating_db_test.go | 21+++++++++++++++++++++
Minternal/federation/federatingactor.go | 14++++++++++++++
Minternal/federation/federatingprotocol.go | 154+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Minternal/gotosocial/actions.go | 7+++++--
Minternal/gtsmodel/account.go | 6+++---
Minternal/gtsmodel/mention.go | 16++++++++++++++--
Minternal/gtsmodel/status.go | 18+++++++++++++++++-
Minternal/gtsmodel/tag.go | 2++
Minternal/media/media_test.go | 3++-
Minternal/message/fediprocess.go | 8++++++++
Ainternal/message/frprocess.go | 42++++++++++++++++++++++++++++++++++++++++++
Minternal/message/processor.go | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minternal/message/processorutil.go | 6+++---
Minternal/message/statusprocess.go | 7+++++++
Minternal/oauth/clientstore_test.go | 3++-
Minternal/router/router.go | 15++++++++++++---
Minternal/transport/controller.go | 8++++----
Dinternal/typeutils/accountable.go | 101-------------------------------------------------------------------------------
Minternal/typeutils/asextractionutil.go | 367+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Ainternal/typeutils/asinterfaces.go | 237+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minternal/typeutils/astointernal.go | 201+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minternal/typeutils/astointernal_test.go | 233+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minternal/typeutils/converter.go | 4++++
Minternal/typeutils/internaltoas.go | 16++++++++--------
Minternal/util/regexes.go | 5+++++
Minternal/util/statustools.go | 28++++++++++++++++++++++------
Minternal/util/statustools_test.go | 8++++----
Minternal/util/uri.go | 6++++++
Mtestrig/db.go | 3++-
Mtestrig/federator.go | 1+
Mtestrig/testmodels.go | 8+++++---
62 files changed, 3670 insertions(+), 1788 deletions(-)

diff --git 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 <http://www.gnu.org/licenses/>. +*/ + +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/api/client/followrequest/deny.go b/internal/api/client/followrequest/deny.go @@ -0,0 +1,27 @@ +/* + 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 <http://www.gnu.org/licenses/>. +*/ + +package followrequest + +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 @@ -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 <http://www.gnu.org/licenses/>. +*/ + +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 @@ -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 <http://www.gnu.org/licenses/>. +*/ + +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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 <http://www.gnu.org/licenses/>. +*/ + +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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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/federating_db.go b/internal/db/federating_db.go @@ -1,359 +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 <http://www.gnu.org/licenses/>. -*/ - -package db - -import ( - "context" - "errors" - "fmt" - "net/url" - "sync" - - "github.com/go-fed/activity/pub" - "github.com/go-fed/activity/streams/vocab" - "github.com/sirupsen/logrus" - "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "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 -} - -func NewFederatingDB(db DB, config *config.Config, log *logrus.Logger) pub.Database { - return &federatingDB{ - locks: new(sync.Map), - db: db, - config: config, - log: log, - } -} - -/* - GO-FED DB INTERFACE-IMPLEMENTING FUNCTIONS -*/ - -// Lock takes a lock for the object at the specified id. If an error -// is returned, the lock must not have been taken. -// -// The lock must be able to succeed for an id that does not exist in -// the database. This means acquiring the lock does not guarantee the -// entry exists in the database. -// -// Locks are encouraged to be lightweight and in the Go layer, as some -// processes require tight loops acquiring and releasing locks. -// -// Used to ensure race conditions in multiple requests do not occur. -func (f *federatingDB) Lock(c context.Context, id *url.URL) error { - // Before any other Database methods are called, the relevant `id` - // entries are locked to allow for fine-grained concurrency. - - // Strategy: create a new lock, if stored, continue. Otherwise, lock the - // existing mutex. - mu := &sync.Mutex{} - mu.Lock() // Optimistically lock if we do store it. - i, loaded := f.locks.LoadOrStore(id.String(), mu) - if loaded { - mu = i.(*sync.Mutex) - mu.Lock() - } - return nil -} - -// Unlock makes the lock for the object at the specified id available. -// If an error is returned, the lock must have still been freed. -// -// Used to ensure race conditions in multiple requests do not occur. -func (f *federatingDB) Unlock(c context.Context, id *url.URL) error { - // Once Go-Fed is done calling Database methods, the relevant `id` - // entries are unlocked. - - i, ok := f.locks.Load(id.String()) - if !ok { - return errors.New("missing an id in unlock") - } - mu := i.(*sync.Mutex) - mu.Unlock() - return nil -} - -// InboxContains returns true if the OrderedCollection at 'inbox' -// contains the specified 'id'. -// -// 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) { - - 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()) - } - _, statusID, err := util.ParseStatusesPath(inbox) - if err != nil { - return false, fmt.Errorf("status URI %s was not parseable: %s", id.String(), err) - } - - if err := f.db.GetByID(statusID, &gtsmodel.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) - } - - // we must have it - return true, nil -} - -// GetInbox returns the first ordered collection page of the outbox at -// the specified IRI, for prepending new items. -// -// 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 -} - -// SetInbox saves the inbox value given from GetInbox, with new items -// prepended. Note that the new items must not be added as independent -// database entries. Separate calls to Create will do that. -// -// The library makes this call only after acquiring a lock first. -func (f *federatingDB) SetInbox(c context.Context, inbox vocab.ActivityStreamsOrderedCollectionPage) error { - return nil -} - -// Owns returns true if the IRI belongs to this instance, and if -// 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) { - // if the id host isn't this instance host, we don't own this IRI - if id.Host != f.config.Host { - return false, nil - } - - // apparently we own it, so what *is* it? - - // check if it's a status, eg /users/example_username/statuses/SOME_UUID_OF_A_STATUS - if util.IsStatusesPath(id) { - _, uid, err := util.ParseStatusesPath(id) - if err != nil { - return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err) - } - if err := f.db.GetWhere("uri", uid, &gtsmodel.Status{}); err != nil { - if _, ok := err.(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) - } - return true, nil - } - - // check if it's a user, eg /users/example_username - if util.IsUserPath(id) { - username, err := util.ParseUserPath(id) - if err != nil { - return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err) - } - if err := f.db.GetLocalAccountByUsername(username, &gtsmodel.Account{}); err != nil { - if _, ok := err.(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) - } - return true, nil - } - - return false, fmt.Errorf("could not match activityID: %s", id.String()) -} - -// ActorForOutbox fetches the actor's IRI for the given outbox IRI. -// -// 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) { - if !util.IsOutboxPath(outboxIRI) { - return nil, fmt.Errorf("%s is not an outbox URI", outboxIRI.String()) - } - acct := &gtsmodel.Account{} - if err := f.db.GetWhere("outbox_uri", outboxIRI.String(), acct); err != nil { - if _, ok := err.(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()) - } - return url.Parse(acct.URI) -} - -// ActorForInbox fetches the actor's IRI for the given outbox IRI. -// -// 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) { - if !util.IsInboxPath(inboxIRI) { - return nil, fmt.Errorf("%s is not an inbox URI", inboxIRI.String()) - } - acct := &gtsmodel.Account{} - if err := f.db.GetWhere("inbox_uri", inboxIRI.String(), acct); err != nil { - if _, ok := err.(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()) - } - return url.Parse(acct.URI) -} - -// OutboxForInbox fetches the corresponding actor's outbox IRI for the -// actor's inbox IRI. -// -// 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) { - if !util.IsInboxPath(inboxIRI) { - return nil, fmt.Errorf("%s is not an inbox URI", inboxIRI.String()) - } - acct := &gtsmodel.Account{} - if err := f.db.GetWhere("inbox_uri", inboxIRI.String(), acct); err != nil { - if _, ok := err.(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()) - } - return url.Parse(acct.OutboxURI) -} - -// Exists returns true if the database has an entry for the specified -// id. It may not be owned by this application instance. -// -// 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) { - return false, nil -} - -// Get returns the database entry for the specified id. -// -// 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) { - return nil, nil -} - -// Create adds a new entry to the database which must be able to be -// keyed by its id. -// -// Note that Activity values received from federated peers may also be -// created in the database this way if the Federating Protocol is -// enabled. The client may freely decide to store only the id instead of -// the entire value. -// -// The library makes this call only after acquiring a lock first. -// -// 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 { - return nil -} - -// Update sets an existing entry to the database based on the value's -// id. -// -// Note that Activity values received from federated peers may also be -// updated in the database this way if the Federating Protocol is -// enabled. The client may freely decide to store only the id instead of -// the entire value. -// -// The library makes this call only after acquiring a lock first. -func (f *federatingDB) Update(c context.Context, asType vocab.Type) error { - return nil -} - -// Delete removes the entry with the given id. -// -// Delete is only called for federated objects. Deletes from the Social -// Protocol instead call Update to create a Tombstone. -// -// The library makes this call only after acquiring a lock first. -func (f *federatingDB) Delete(c context.Context, id *url.URL) error { - return nil -} - -// GetOutbox returns the first ordered collection page of the outbox -// at the specified IRI, for prepending new items. -// -// 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) { - return nil, nil -} - -// SetOutbox saves the outbox value given from GetOutbox, with new items -// prepended. Note that the new items must not be added as independent -// database entries. Separate calls to Create will do that. -// -// The library makes this call only after acquiring a lock first. -func (f *federatingDB) SetOutbox(c context.Context, outbox vocab.ActivityStreamsOrderedCollectionPage) error { - return nil -} - -// NewID creates a new IRI id for the provided activity or object. The -// implementation does not need to set the 'id' property and simply -// needs to determine the value. -// -// 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 -} - -// Followers obtains the Followers Collection for an actor with the -// given id. -// -// If modified, the library will then call Update. -// -// 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 -} - -// Following obtains the Following Collection for an actor with the -// given id. -// -// 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 -} - -// Liked obtains the Liked Collection for an actor with the -// given id. -// -// 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) { - return nil, nil -} diff --git a/internal/db/federating_db_test.go b/internal/db/federating_db_test.go @@ -1,21 +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 <http://www.gnu.org/licenses/>. -*/ - -package db - -// TODO: write tests for pgfed diff --git a/internal/db/pg.go b/internal/db/pg.go @@ -1,1089 +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 <http://www.gnu.org/licenses/>. -*/ - -package db - -import ( - "context" - "crypto/rand" - "crypto/rsa" - "errors" - "fmt" - "net" - "net/mail" - "regexp" - "strings" - "time" - - "github.com/go-fed/activity/pub" - "github.com/go-pg/pg/extra/pgdebug" - "github.com/go-pg/pg/v10" - "github.com/go-pg/pg/v10/orm" - "github.com/google/uuid" - "github.com/sirupsen/logrus" - "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/util" - "golang.org/x/crypto/bcrypt" -) - -// postgresService satisfies the DB interface -type postgresService struct { - config *config.Config - conn *pg.DB - log *logrus.Logger - cancel context.CancelFunc - federationDB pub.Database -} - -// 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) { - opts, err := derivePGOptions(c) - if err != nil { - return nil, fmt.Errorf("could not create postgres service: %s", err) - } - log.Debugf("using pg options: %+v", opts) - - // create a connection - pgCtx, cancel := context.WithCancel(ctx) - conn := pg.Connect(opts).WithContext(pgCtx) - - // this will break the logfmt format we normally log in, - // since we can't choose where pg outputs to and it defaults to - // stdout. So use this option with care! - if log.GetLevel() >= logrus.TraceLevel { - conn.AddQueryHook(pgdebug.DebugHook{ - // Print all queries. - Verbose: true, - }) - } - - // actually *begin* the connection so that we can tell if the db is there and listening - if err := conn.Ping(ctx); err != nil { - cancel() - return nil, fmt.Errorf("db connection error: %s", err) - } - - // print out discovered postgres version - var version string - if _, err = conn.QueryOneContext(ctx, pg.Scan(&version), "SELECT version()"); err != nil { - cancel() - return nil, fmt.Errorf("db connection error: %s", err) - } - log.Infof("connected to postgres version: %s", version) - - ps := &postgresService{ - config: c, - conn: conn, - log: log, - cancel: cancel, - } - - federatingDB := NewFederatingDB(ps, c, log) - ps.federationDB = federatingDB - - // we can confidently return this useable postgres service now - return ps, nil -} - -/* - HANDY STUFF -*/ - -// 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) - } - - // validate port - if c.DBConfig.Port == 0 { - return nil, errors.New("no port set") - } - - // validate address - if c.DBConfig.Address == "" { - return nil, errors.New("no address set") - } - - ipv4Regex := regexp.MustCompile(`^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$`) - hostnameRegex := regexp.MustCompile(`^(?:[a-z0-9]+(?:-[a-z0-9]+)*\.)+[a-z]{2,}$`) - if !hostnameRegex.MatchString(c.DBConfig.Address) && !ipv4Regex.MatchString(c.DBConfig.Address) && c.DBConfig.Address != "localhost" { - return nil, fmt.Errorf("address %s was neither an ipv4 address nor a valid hostname", c.DBConfig.Address) - } - - // validate username - if c.DBConfig.User == "" { - return nil, errors.New("no user set") - } - - // validate that there's a password - if c.DBConfig.Password == "" { - return nil, errors.New("no password set") - } - - // validate database - if c.DBConfig.Database == "" { - return nil, errors.New("no database set") - } - - // We can rely on the pg library we're using to set - // sensible defaults for everything we don't set here. - options := &pg.Options{ - Addr: fmt.Sprintf("%s:%d", c.DBConfig.Address, c.DBConfig.Port), - User: c.DBConfig.User, - Password: c.DBConfig.Password, - Database: c.DBConfig.Database, - ApplicationName: c.ApplicationName, - } - - return options, nil -} - -/* - FEDERATION FUNCTIONALITY -*/ - -func (ps *postgresService) Federation() pub.Database { - return ps.federationDB -} - -/* - BASIC DB FUNCTIONALITY -*/ - -func (ps *postgresService) CreateTable(i interface{}) error { - return ps.conn.Model(i).CreateTable(&orm.CreateTableOptions{ - IfNotExists: true, - }) -} - -func (ps *postgresService) DropTable(i interface{}) error { - return ps.conn.Model(i).DropTable(&orm.DropTableOptions{ - IfExists: true, - }) -} - -func (ps *postgresService) Stop(ctx context.Context) error { - ps.log.Info("closing db connection") - if err := ps.conn.Close(); err != nil { - // only cancel if there's a problem closing the db - ps.cancel() - return err - } - return nil -} - -func (ps *postgresService) IsHealthy(ctx context.Context) error { - return ps.conn.Ping(ctx) -} - -func (ps *postgresService) CreateSchema(ctx context.Context) error { - models := []interface{}{ - (*gtsmodel.Account)(nil), - (*gtsmodel.Status)(nil), - (*gtsmodel.User)(nil), - } - ps.log.Info("creating db schema") - - for _, model := range models { - err := ps.conn.Model(model).CreateTable(&orm.CreateTableOptions{ - IfNotExists: true, - }) - if err != nil { - return err - } - } - - ps.log.Info("db schema created") - return nil -} - -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 err - - } - return nil -} - -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 err - } - return nil -} - -// func (ps *postgresService) GetWhereMany(i interface{}, where ...model.Where) error { -// return nil -// } - -func (ps *postgresService) GetAll(i interface{}) error { - if err := ps.conn.Model(i).Select(); err != nil { - if err == pg.ErrNoRows { - return ErrNoEntries{} - } - return err - } - return nil -} - -func (ps *postgresService) Put(i interface{}) error { - _, err := ps.conn.Model(i).Insert(i) - return err -} - -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 err - } - return nil -} - -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 err - } - return nil -} - -func (ps *postgresService) UpdateOneByID(id string, key string, value interface{}, i interface{}) error { - _, err := ps.conn.Model(i).Set("? = ?", pg.Safe(key), value).Where("id = ?", id).Update() - return err -} - -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 err - } - return nil -} - -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 err - } - return nil -} - -/* - HANDY SHORTCUTS -*/ - -func (ps *postgresService) CreateInstanceAccount() error { - username := ps.config.Host - key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - ps.log.Errorf("error creating new rsa key: %s", err) - return err - } - - newAccountURIs := util.GenerateURIsForAccount(username, ps.config.Protocol, ps.config.Host) - a := &gtsmodel.Account{ - Username: ps.config.Host, - DisplayName: username, - URL: newAccountURIs.UserURL, - PrivateKey: key, - PublicKey: &key.PublicKey, - PublicKeyURI: newAccountURIs.PublicKeyURI, - ActorType: gtsmodel.ActivityStreamsPerson, - URI: newAccountURIs.UserURI, - InboxURI: newAccountURIs.InboxURI, - OutboxURI: newAccountURIs.OutboxURI, - FollowersURI: newAccountURIs.FollowersURI, - FollowingURI: newAccountURIs.FollowingURI, - FeaturedCollectionURI: newAccountURIs.CollectionURI, - } - inserted, err := ps.conn.Model(a).Where("username = ?", username).SelectOrInsert() - if err != nil { - return err - } - if inserted { - ps.log.Infof("created instance account %s with id %s", username, a.ID) - } else { - ps.log.Infof("instance account %s already exists with id %s", username, a.ID) - } - return nil -} - -func (ps *postgresService) CreateInstanceInstance() error { - i := &gtsmodel.Instance{ - Domain: ps.config.Host, - Title: ps.config.Host, - URI: fmt.Sprintf("%s://%s", ps.config.Protocol, ps.config.Host), - } - inserted, err := ps.conn.Model(i).Where("domain = ?", ps.config.Host).SelectOrInsert() - if err != nil { - return err - } - if inserted { - ps.log.Infof("created instance instance %s with id %s", ps.config.Host, i.ID) - } else { - ps.log.Infof("instance instance %s already exists with id %s", ps.config.Host, i.ID) - } - return nil -} - -func (ps *postgresService) GetAccountByUserID(userID string, account *gtsmodel.Account) error { - user := &gtsmodel.User{ - ID: userID, - } - if err := ps.conn.Model(user).Where("id = ?", userID).Select(); err != nil { - if err == pg.ErrNoRows { - return ErrNoEntries{} - } - return err - } - if err := ps.conn.Model(account).Where("id = ?", user.AccountID).Select(); err != nil { - if err == pg.ErrNoRows { - return ErrNoEntries{} - } - return err - } - return nil -} - -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 err - } - return nil -} - -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 err - } - return nil -} - -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 err - } - return nil -} - -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 err - } - return nil -} - -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 err - } - return nil -} - -func (ps *postgresService) GetStatusesByTimeDescending(accountID string, statuses *[]gtsmodel.Status, limit int) error { - q := ps.conn.Model(statuses).Order("created_at DESC") - if limit != 0 { - q = q.Limit(limit) - } - if accountID != "" { - q = q.Where("account_id = ?", accountID) - } - if err := q.Select(); err != nil { - if err == pg.ErrNoRows { - return ErrNoEntries{} - } - return err - } - return nil -} - -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 err - } - return nil - -} - -func (ps *postgresService) IsUsernameAvailable(username string) error { - // if no error we fail because it means we found something - // if error but it's not pg.ErrNoRows then we fail - // if err is pg.ErrNoRows we're good, we found nothing so continue - if err := ps.conn.Model(&gtsmodel.Account{}).Where("username = ?", username).Where("domain = ?", nil).Select(); err == nil { - return fmt.Errorf("username %s already in use", username) - } else if err != pg.ErrNoRows { - return fmt.Errorf("db error: %s", err) - } - return nil -} - -func (ps *postgresService) IsEmailAvailable(email string) error { - // parse the domain from the email - m, err := mail.ParseAddress(email) - if err != nil { - return fmt.Errorf("error parsing email address %s: %s", email, err) - } - domain := strings.Split(m.Address, "@")[1] // domain will always be the second part after @ - - // check if the email domain is blocked - if err := ps.conn.Model(&gtsmodel.EmailDomainBlock{}).Where("domain = ?", domain).Select(); err == nil { - // fail because we found something - return fmt.Errorf("email domain %s is blocked", domain) - } else if err != pg.ErrNoRows { - // fail because we got an unexpected error - return fmt.Errorf("db error: %s", err) - } - - // check if this email is associated with a user already - if err := ps.conn.Model(&gtsmodel.User{}).Where("email = ?", email).WhereOr("unconfirmed_email = ?", email).Select(); err == nil { - // fail because we found something - return fmt.Errorf("email %s already in use", email) - } else if err != pg.ErrNoRows { - // fail because we got an unexpected error - return fmt.Errorf("db error: %s", err) - } - return nil -} - -func (ps *postgresService) NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string) (*gtsmodel.User, error) { - key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - ps.log.Errorf("error creating new rsa key: %s", err) - return nil, err - } - - newAccountURIs := util.GenerateURIsForAccount(username, ps.config.Protocol, ps.config.Host) - - a := &gtsmodel.Account{ - Username: username, - DisplayName: username, - Reason: reason, - URL: newAccountURIs.UserURL, - PrivateKey: key, - PublicKey: &key.PublicKey, - PublicKeyURI: newAccountURIs.PublicKeyURI, - ActorType: gtsmodel.ActivityStreamsPerson, - URI: newAccountURIs.UserURI, - InboxURI: newAccountURIs.InboxURI, - OutboxURI: newAccountURIs.OutboxURI, - FollowersURI: newAccountURIs.FollowersURI, - FollowingURI: newAccountURIs.FollowingURI, - FeaturedCollectionURI: newAccountURIs.CollectionURI, - } - if _, err = ps.conn.Model(a).Insert(); err != nil { - return nil, err - } - - pw, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - return nil, fmt.Errorf("error hashing password: %s", err) - } - u := &gtsmodel.User{ - AccountID: a.ID, - EncryptedPassword: string(pw), - SignUpIP: signUpIP, - Locale: locale, - UnconfirmedEmail: email, - CreatedByApplicationID: appID, - Approved: !requireApproval, // if we don't require moderator approval, just pre-approve the user - } - if _, err = ps.conn.Model(u).Insert(); err != nil { - return nil, err - } - - return u, nil -} - -func (ps *postgresService) SetHeaderOrAvatarForAccountID(mediaAttachment *gtsmodel.MediaAttachment, accountID string) error { - if mediaAttachment.Avatar && mediaAttachment.Header { - return errors.New("one media attachment cannot be both header and avatar") - } - - var headerOrAVI string - if mediaAttachment.Avatar { - headerOrAVI = "avatar" - } else if mediaAttachment.Header { - headerOrAVI = "header" - } else { - return errors.New("given media attachment was neither a header nor an avatar") - } - - // TODO: there are probably more side effects here that need to be handled - if _, err := ps.conn.Model(mediaAttachment).OnConflict("(id) DO UPDATE").Insert(); err != nil { - return err - } - - if _, err := ps.conn.Model(&gtsmodel.Account{}).Set(fmt.Sprintf("%s_media_attachment_id = ?", headerOrAVI), mediaAttachment.ID).Where("id = ?", accountID).Update(); err != nil { - return err - } - return nil -} - -func (ps *postgresService) GetHeaderForAccountID(header *gtsmodel.MediaAttachment, accountID string) error { - acct := &gtsmodel.Account{} - if err := ps.conn.Model(acct).Where("id = ?", accountID).Select(); err != nil { - if err == pg.ErrNoRows { - return ErrNoEntries{} - } - return err - } - - if acct.HeaderMediaAttachmentID == "" { - return ErrNoEntries{} - } - - if err := ps.conn.Model(header).Where("id = ?", acct.HeaderMediaAttachmentID).Select(); err != nil { - if err == pg.ErrNoRows { - return ErrNoEntries{} - } - return err - } - return nil -} - -func (ps *postgresService) GetAvatarForAccountID(avatar *gtsmodel.MediaAttachment, accountID string) error { - acct := &gtsmodel.Account{} - if err := ps.conn.Model(acct).Where("id = ?", accountID).Select(); err != nil { - if err == pg.ErrNoRows { - return ErrNoEntries{} - } - return err - } - - if acct.AvatarMediaAttachmentID == "" { - return ErrNoEntries{} - } - - if err := ps.conn.Model(avatar).Where("id = ?", acct.AvatarMediaAttachmentID).Select(); err != nil { - if err == pg.ErrNoRows { - return ErrNoEntries{} - } - return err - } - return nil -} - -func (ps *postgresService) Blocked(account1 string, account2 string) (bool, error) { - // TODO: check domain blocks as well - var blocked bool - if err := ps.conn.Model(&gtsmodel.Block{}). - Where("account_id = ?", account1).Where("target_account_id = ?", account2). - WhereOr("target_account_id = ?", account1).Where("account_id = ?", account2). - Select(); err != nil { - if err == pg.ErrNoRows { - blocked = false - return blocked, nil - } - return blocked, err - } - blocked = true - return blocked, nil -} - -func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account, relevantAccounts *gtsmodel.RelevantAccounts) (bool, error) { - l := ps.log.WithField("func", "StatusVisible") - - // if target account is suspended then don't show the status - if !targetAccount.SuspendedAt.IsZero() { - l.Debug("target account suspended at is not zero") - return false, nil - } - - // if the target user doesn't exist (anymore) then the status also shouldn't be visible - targetUser := &gtsmodel.User{} - 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, err - } - - // if target user is disabled, not yet approved, or not confirmed then don't show the status - // (although in the latter two cases it's unlikely they posted a status yet anyway, but you never know!) - if targetUser.Disabled || !targetUser.Approved || targetUser.ConfirmedAt.IsZero() { - l.Debug("target user is disabled, not approved, or not confirmed") - return false, nil - } - - // If requesting account is nil, that means whoever requested the status didn't auth, or their auth failed. - // In this case, we can still serve the status if it's public, otherwise we definitely shouldn't. - if requestingAccount == nil { - - if targetStatus.Visibility == gtsmodel.VisibilityPublic { - return true, nil - } - l.Debug("requesting account is nil but the target status isn't public") - return false, nil - } - - // if requesting account is suspended then don't show the status -- although they probably shouldn't have gotten - // this far (ie., been authed) in the first place: this is just for safety. - if !requestingAccount.SuspendedAt.IsZero() { - l.Debug("requesting account is suspended") - return false, nil - } - - // check if we have a local account -- if so we can check the user for that account in the DB - if requestingAccount.Domain == "" { - requestingUser := &gtsmodel.User{} - if err := ps.conn.Model(requestingUser).Where("account_id = ?", requestingAccount.ID).Select(); err != nil { - // if the requesting account is local but doesn't have a corresponding user in the db this is a problem - if err == pg.ErrNoRows { - l.Debug("requesting account is local but there's no corresponding user") - return false, nil - } - l.Debugf("requesting account is local but there was an error getting the corresponding user: %s", err) - return false, err - } - // okay, user exists, so make sure it has full privileges/is confirmed/approved - if requestingUser.Disabled || !requestingUser.Approved || requestingUser.ConfirmedAt.IsZero() { - l.Debug("requesting account is local but corresponding user is either disabled, not approved, or not confirmed") - return false, nil - } - } - - // if the target status belongs to the requesting account, they should always be able to view it at this point - if targetStatus.AccountID == requestingAccount.ID { - return true, nil - } - - // At this point we have a populated targetAccount, targetStatus, and requestingAccount, so we can check for blocks and whathaveyou - // First check if a block exists directly between the target account (which authored the status) and the requesting account. - if blocked, err := ps.Blocked(targetAccount.ID, requestingAccount.ID); err != nil { - l.Debugf("something went wrong figuring out if the accounts have a block: %s", err) - return false, err - } else if blocked { - // don't allow the status to be viewed if a block exists in *either* direction between these two accounts, no creepy stalking please - l.Debug("a block exists between requesting account and target account") - return false, nil - } - - // check other accounts mentioned/boosted by/replied to by the status, if they exist - if relevantAccounts != nil { - // status replies to account id - if relevantAccounts.ReplyToAccount != nil { - if blocked, err := ps.Blocked(relevantAccounts.ReplyToAccount.ID, requestingAccount.ID); err != nil { - return false, err - } else if blocked { - return false, nil - } - } - - // status boosts accounts id - if relevantAccounts.BoostedAccount != nil { - if blocked, err := ps.Blocked(relevantAccounts.BoostedAccount.ID, requestingAccount.ID); err != nil { - return false, err - } else if blocked { - return false, nil - } - } - - // status boosts a reply to account id - if relevantAccounts.BoostedReplyToAccount != nil { - if blocked, err := ps.Blocked(relevantAccounts.BoostedReplyToAccount.ID, requestingAccount.ID); err != nil { - return false, err - } else if blocked { - return false, nil - } - } - - // status mentions accounts - for _, a := range relevantAccounts.MentionedAccounts { - if blocked, err := ps.Blocked(a.ID, requestingAccount.ID); err != nil { - return false, err - } else if blocked { - return false, nil - } - } - } - - // at this point we know neither account blocks the other, or another account mentioned or otherwise referred to in the status - // that means it's now just a matter of checking the visibility settings of the status itself - switch targetStatus.Visibility { - case gtsmodel.VisibilityPublic, gtsmodel.VisibilityUnlocked: - // no problem here, just return OK - return true, nil - case gtsmodel.VisibilityFollowersOnly: - // check one-way follow - follows, err := ps.Follows(requestingAccount, targetAccount) - if err != nil { - return false, err - } - if !follows { - return false, nil - } - return true, nil - case gtsmodel.VisibilityMutualsOnly: - // check mutual follow - mutuals, err := ps.Mutuals(requestingAccount, targetAccount) - if err != nil { - return false, err - } - if !mutuals { - return false, nil - } - return true, nil - case gtsmodel.VisibilityDirect: - // make sure the requesting account is mentioned in the status - for _, menchie := range targetStatus.Mentions { - if menchie == requestingAccount.ID { - return true, nil // yep it's mentioned! - } - } - return false, nil // it's not mentioned -_- - } - - return false, errors.New("reached the end of StatusVisible with no result") -} - -func (ps *postgresService) Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) { - return ps.conn.Model(&gtsmodel.Follow{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists() -} - -func (ps *postgresService) Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error) { - // make sure account 1 follows account 2 - f1, err := ps.conn.Model(&gtsmodel.Follow{}).Where("account_id = ?", account1.ID).Where("target_account_id = ?", account2.ID).Exists() - if err != nil { - if err == pg.ErrNoRows { - return false, nil - } - return false, err - } - - // make sure account 2 follows account 1 - f2, err := ps.conn.Model(&gtsmodel.Follow{}).Where("account_id = ?", account2.ID).Where("target_account_id = ?", account1.ID).Exists() - if err != nil { - if err == pg.ErrNoRows { - return false, nil - } - return false, err - } - - return f1 && f2, nil -} - -func (ps *postgresService) PullRelevantAccountsFromStatus(targetStatus *gtsmodel.Status) (*gtsmodel.RelevantAccounts, error) { - accounts := &gtsmodel.RelevantAccounts{ - MentionedAccounts: []*gtsmodel.Account{}, - } - - // get the replied to account from the status and add it to the pile - if targetStatus.InReplyToAccountID != "" { - repliedToAccount := &gtsmodel.Account{} - if err := ps.conn.Model(repliedToAccount).Where("id = ?", targetStatus.InReplyToAccountID).Select(); err != nil { - return accounts, err - } - accounts.ReplyToAccount = repliedToAccount - } - - // get the boosted account from the status and add it to the pile - if targetStatus.BoostOfID != "" { - // retrieve the boosted status first - boostedStatus := &gtsmodel.Status{} - if err := ps.conn.Model(boostedStatus).Where("id = ?", targetStatus.BoostOfID).Select(); err != nil { - return accounts, err - } - boostedAccount := &gtsmodel.Account{} - if err := ps.conn.Model(boostedAccount).Where("id = ?", boostedStatus.AccountID).Select(); err != nil { - return accounts, err - } - accounts.BoostedAccount = boostedAccount - - // the boosted status might be a reply to another account so we should get that too - if boostedStatus.InReplyToAccountID != "" { - boostedStatusRepliedToAccount := &gtsmodel.Account{} - if err := ps.conn.Model(boostedStatusRepliedToAccount).Where("id = ?", boostedStatus.InReplyToAccountID).Select(); err != nil { - return accounts, err - } - accounts.BoostedReplyToAccount = boostedStatusRepliedToAccount - } - } - - // now get all accounts with IDs that are mentioned in the status - for _, mentionedAccountID := range targetStatus.Mentions { - mentionedAccount := &gtsmodel.Account{} - if err := ps.conn.Model(mentionedAccount).Where("id = ?", mentionedAccountID).Select(); err != nil { - return accounts, err - } - accounts.MentionedAccounts = append(accounts.MentionedAccounts, mentionedAccount) - } - - return accounts, nil -} - -func (ps *postgresService) GetReplyCountForStatus(status *gtsmodel.Status) (int, error) { - return ps.conn.Model(&gtsmodel.Status{}).Where("in_reply_to_id = ?", status.ID).Count() -} - -func (ps *postgresService) GetReblogCountForStatus(status *gtsmodel.Status) (int, error) { - return ps.conn.Model(&gtsmodel.Status{}).Where("boost_of_id = ?", status.ID).Count() -} - -func (ps *postgresService) GetFaveCountForStatus(status *gtsmodel.Status) (int, error) { - return ps.conn.Model(&gtsmodel.StatusFave{}).Where("status_id = ?", status.ID).Count() -} - -func (ps *postgresService) StatusFavedBy(status *gtsmodel.Status, accountID string) (bool, error) { - return ps.conn.Model(&gtsmodel.StatusFave{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() -} - -func (ps *postgresService) StatusRebloggedBy(status *gtsmodel.Status, accountID string) (bool, error) { - return ps.conn.Model(&gtsmodel.Status{}).Where("boost_of_id = ?", status.ID).Where("account_id = ?", accountID).Exists() -} - -func (ps *postgresService) StatusMutedBy(status *gtsmodel.Status, accountID string) (bool, error) { - return ps.conn.Model(&gtsmodel.StatusMute{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() -} - -func (ps *postgresService) StatusBookmarkedBy(status *gtsmodel.Status, accountID string) (bool, error) { - return ps.conn.Model(&gtsmodel.StatusBookmark{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() -} - -func (ps *postgresService) StatusPinnedBy(status *gtsmodel.Status, accountID string) (bool, error) { - return ps.conn.Model(&gtsmodel.StatusPin{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() -} - -func (ps *postgresService) FaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error) { - // first check if a fave already exists, we can just return if so - existingFave := &gtsmodel.StatusFave{} - err := ps.conn.Model(existingFave).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Select() - if err == nil { - // fave already exists so just return nothing at all - return nil, nil - } - - // an error occurred so it might exist or not, we don't know - if err != pg.ErrNoRows { - return nil, err - } - - // it doesn't exist so create it - newFave := &gtsmodel.StatusFave{ - AccountID: accountID, - TargetAccountID: status.AccountID, - StatusID: status.ID, - } - if _, err = ps.conn.Model(newFave).Insert(); err != nil { - return nil, err - } - - return newFave, nil -} - -func (ps *postgresService) UnfaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error) { - // if a fave doesn't exist, we don't need to do anything - existingFave := &gtsmodel.StatusFave{} - err := ps.conn.Model(existingFave).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Select() - // the fave doesn't exist so return nothing at all - if err == pg.ErrNoRows { - return nil, nil - } - - // an error occurred so it might exist or not, we don't know - if err != nil && err != pg.ErrNoRows { - return nil, err - } - - // the fave exists so remove it - if _, err = ps.conn.Model(&gtsmodel.StatusFave{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Delete(); err != nil { - return nil, err - } - - return existingFave, nil -} - -func (ps *postgresService) WhoFavedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error) { - accounts := []*gtsmodel.Account{} - - faves := []*gtsmodel.StatusFave{} - if err := ps.conn.Model(&faves).Where("status_id = ?", status.ID).Select(); err != nil { - if err == pg.ErrNoRows { - return accounts, nil // no rows just means nobody has faved this status, so that's fine - } - return nil, err // an actual error has occurred - } - - for _, f := range faves { - acc := &gtsmodel.Account{} - if err := ps.conn.Model(acc).Where("id = ?", f.AccountID).Select(); err != nil { - if err == pg.ErrNoRows { - continue // the account doesn't exist for some reason??? but this isn't the place to worry about that so just skip it - } - return nil, err // an actual error has occurred - } - accounts = append(accounts, acc) - } - return accounts, nil -} - -/* - CONVERSION FUNCTIONS -*/ - -func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, originAccountID string, statusID string) ([]*gtsmodel.Mention, error) { - menchies := []*gtsmodel.Mention{} - for _, a := range targetAccounts { - // A mentioned account looks like "@test@example.org" or just "@test" for a local account - // -- we can guarantee this from the regex that targetAccounts should have been derived from. - // But we still need to do a bit of fiddling to get what we need here -- the username and domain (if given). - - // 1. trim off the first @ - t := strings.TrimPrefix(a, "@") - - // 2. split the username and domain - s := strings.Split(t, "@") - - // 3. if it's length 1 it's a local account, length 2 means remote, anything else means something is wrong - var local bool - switch len(s) { - case 1: - local = true - case 2: - local = false - default: - return nil, fmt.Errorf("mentioned account format '%s' was not valid", a) - } - - var username, domain string - username = s[0] - if !local { - domain = s[1] - } - - // 4. check we now have a proper username and domain - if username == "" || (!local && domain == "") { - return nil, fmt.Errorf("username or domain for '%s' was nil", a) - } - - // okay we're good now, we can start pulling accounts out of the database - mentionedAccount := &gtsmodel.Account{} - var err error - if local { - // local user -- should have a null domain - err = ps.conn.Model(mentionedAccount).Where("username = ?", username).Where("? IS NULL", pg.Ident("domain")).Select() - } else { - // remote user -- should have domain defined - err = ps.conn.Model(mentionedAccount).Where("username = ?", username).Where("? = ?", pg.Ident("domain"), domain).Select() - } - - if err != nil { - if err == pg.ErrNoRows { - // no result found for this username/domain so just don't include it as a mencho and carry on about our business - ps.log.Debugf("no account found with username '%s' and domain '%s', skipping it", username, domain) - continue - } - // a serious error has happened so bail - return nil, fmt.Errorf("error getting account with username '%s' and domain '%s': %s", username, domain, err) - } - - // id, createdAt and updatedAt will be populated by the db, so we have everything we need! - menchies = append(menchies, &gtsmodel.Mention{ - StatusID: statusID, - OriginAccountID: originAccountID, - TargetAccountID: mentionedAccount.ID, - }) - } - return menchies, nil -} - -func (ps *postgresService) TagStringsToTags(tags []string, originAccountID string, statusID string) ([]*gtsmodel.Tag, error) { - newTags := []*gtsmodel.Tag{} - for _, t := range tags { - tag := &gtsmodel.Tag{} - // we can use selectorinsert here to create the new tag if it doesn't exist already - // inserted will be true if this is a new tag we just created - if err := ps.conn.Model(tag).Where("name = ?", t).Select(); err != nil { - if err == pg.ErrNoRows { - // tag doesn't exist yet so populate it - tag.ID = uuid.NewString() - tag.Name = t - tag.FirstSeenFromAccountID = originAccountID - tag.CreatedAt = time.Now() - tag.UpdatedAt = time.Now() - tag.Useable = true - tag.Listable = true - } else { - return nil, fmt.Errorf("error getting tag with name %s: %s", t, err) - } - } - - // bail already if the tag isn't useable - if !tag.Useable { - continue - } - tag.LastStatusAt = time.Now() - newTags = append(newTags, tag) - } - return newTags, nil -} - -func (ps *postgresService) EmojiStringsToEmojis(emojis []string, originAccountID string, statusID string) ([]*gtsmodel.Emoji, error) { - newEmojis := []*gtsmodel.Emoji{} - for _, e := range emojis { - emoji := &gtsmodel.Emoji{} - err := ps.conn.Model(emoji).Where("shortcode = ?", e).Where("visible_in_picker = true").Where("disabled = false").Select() - if err != nil { - if err == pg.ErrNoRows { - // no result found for this username/domain so just don't include it as an emoji and carry on about our business - ps.log.Debugf("no emoji found with shortcode %s, skipping it", e) - continue - } - // a serious error has happened so bail - return nil, fmt.Errorf("error getting emoji with shortcode %s: %s", e, err) - } - newEmojis = append(newEmojis, emoji) - } - return newEmojis, nil -} diff --git a/internal/db/pg/pg.go b/internal/db/pg/pg.go @@ -0,0 +1,1127 @@ +/* + 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 <http://www.gnu.org/licenses/>. +*/ + +package pg + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "errors" + "fmt" + "net" + "net/mail" + "regexp" + "strings" + "time" + + "github.com/go-fed/activity/pub" + "github.com/go-pg/pg/extra/pgdebug" + "github.com/go-pg/pg/v10" + "github.com/go-pg/pg/v10/orm" + "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" +) + +// postgresService satisfies the DB interface +type postgresService struct { + config *config.Config + conn *pg.DB + log *logrus.Logger + cancel context.CancelFunc + federationDB pub.Database +} + +// 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.DB, error) { + opts, err := derivePGOptions(c) + if err != nil { + return nil, fmt.Errorf("could not create postgres service: %s", err) + } + log.Debugf("using pg options: %+v", opts) + + // create a connection + pgCtx, cancel := context.WithCancel(ctx) + conn := pg.Connect(opts).WithContext(pgCtx) + + // this will break the logfmt format we normally log in, + // since we can't choose where pg outputs to and it defaults to + // stdout. So use this option with care! + if log.GetLevel() >= logrus.TraceLevel { + conn.AddQueryHook(pgdebug.DebugHook{ + // Print all queries. + Verbose: true, + }) + } + + // actually *begin* the connection so that we can tell if the db is there and listening + if err := conn.Ping(ctx); err != nil { + cancel() + return nil, fmt.Errorf("db connection error: %s", err) + } + + // print out discovered postgres version + var version string + if _, err = conn.QueryOneContext(ctx, pg.Scan(&version), "SELECT version()"); err != nil { + cancel() + return nil, fmt.Errorf("db connection error: %s", err) + } + log.Infof("connected to postgres version: %s", version) + + ps := &postgresService{ + config: c, + conn: conn, + log: log, + cancel: cancel, + } + + federatingDB := federation.NewFederatingDB(ps, c, log) + ps.federationDB = federatingDB + + // we can confidently return this useable postgres service now + return ps, nil +} + +/* + HANDY STUFF +*/ + +// 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) != db.DBTypePostgres { + return nil, fmt.Errorf("expected db type of %s but got %s", db.DBTypePostgres, c.DBConfig.Type) + } + + // validate port + if c.DBConfig.Port == 0 { + return nil, errors.New("no port set") + } + + // validate address + if c.DBConfig.Address == "" { + return nil, errors.New("no address set") + } + + ipv4Regex := regexp.MustCompile(`^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$`) + hostnameRegex := regexp.MustCompile(`^(?:[a-z0-9]+(?:-[a-z0-9]+)*\.)+[a-z]{2,}$`) + if !hostnameRegex.MatchString(c.DBConfig.Address) && !ipv4Regex.MatchString(c.DBConfig.Address) && c.DBConfig.Address != "localhost" { + return nil, fmt.Errorf("address %s was neither an ipv4 address nor a valid hostname", c.DBConfig.Address) + } + + // validate username + if c.DBConfig.User == "" { + return nil, errors.New("no user set") + } + + // validate that there's a password + if c.DBConfig.Password == "" { + return nil, errors.New("no password set") + } + + // validate database + if c.DBConfig.Database == "" { + return nil, errors.New("no database set") + } + + // We can rely on the pg library we're using to set + // sensible defaults for everything we don't set here. + options := &pg.Options{ + Addr: fmt.Sprintf("%s:%d", c.DBConfig.Address, c.DBConfig.Port), + User: c.DBConfig.User, + Password: c.DBConfig.Password, + Database: c.DBConfig.Database, + ApplicationName: c.ApplicationName, + } + + return options, nil +} + +/* + FEDERATION FUNCTIONALITY +*/ + +func (ps *postgresService) Federation() pub.Database { + return ps.federationDB +} + +/* + BASIC DB FUNCTIONALITY +*/ + +func (ps *postgresService) CreateTable(i interface{}) error { + return ps.conn.Model(i).CreateTable(&orm.CreateTableOptions{ + IfNotExists: true, + }) +} + +func (ps *postgresService) DropTable(i interface{}) error { + return ps.conn.Model(i).DropTable(&orm.DropTableOptions{ + IfExists: true, + }) +} + +func (ps *postgresService) Stop(ctx context.Context) error { + ps.log.Info("closing db connection") + if err := ps.conn.Close(); err != nil { + // only cancel if there's a problem closing the db + ps.cancel() + return err + } + return nil +} + +func (ps *postgresService) IsHealthy(ctx context.Context) error { + return ps.conn.Ping(ctx) +} + +func (ps *postgresService) CreateSchema(ctx context.Context) error { + models := []interface{}{ + (*gtsmodel.Account)(nil), + (*gtsmodel.Status)(nil), + (*gtsmodel.User)(nil), + } + ps.log.Info("creating db schema") + + for _, model := range models { + err := ps.conn.Model(model).CreateTable(&orm.CreateTableOptions{ + IfNotExists: true, + }) + if err != nil { + return err + } + } + + ps.log.Info("db schema created") + return nil +} + +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 db.ErrNoEntries{} + } + return err + + } + return nil +} + +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 db.ErrNoEntries{} + } + return err + } + return nil +} + +// func (ps *postgresService) GetWhereMany(i interface{}, where ...model.Where) error { +// return nil +// } + +func (ps *postgresService) GetAll(i interface{}) error { + if err := ps.conn.Model(i).Select(); err != nil { + if err == pg.ErrNoRows { + return db.ErrNoEntries{} + } + return err + } + return nil +} + +func (ps *postgresService) Put(i interface{}) error { + _, err := ps.conn.Model(i).Insert(i) + return err +} + +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 db.ErrNoEntries{} + } + return err + } + return nil +} + +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 db.ErrNoEntries{} + } + return err + } + return nil +} + +func (ps *postgresService) UpdateOneByID(id string, key string, value interface{}, i interface{}) error { + _, err := ps.conn.Model(i).Set("? = ?", pg.Safe(key), value).Where("id = ?", id).Update() + return err +} + +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 db.ErrNoEntries{} + } + return err + } + return nil +} + +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 db.ErrNoEntries{} + } + return err + } + return nil +} + +/* + HANDY SHORTCUTS +*/ + +func (ps *postgresService) AcceptFollowRequest(originAccountID string, targetAccountID string) error { + fr := &gtsmodel.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 := &gtsmodel.Follow{ + AccountID: originAccountID, + TargetAccountID: targetAccountID, + URI: fr.URI, + } + + if _, err := ps.conn.Model(follow).Insert(); err != nil { + return err + } + + if _, err := ps.conn.Model(&gtsmodel.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) + if err != nil { + ps.log.Errorf("error creating new rsa key: %s", err) + return err + } + + newAccountURIs := util.GenerateURIsForAccount(username, ps.config.Protocol, ps.config.Host) + a := &gtsmodel.Account{ + Username: ps.config.Host, + DisplayName: username, + URL: newAccountURIs.UserURL, + PrivateKey: key, + PublicKey: &key.PublicKey, + PublicKeyURI: newAccountURIs.PublicKeyURI, + ActorType: gtsmodel.ActivityStreamsPerson, + URI: newAccountURIs.UserURI, + InboxURI: newAccountURIs.InboxURI, + OutboxURI: newAccountURIs.OutboxURI, + FollowersURI: newAccountURIs.FollowersURI, + FollowingURI: newAccountURIs.FollowingURI, + FeaturedCollectionURI: newAccountURIs.CollectionURI, + } + inserted, err := ps.conn.Model(a).Where("username = ?", username).SelectOrInsert() + if err != nil { + return err + } + if inserted { + ps.log.Infof("created instance account %s with id %s", username, a.ID) + } else { + ps.log.Infof("instance account %s already exists with id %s", username, a.ID) + } + return nil +} + +func (ps *postgresService) CreateInstanceInstance() error { + i := &gtsmodel.Instance{ + Domain: ps.config.Host, + Title: ps.config.Host, + URI: fmt.Sprintf("%s://%s", ps.config.Protocol, ps.config.Host), + } + inserted, err := ps.conn.Model(i).Where("domain = ?", ps.config.Host).SelectOrInsert() + if err != nil { + return err + } + if inserted { + ps.log.Infof("created instance instance %s with id %s", ps.config.Host, i.ID) + } else { + ps.log.Infof("instance instance %s already exists with id %s", ps.config.Host, i.ID) + } + return nil +} + +func (ps *postgresService) GetAccountByUserID(userID string, account *gtsmodel.Account) error { + user := &gtsmodel.User{ + ID: userID, + } + if err := ps.conn.Model(user).Where("id = ?", userID).Select(); err != nil { + if err == pg.ErrNoRows { + return db.ErrNoEntries{} + } + return err + } + if err := ps.conn.Model(account).Where("id = ?", user.AccountID).Select(); err != nil { + if err == pg.ErrNoRows { + return db.ErrNoEntries{} + } + return err + } + return nil +} + +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 db.ErrNoEntries{} + } + return err + } + return nil +} + +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 nil + } + return err + } + return nil +} + +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 nil + } + return err + } + return nil +} + +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 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 + } + return nil +} + +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 db.ErrNoEntries{} + } + return err + } + return nil +} + +func (ps *postgresService) GetStatusesByTimeDescending(accountID string, statuses *[]gtsmodel.Status, limit int) error { + q := ps.conn.Model(statuses).Order("created_at DESC") + if limit != 0 { + q = q.Limit(limit) + } + if accountID != "" { + q = q.Where("account_id = ?", accountID) + } + if err := q.Select(); err != nil { + if err == pg.ErrNoRows { + return db.ErrNoEntries{} + } + return err + } + return nil +} + +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 db.ErrNoEntries{} + } + return err + } + return nil + +} + +func (ps *postgresService) IsUsernameAvailable(username string) error { + // if no error we fail because it means we found something + // if error but it's not pg.ErrNoRows then we fail + // if err is pg.ErrNoRows we're good, we found nothing so continue + if err := ps.conn.Model(&gtsmodel.Account{}).Where("username = ?", username).Where("domain = ?", nil).Select(); err == nil { + return fmt.Errorf("username %s already in use", username) + } else if err != pg.ErrNoRows { + return fmt.Errorf("db error: %s", err) + } + return nil +} + +func (ps *postgresService) IsEmailAvailable(email string) error { + // parse the domain from the email + m, err := mail.ParseAddress(email) + if err != nil { + return fmt.Errorf("error parsing email address %s: %s", email, err) + } + domain := strings.Split(m.Address, "@")[1] // domain will always be the second part after @ + + // check if the email domain is blocked + if err := ps.conn.Model(&gtsmodel.EmailDomainBlock{}).Where("domain = ?", domain).Select(); err == nil { + // fail because we found something + return fmt.Errorf("email domain %s is blocked", domain) + } else if err != pg.ErrNoRows { + // fail because we got an unexpected error + return fmt.Errorf("db error: %s", err) + } + + // check if this email is associated with a user already + if err := ps.conn.Model(&gtsmodel.User{}).Where("email = ?", email).WhereOr("unconfirmed_email = ?", email).Select(); err == nil { + // fail because we found something + return fmt.Errorf("email %s already in use", email) + } else if err != pg.ErrNoRows { + // fail because we got an unexpected error + return fmt.Errorf("db error: %s", err) + } + return nil +} + +func (ps *postgresService) NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string) (*gtsmodel.User, error) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + ps.log.Errorf("error creating new rsa key: %s", err) + return nil, err + } + + newAccountURIs := util.GenerateURIsForAccount(username, ps.config.Protocol, ps.config.Host) + + a := &gtsmodel.Account{ + Username: username, + DisplayName: username, + Reason: reason, + URL: newAccountURIs.UserURL, + PrivateKey: key, + PublicKey: &key.PublicKey, + PublicKeyURI: newAccountURIs.PublicKeyURI, + ActorType: gtsmodel.ActivityStreamsPerson, + URI: newAccountURIs.UserURI, + InboxURI: newAccountURIs.InboxURI, + OutboxURI: newAccountURIs.OutboxURI, + FollowersURI: newAccountURIs.FollowersURI, + FollowingURI: newAccountURIs.FollowingURI, + FeaturedCollectionURI: newAccountURIs.CollectionURI, + } + if _, err = ps.conn.Model(a).Insert(); err != nil { + return nil, err + } + + pw, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return nil, fmt.Errorf("error hashing password: %s", err) + } + u := &gtsmodel.User{ + AccountID: a.ID, + EncryptedPassword: string(pw), + SignUpIP: signUpIP, + Locale: locale, + UnconfirmedEmail: email, + CreatedByApplicationID: appID, + Approved: !requireApproval, // if we don't require moderator approval, just pre-approve the user + } + if _, err = ps.conn.Model(u).Insert(); err != nil { + return nil, err + } + + return u, nil +} + +func (ps *postgresService) SetHeaderOrAvatarForAccountID(mediaAttachment *gtsmodel.MediaAttachment, accountID string) error { + if mediaAttachment.Avatar && mediaAttachment.Header { + return errors.New("one media attachment cannot be both header and avatar") + } + + var headerOrAVI string + if mediaAttachment.Avatar { + headerOrAVI = "avatar" + } else if mediaAttachment.Header { + headerOrAVI = "header" + } else { + return errors.New("given media attachment was neither a header nor an avatar") + } + + // TODO: there are probably more side effects here that need to be handled + if _, err := ps.conn.Model(mediaAttachment).OnConflict("(id) DO UPDATE").Insert(); err != nil { + return err + } + + if _, err := ps.conn.Model(&gtsmodel.Account{}).Set(fmt.Sprintf("%s_media_attachment_id = ?", headerOrAVI), mediaAttachment.ID).Where("id = ?", accountID).Update(); err != nil { + return err + } + return nil +} + +func (ps *postgresService) GetHeaderForAccountID(header *gtsmodel.MediaAttachment, accountID string) error { + acct := &gtsmodel.Account{} + if err := ps.conn.Model(acct).Where("id = ?", accountID).Select(); err != nil { + if err == pg.ErrNoRows { + return db.ErrNoEntries{} + } + return err + } + + if acct.HeaderMediaAttachmentID == "" { + return db.ErrNoEntries{} + } + + if err := ps.conn.Model(header).Where("id = ?", acct.HeaderMediaAttachmentID).Select(); err != nil { + if err == pg.ErrNoRows { + return db.ErrNoEntries{} + } + return err + } + return nil +} + +func (ps *postgresService) GetAvatarForAccountID(avatar *gtsmodel.MediaAttachment, accountID string) error { + acct := &gtsmodel.Account{} + if err := ps.conn.Model(acct).Where("id = ?", accountID).Select(); err != nil { + if err == pg.ErrNoRows { + return db.ErrNoEntries{} + } + return err + } + + if acct.AvatarMediaAttachmentID == "" { + return db.ErrNoEntries{} + } + + if err := ps.conn.Model(avatar).Where("id = ?", acct.AvatarMediaAttachmentID).Select(); err != nil { + if err == pg.ErrNoRows { + return db.ErrNoEntries{} + } + return err + } + return nil +} + +func (ps *postgresService) Blocked(account1 string, account2 string) (bool, error) { + // TODO: check domain blocks as well + var blocked bool + if err := ps.conn.Model(&gtsmodel.Block{}). + Where("account_id = ?", account1).Where("target_account_id = ?", account2). + WhereOr("target_account_id = ?", account1).Where("account_id = ?", account2). + Select(); err != nil { + if err == pg.ErrNoRows { + blocked = false + return blocked, nil + } + return blocked, err + } + blocked = true + return blocked, nil +} + +func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account, relevantAccounts *gtsmodel.RelevantAccounts) (bool, error) { + l := ps.log.WithField("func", "StatusVisible") + + // if target account is suspended then don't show the status + if !targetAccount.SuspendedAt.IsZero() { + l.Debug("target account suspended at is not zero") + return false, nil + } + + // if the target user doesn't exist (anymore) then the status also shouldn't be visible + targetUser := &gtsmodel.User{} + 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, db.ErrNoEntries{} + } + return false, err + } + + // if target user is disabled, not yet approved, or not confirmed then don't show the status + // (although in the latter two cases it's unlikely they posted a status yet anyway, but you never know!) + if targetUser.Disabled || !targetUser.Approved || targetUser.ConfirmedAt.IsZero() { + l.Debug("target user is disabled, not approved, or not confirmed") + return false, nil + } + + // If requesting account is nil, that means whoever requested the status didn't auth, or their auth failed. + // In this case, we can still serve the status if it's public, otherwise we definitely shouldn't. + if requestingAccount == nil { + + if targetStatus.Visibility == gtsmodel.VisibilityPublic { + return true, nil + } + l.Debug("requesting account is nil but the target status isn't public") + return false, nil + } + + // if requesting account is suspended then don't show the status -- although they probably shouldn't have gotten + // this far (ie., been authed) in the first place: this is just for safety. + if !requestingAccount.SuspendedAt.IsZero() { + l.Debug("requesting account is suspended") + return false, nil + } + + // check if we have a local account -- if so we can check the user for that account in the DB + if requestingAccount.Domain == "" { + requestingUser := &gtsmodel.User{} + if err := ps.conn.Model(requestingUser).Where("account_id = ?", requestingAccount.ID).Select(); err != nil { + // if the requesting account is local but doesn't have a corresponding user in the db this is a problem + if err == pg.ErrNoRows { + l.Debug("requesting account is local but there's no corresponding user") + return false, nil + } + l.Debugf("requesting account is local but there was an error getting the corresponding user: %s", err) + return false, err + } + // okay, user exists, so make sure it has full privileges/is confirmed/approved + if requestingUser.Disabled || !requestingUser.Approved || requestingUser.ConfirmedAt.IsZero() { + l.Debug("requesting account is local but corresponding user is either disabled, not approved, or not confirmed") + return false, nil + } + } + + // if the target status belongs to the requesting account, they should always be able to view it at this point + if targetStatus.AccountID == requestingAccount.ID { + return true, nil + } + + // At this point we have a populated targetAccount, targetStatus, and requestingAccount, so we can check for blocks and whathaveyou + // First check if a block exists directly between the target account (which authored the status) and the requesting account. + if blocked, err := ps.Blocked(targetAccount.ID, requestingAccount.ID); err != nil { + l.Debugf("something went wrong figuring out if the accounts have a block: %s", err) + return false, err + } else if blocked { + // don't allow the status to be viewed if a block exists in *either* direction between these two accounts, no creepy stalking please + l.Debug("a block exists between requesting account and target account") + return false, nil + } + + // check other accounts mentioned/boosted by/replied to by the status, if they exist + if relevantAccounts != nil { + // status replies to account id + if relevantAccounts.ReplyToAccount != nil { + if blocked, err := ps.Blocked(relevantAccounts.ReplyToAccount.ID, requestingAccount.ID); err != nil { + return false, err + } else if blocked { + return false, nil + } + } + + // status boosts accounts id + if relevantAccounts.BoostedAccount != nil { + if blocked, err := ps.Blocked(relevantAccounts.BoostedAccount.ID, requestingAccount.ID); err != nil { + return false, err + } else if blocked { + return false, nil + } + } + + // status boosts a reply to account id + if relevantAccounts.BoostedReplyToAccount != nil { + if blocked, err := ps.Blocked(relevantAccounts.BoostedReplyToAccount.ID, requestingAccount.ID); err != nil { + return false, err + } else if blocked { + return false, nil + } + } + + // status mentions accounts + for _, a := range relevantAccounts.MentionedAccounts { + if blocked, err := ps.Blocked(a.ID, requestingAccount.ID); err != nil { + return false, err + } else if blocked { + return false, nil + } + } + } + + // at this point we know neither account blocks the other, or another account mentioned or otherwise referred to in the status + // that means it's now just a matter of checking the visibility settings of the status itself + switch targetStatus.Visibility { + case gtsmodel.VisibilityPublic, gtsmodel.VisibilityUnlocked: + // no problem here, just return OK + return true, nil + case gtsmodel.VisibilityFollowersOnly: + // check one-way follow + follows, err := ps.Follows(requestingAccount, targetAccount) + if err != nil { + return false, err + } + if !follows { + return false, nil + } + return true, nil + case gtsmodel.VisibilityMutualsOnly: + // check mutual follow + mutuals, err := ps.Mutuals(requestingAccount, targetAccount) + if err != nil { + return false, err + } + if !mutuals { + return false, nil + } + return true, nil + case gtsmodel.VisibilityDirect: + // make sure the requesting account is mentioned in the status + for _, menchie := range targetStatus.Mentions { + if menchie == requestingAccount.ID { + return true, nil // yep it's mentioned! + } + } + return false, nil // it's not mentioned -_- + } + + return false, errors.New("reached the end of StatusVisible with no result") +} + +func (ps *postgresService) Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) { + return ps.conn.Model(&gtsmodel.Follow{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists() +} + +func (ps *postgresService) Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error) { + // make sure account 1 follows account 2 + f1, err := ps.conn.Model(&gtsmodel.Follow{}).Where("account_id = ?", account1.ID).Where("target_account_id = ?", account2.ID).Exists() + if err != nil { + if err == pg.ErrNoRows { + return false, nil + } + return false, err + } + + // make sure account 2 follows account 1 + f2, err := ps.conn.Model(&gtsmodel.Follow{}).Where("account_id = ?", account2.ID).Where("target_account_id = ?", account1.ID).Exists() + if err != nil { + if err == pg.ErrNoRows { + return false, nil + } + return false, err + } + + return f1 && f2, nil +} + +func (ps *postgresService) PullRelevantAccountsFromStatus(targetStatus *gtsmodel.Status) (*gtsmodel.RelevantAccounts, error) { + accounts := &gtsmodel.RelevantAccounts{ + MentionedAccounts: []*gtsmodel.Account{}, + } + + // get the replied to account from the status and add it to the pile + if targetStatus.InReplyToAccountID != "" { + repliedToAccount := &gtsmodel.Account{} + if err := ps.conn.Model(repliedToAccount).Where("id = ?", targetStatus.InReplyToAccountID).Select(); err != nil { + return accounts, err + } + accounts.ReplyToAccount = repliedToAccount + } + + // get the boosted account from the status and add it to the pile + if targetStatus.BoostOfID != "" { + // retrieve the boosted status first + boostedStatus := &gtsmodel.Status{} + if err := ps.conn.Model(boostedStatus).Where("id = ?", targetStatus.BoostOfID).Select(); err != nil { + return accounts, err + } + boostedAccount := &gtsmodel.Account{} + if err := ps.conn.Model(boostedAccount).Where("id = ?", boostedStatus.AccountID).Select(); err != nil { + return accounts, err + } + accounts.BoostedAccount = boostedAccount + + // the boosted status might be a reply to another account so we should get that too + if boostedStatus.InReplyToAccountID != "" { + boostedStatusRepliedToAccount := &gtsmodel.Account{} + if err := ps.conn.Model(boostedStatusRepliedToAccount).Where("id = ?", boostedStatus.InReplyToAccountID).Select(); err != nil { + return accounts, err + } + accounts.BoostedReplyToAccount = boostedStatusRepliedToAccount + } + } + + // now get all accounts with IDs that are mentioned in the status + for _, mentionedAccountID := range targetStatus.Mentions { + mentionedAccount := &gtsmodel.Account{} + if err := ps.conn.Model(mentionedAccount).Where("id = ?", mentionedAccountID).Select(); err != nil { + return accounts, err + } + accounts.MentionedAccounts = append(accounts.MentionedAccounts, mentionedAccount) + } + + return accounts, nil +} + +func (ps *postgresService) GetReplyCountForStatus(status *gtsmodel.Status) (int, error) { + return ps.conn.Model(&gtsmodel.Status{}).Where("in_reply_to_id = ?", status.ID).Count() +} + +func (ps *postgresService) GetReblogCountForStatus(status *gtsmodel.Status) (int, error) { + return ps.conn.Model(&gtsmodel.Status{}).Where("boost_of_id = ?", status.ID).Count() +} + +func (ps *postgresService) GetFaveCountForStatus(status *gtsmodel.Status) (int, error) { + return ps.conn.Model(&gtsmodel.StatusFave{}).Where("status_id = ?", status.ID).Count() +} + +func (ps *postgresService) StatusFavedBy(status *gtsmodel.Status, accountID string) (bool, error) { + return ps.conn.Model(&gtsmodel.StatusFave{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() +} + +func (ps *postgresService) StatusRebloggedBy(status *gtsmodel.Status, accountID string) (bool, error) { + return ps.conn.Model(&gtsmodel.Status{}).Where("boost_of_id = ?", status.ID).Where("account_id = ?", accountID).Exists() +} + +func (ps *postgresService) StatusMutedBy(status *gtsmodel.Status, accountID string) (bool, error) { + return ps.conn.Model(&gtsmodel.StatusMute{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() +} + +func (ps *postgresService) StatusBookmarkedBy(status *gtsmodel.Status, accountID string) (bool, error) { + return ps.conn.Model(&gtsmodel.StatusBookmark{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() +} + +func (ps *postgresService) StatusPinnedBy(status *gtsmodel.Status, accountID string) (bool, error) { + return ps.conn.Model(&gtsmodel.StatusPin{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() +} + +func (ps *postgresService) FaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error) { + // first check if a fave already exists, we can just return if so + existingFave := &gtsmodel.StatusFave{} + err := ps.conn.Model(existingFave).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Select() + if err == nil { + // fave already exists so just return nothing at all + return nil, nil + } + + // an error occurred so it might exist or not, we don't know + if err != pg.ErrNoRows { + return nil, err + } + + // it doesn't exist so create it + newFave := &gtsmodel.StatusFave{ + AccountID: accountID, + TargetAccountID: status.AccountID, + StatusID: status.ID, + } + if _, err = ps.conn.Model(newFave).Insert(); err != nil { + return nil, err + } + + return newFave, nil +} + +func (ps *postgresService) UnfaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error) { + // if a fave doesn't exist, we don't need to do anything + existingFave := &gtsmodel.StatusFave{} + err := ps.conn.Model(existingFave).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Select() + // the fave doesn't exist so return nothing at all + if err == pg.ErrNoRows { + return nil, nil + } + + // an error occurred so it might exist or not, we don't know + if err != nil && err != pg.ErrNoRows { + return nil, err + } + + // the fave exists so remove it + if _, err = ps.conn.Model(&gtsmodel.StatusFave{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Delete(); err != nil { + return nil, err + } + + return existingFave, nil +} + +func (ps *postgresService) WhoFavedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error) { + accounts := []*gtsmodel.Account{} + + faves := []*gtsmodel.StatusFave{} + if err := ps.conn.Model(&faves).Where("status_id = ?", status.ID).Select(); err != nil { + if err == pg.ErrNoRows { + return accounts, nil // no rows just means nobody has faved this status, so that's fine + } + return nil, err // an actual error has occurred + } + + for _, f := range faves { + acc := &gtsmodel.Account{} + if err := ps.conn.Model(acc).Where("id = ?", f.AccountID).Select(); err != nil { + if err == pg.ErrNoRows { + continue // the account doesn't exist for some reason??? but this isn't the place to worry about that so just skip it + } + return nil, err // an actual error has occurred + } + accounts = append(accounts, acc) + } + return accounts, nil +} + +/* + CONVERSION FUNCTIONS +*/ + +func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, originAccountID string, statusID string) ([]*gtsmodel.Mention, error) { + menchies := []*gtsmodel.Mention{} + for _, a := range targetAccounts { + // A mentioned account looks like "@test@example.org" or just "@test" for a local account + // -- we can guarantee this from the regex that targetAccounts should have been derived from. + // But we still need to do a bit of fiddling to get what we need here -- the username and domain (if given). + + // 1. trim off the first @ + t := strings.TrimPrefix(a, "@") + + // 2. split the username and domain + s := strings.Split(t, "@") + + // 3. if it's length 1 it's a local account, length 2 means remote, anything else means something is wrong + var local bool + switch len(s) { + case 1: + local = true + case 2: + local = false + default: + return nil, fmt.Errorf("mentioned account format '%s' was not valid", a) + } + + var username, domain string + username = s[0] + if !local { + domain = s[1] + } + + // 4. check we now have a proper username and domain + if username == "" || (!local && domain == "") { + return nil, fmt.Errorf("username or domain for '%s' was nil", a) + } + + // okay we're good now, we can start pulling accounts out of the database + mentionedAccount := &gtsmodel.Account{} + var err error + if local { + // local user -- should have a null domain + err = ps.conn.Model(mentionedAccount).Where("username = ?", username).Where("? IS NULL", pg.Ident("domain")).Select() + } else { + // remote user -- should have domain defined + err = ps.conn.Model(mentionedAccount).Where("username = ?", username).Where("? = ?", pg.Ident("domain"), domain).Select() + } + + if err != nil { + if err == pg.ErrNoRows { + // no result found for this username/domain so just don't include it as a mencho and carry on about our business + ps.log.Debugf("no account found with username '%s' and domain '%s', skipping it", username, domain) + continue + } + // a serious error has happened so bail + return nil, fmt.Errorf("error getting account with username '%s' and domain '%s': %s", username, domain, err) + } + + // id, createdAt and updatedAt will be populated by the db, so we have everything we need! + menchies = append(menchies, &gtsmodel.Mention{ + StatusID: statusID, + OriginAccountID: originAccountID, + TargetAccountID: mentionedAccount.ID, + }) + } + return menchies, nil +} + +func (ps *postgresService) TagStringsToTags(tags []string, originAccountID string, statusID string) ([]*gtsmodel.Tag, error) { + newTags := []*gtsmodel.Tag{} + for _, t := range tags { + tag := &gtsmodel.Tag{} + // we can use selectorinsert here to create the new tag if it doesn't exist already + // inserted will be true if this is a new tag we just created + if err := ps.conn.Model(tag).Where("name = ?", t).Select(); err != nil { + if err == pg.ErrNoRows { + // tag doesn't exist yet so populate it + tag.ID = uuid.NewString() + tag.Name = t + tag.FirstSeenFromAccountID = originAccountID + tag.CreatedAt = time.Now() + tag.UpdatedAt = time.Now() + tag.Useable = true + tag.Listable = true + } else { + return nil, fmt.Errorf("error getting tag with name %s: %s", t, err) + } + } + + // bail already if the tag isn't useable + if !tag.Useable { + continue + } + tag.LastStatusAt = time.Now() + newTags = append(newTags, tag) + } + return newTags, nil +} + +func (ps *postgresService) EmojiStringsToEmojis(emojis []string, originAccountID string, statusID string) ([]*gtsmodel.Emoji, error) { + newEmojis := []*gtsmodel.Emoji{} + for _, e := range emojis { + emoji := &gtsmodel.Emoji{} + err := ps.conn.Model(emoji).Where("shortcode = ?", e).Where("visible_in_picker = true").Where("disabled = false").Select() + if err != nil { + if err == pg.ErrNoRows { + // no result found for this username/domain so just don't include it as an emoji and carry on about our business + ps.log.Debugf("no emoji found with shortcode %s, skipping it", e) + continue + } + // a serious error has happened so bail + return nil, fmt.Errorf("error getting emoji with shortcode %s: %s", e, err) + } + newEmojis = append(newEmojis, emoji) + } + return newEmojis, nil +} diff --git a/internal/db/pg_test.go b/internal/db/pg_test.go @@ -1,21 +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 <http://www.gnu.org/licenses/>. -*/ - -package db_test - -// TODO: write tests for postgres diff --git 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 @@ -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/federation/federating_db.go b/internal/federation/federating_db.go @@ -0,0 +1,599 @@ +/* + 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 <http://www.gnu.org/licenses/>. +*/ + +package federation + +import ( + "context" + "errors" + "fmt" + "net/url" + "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.DB + config *config.Config + log *logrus.Logger + typeConverter typeutils.TypeConverter +} + +// 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, + typeConverter: typeutils.NewConverter(config, db), + } +} + +/* + GO-FED DB INTERFACE-IMPLEMENTING FUNCTIONS +*/ + +// Lock takes a lock for the object at the specified id. If an error +// is returned, the lock must not have been taken. +// +// The lock must be able to succeed for an id that does not exist in +// the database. This means acquiring the lock does not guarantee the +// entry exists in the database. +// +// Locks are encouraged to be lightweight and in the Go layer, as some +// processes require tight loops acquiring and releasing locks. +// +// Used to ensure race conditions in multiple requests do not occur. +func (f *federatingDB) Lock(c context.Context, id *url.URL) error { + // Before any other Database methods are called, the relevant `id` + // entries are locked to allow for fine-grained concurrency. + + // Strategy: create a new lock, if stored, continue. Otherwise, lock the + // existing mutex. + mu := &sync.Mutex{} + mu.Lock() // Optimistically lock if we do store it. + i, loaded := f.locks.LoadOrStore(id.String(), mu) + if loaded { + mu = i.(*sync.Mutex) + mu.Lock() + } + return nil +} + +// Unlock makes the lock for the object at the specified id available. +// If an error is returned, the lock must have still been freed. +// +// Used to ensure race conditions in multiple requests do not occur. +func (f *federatingDB) Unlock(c context.Context, id *url.URL) error { + // Once Go-Fed is done calling Database methods, the relevant `id` + // entries are unlocked. + + i, ok := f.locks.Load(id.String()) + if !ok { + return errors.New("missing an id in unlock") + } + mu := i.(*sync.Mutex) + mu.Unlock() + return nil +} + +// InboxContains returns true if the OrderedCollection at 'inbox' +// contains the specified 'id'. +// +// 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()) + } + + activityI := c.Value(util.APActivity) + if activityI == nil { + return false, fmt.Errorf("no activity was set for id %s", id.String()) + } + activity, ok := activityI.(pub.Activity) + if !ok || activity == nil { + return false, fmt.Errorf("could not parse contextual activity for id %s", id.String()) + } + + l.Debugf("activity type %s for id %s", activity.GetTypeName(), id.String()) + + return false, nil + + // if err := f.db.GetByID(statusID, &gtsmodel.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 +// the specified IRI, for prepending new items. +// +// 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) { + 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 +// prepended. Note that the new items must not be added as independent +// database entries. Separate calls to Create will do that. +// +// 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 +} + +// Owns returns true if the IRI belongs to this instance, and if +// 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 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) { + _, uid, err := util.ParseStatusesPath(id) + if err != nil { + return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err) + } + if err := f.db.GetWhere("uri", uid, &gtsmodel.Status{}); err != nil { + 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 + } + + // check if it's a user, eg /users/example_username + if util.IsUserPath(id) { + username, err := util.ParseUserPath(id) + if err != nil { + return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err) + } + if err := f.db.GetLocalAccountByUsername(username, &gtsmodel.Account{}); err != nil { + 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 + } + + return false, fmt.Errorf("could not match activityID: %s", id.String()) +} + +// ActorForOutbox fetches the actor's IRI for the given outbox IRI. +// +// 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 := &gtsmodel.Account{} + if err := f.db.GetWhere("outbox_uri", outboxIRI.String(), acct); err != nil { + 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()) + } + return url.Parse(acct.URI) +} + +// ActorForInbox fetches the actor's IRI for the given outbox IRI. +// +// 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 := &gtsmodel.Account{} + if err := f.db.GetWhere("inbox_uri", inboxIRI.String(), acct); err != nil { + 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()) + } + return url.Parse(acct.URI) +} + +// OutboxForInbox fetches the corresponding actor's outbox IRI for the +// actor's inbox IRI. +// +// 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 := &gtsmodel.Account{} + if err := f.db.GetWhere("inbox_uri", inboxIRI.String(), acct); err != nil { + 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()) + } + return url.Parse(acct.OutboxURI) +} + +// Exists returns true if the database has an entry for the specified +// id. It may not be owned by this application instance. +// +// 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 +} + +// Get returns the database entry for the specified id. +// +// 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 := &gtsmodel.Account{} + if err := f.db.GetWhere("uri", id.String(), acct); err != nil { + return nil, err + } + return f.typeConverter.AccountToAS(acct) + } + + return nil, nil +} + +// Create adds a new entry to the database which must be able to be +// keyed by its id. +// +// Note that Activity values received from federated peers may also be +// created in the database this way if the Federating Protocol is +// enabled. The client may freely decide to store only the id instead of +// the entire value. +// +// The library makes this call only after acquiring a lock first. +// +// 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 +} + +// Update sets an existing entry to the database based on the value's +// id. +// +// Note that Activity values received from federated peers may also be +// updated in the database this way if the Federating Protocol is +// enabled. The client may freely decide to store only the id instead of +// the entire value. +// +// 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 +} + +// Delete removes the entry with the given id. +// +// Delete is only called for federated objects. Deletes from the Social +// Protocol instead call Update to create a Tombstone. +// +// 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 +} + +// GetOutbox returns the first ordered collection page of the outbox +// at the specified IRI, for prepending new items. +// +// 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 +} + +// SetOutbox saves the outbox value given from GetOutbox, with new items +// prepended. Note that the new items must not be added as independent +// database entries. Separate calls to Create will do that. +// +// 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 +} + +// NewID creates a new IRI id for the provided activity or object. The +// implementation does not need to set the 'id' property and simply +// needs to determine the value. +// +// 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) { + 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 +// given id. +// +// If modified, the library will then call Update. +// +// 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) { + l := f.log.WithFields( + logrus.Fields{ + "func": "Followers", + "actorIRI": actorIRI.String(), + }, + ) + l.Debugf("entering FOLLOWERS function with actorIRI %s", actorIRI.String()) + + acct := &gtsmodel.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 := &gtsmodel.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 +// given id. +// +// 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) (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 := &gtsmodel.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 := &gtsmodel.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 +// given id. +// +// 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) (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/federation/federating_db_test.go b/internal/federation/federating_db_test.go @@ -0,0 +1,21 @@ +/* + 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 <http://www.gnu.org/licenses/>. +*/ + +package federation + +// TODO: write tests for pgfed diff --git 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 @@ -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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 := &gtsmodel.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 @@ -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 := &gtsmodel.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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 <http://www.gnu.org/licenses/>. -*/ - -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 @@ -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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 @@ -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 <http://www.gnu.org/licenses/>. +*/ + +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 @@ -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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 @@ -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": "<p><span class=\"h-card\"><a href=\"https://social.pixie.town/@f0x\" class=\"u-url mention\">@<span>f0x</span></a></span> nice there it is:</p><p><a href=\"https://social.pixie.town/users/f0x/statuses/106221628567855262/activity\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">social.pixie.town/users/f0x/st</span><span class=\"invisible\">atuses/106221628567855262/activity</span></a></p>", + "contentMap": { + "en": "<p><span class=\"h-card\"><a href=\"https://social.pixie.town/@f0x\" class=\"u-url mention\">@<span>f0x</span></a></span> nice there it is:</p><p><a href=\"https://social.pixie.town/users/f0x/statuses/106221628567855262/activity\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">social.pixie.town/users/f0x/st</span><span class=\"invisible\">atuses/106221628567855262/activity</span></a></p>" + }, + "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": "<p>just testing activitypub representations of <a href=\"https://ondergrond.org/tags/tags\" class=\"mention hashtag\" rel=\"tag\">#<span>tags</span></a> and <a href=\"https://ondergrond.org/tags/emoji\" class=\"mention hashtag\" rel=\"tag\">#<span>emoji</span></a> :party_parrot: :amaze: :blobsunglasses: </p><p>don&apos;t mind me....</p>", + "contentMap": { + "en": "<p>just testing activitypub representations of <a href=\"https://ondergrond.org/tags/tags\" class=\"mention hashtag\" rel=\"tag\">#<span>tags</span></a> and <a href=\"https://ondergrond.org/tags/emoji\" class=\"mention hashtag\" rel=\"tag\">#<span>emoji</span></a> :party_parrot: :amaze: :blobsunglasses: </p><p>don&apos;t mind me....</p>" + }, + "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 @@ -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 @@ -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 := &gtsmodel.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 := &gtsmodel.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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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{