commit 3d77f81c7fed002c628db82d822cc46c56a57e64
parent c4d791be75a9b3ebd93ca3675525163b326350f2
Author: Tobi Smethurst <31960611+tsmethurst@users.noreply.github.com>
Date: Sun, 30 May 2021 13:12:00 +0200
Move a lot of stuff + tidy stuff (#37)
Lots of renaming and moving stuff, some bug fixes, more lenient parsing of notifications and home timeline.
Diffstat:
92 files changed, 4777 insertions(+), 5154 deletions(-)
diff --git a/cmd/gotosocial/main.go b/cmd/gotosocial/main.go
@@ -23,11 +23,10 @@ import (
"os"
"github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/action"
- "github.com/superseriousbusiness/gotosocial/internal/clitools/admin/account"
+ "github.com/superseriousbusiness/gotosocial/internal/cliactions"
+ "github.com/superseriousbusiness/gotosocial/internal/cliactions/admin/account"
+ "github.com/superseriousbusiness/gotosocial/internal/cliactions/server"
"github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gotosocial"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/testrig"
@@ -259,7 +258,7 @@ func main() {
Name: "start",
Usage: "start the gotosocial server",
Action: func(c *cli.Context) error {
- return runAction(c, gotosocial.Run)
+ return runAction(c, server.Start)
},
},
},
@@ -362,19 +361,19 @@ func main() {
},
},
},
- {
- Name: "db",
- Usage: "database-related tasks and utils",
- Subcommands: []*cli.Command{
- {
- Name: "init",
- Usage: "initialize a database with the required schema for gotosocial; has no effect & is safe to run on an already-initialized db",
- Action: func(c *cli.Context) error {
- return runAction(c, db.Initialize)
- },
- },
- },
- },
+ // {
+ // Name: "db",
+ // Usage: "database-related tasks and utils",
+ // Subcommands: []*cli.Command{
+ // {
+ // Name: "init",
+ // Usage: "initialize a database with the required schema for gotosocial; has no effect & is safe to run on an already-initialized db",
+ // Action: func(c *cli.Context) error {
+ // return runAction(c, db.Initialize)
+ // },
+ // },
+ // },
+ // },
{
Name: "testrig",
Usage: "gotosocial testrig tasks",
@@ -399,7 +398,7 @@ func main() {
// runAction builds up the config and logger necessary for any
// gotosocial action, and then executes the action.
-func runAction(c *cli.Context, a action.GTSAction) error {
+func runAction(c *cli.Context, a cliactions.GTSAction) error {
// create a new *config.Config based on the config path provided...
conf, err := config.FromFile(c.String(config.GetFlagNames().ConfigPath))
diff --git a/go.mod b/go.mod
@@ -5,23 +5,23 @@ 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-exif v0.0.0-20210512055020-8213cfabc61b // indirect
+ github.com/dsoprea/go-exif/v2 v2.0.0-20210512055020-8213cfabc61b // 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-jpeg-image-structure v0.0.0-20210512043942-b434301c6836 // 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-png-image-structure v0.0.0-20210512210324-29b889a6093d // 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.7.1
- github.com/go-errors/errors v1.2.0 // indirect
+ github.com/gin-gonic/gin v1.7.2
+ github.com/go-errors/errors v1.4.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.9.1
- github.com/go-playground/validator/v10 v10.6.0 // indirect
+ github.com/go-pg/pg/v10 v10.9.3
+ github.com/go-playground/validator/v10 v10.6.1 // 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
@@ -29,27 +29,26 @@ require (
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/mattn/go-isatty v0.0.13 // 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/gomega v1.12.0 // indirect
+ github.com/onsi/gomega v1.13.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/superseriousbusiness/oauth2/v4 v4.3.0-SSB
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/ugorji/go v1.2.6 // indirect
github.com/urfave/cli/v2 v2.3.0
- github.com/vmihailenco/msgpack/v5 v5.3.1 // indirect
+ github.com/vmihailenco/msgpack/v5 v5.3.4 // indirect
github.com/wagslane/go-password-validator v0.3.0
- 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/crypto v0.0.0-20210513164829-c07d793c2f9a
+ golang.org/x/net v0.0.0-20210525063256-abc453219eb5 // indirect
+ golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea // indirect
golang.org/x/text v0.3.6
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v2 v2.4.0
diff --git a/go.sum b/go.sum
@@ -21,18 +21,20 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
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/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 v0.0.0-20210512055020-8213cfabc61b h1:NSYszMk5S88hDGF0benZ9PolrCffN7Ojx0zFdWgStB4=
+github.com/dsoprea/go-exif v0.0.0-20210512055020-8213cfabc61b/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/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0=
-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-exif/v2 v2.0.0-20210512055020-8213cfabc61b h1:C0NJXglXQIT4SC7AItpV+RU36X98c46kZTVmDAVaBR8=
+github.com/dsoprea/go-exif/v2 v2.0.0-20210512055020-8213cfabc61b/go.mod h1:oKrjk2kb3rAR5NbtSTLUMvMSbc+k8ZosI3MaVH47noc=
+github.com/dsoprea/go-exif/v3 v3.0.0-20200717053412-08f1b6708903/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8=
+github.com/dsoprea/go-exif/v3 v3.0.0-20210512043655-120bcdb2a55e/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk=
github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM=
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-jpeg-image-structure v0.0.0-20210512043942-b434301c6836 h1:OHRfKIVRz2XrhZ6A7fJKHLoKky1giN+VUgU2npF0BvE=
+github.com/dsoprea/go-jpeg-image-structure v0.0.0-20210512043942-b434301c6836/go.mod h1:6+tQXZ+I62x13UZ+hemLVoZIuq/usVzvau7bqwUo9P0=
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/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd h1:l+vLbuxptsC6VQyQsfD7NnEC8BZuFpz45PgY+pH8YTg=
@@ -41,12 +43,13 @@ github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c/g
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-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-png-image-structure v0.0.0-20210512210324-29b889a6093d h1:8+qI8ant/vZkNSsbwSjIR6XJfWcDVTg/qx/3pRUUZNA=
+github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d/go.mod h1:yTR3tKgyk20phAFg6IE9ulMA5NjEDD2wyx+okRFLVtw=
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/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e/go.mod h1:uAzdkPTub5Y9yQwXe8W4m2XuP0tK4a9Q/dantD0+uaU=
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=
@@ -64,13 +67,14 @@ github.com/gin-contrib/sessions v0.0.3/go.mod h1:8C/J6cad3Il1mWYYgtw0w+hqasmpvy2
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.7.1 h1:qC89GU3p8TvKWMAVhEpmpB2CIb1hnqt2UdKZaP93mS8=
-github.com/gin-gonic/gin v1.7.1/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
+github.com/gin-gonic/gin v1.7.2 h1:Tg03T9yM2xa8j6I3Z3oqLaQRSmKvxPd6g/2HJ6zICFA=
+github.com/gin-gonic/gin v1.7.2/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/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
-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-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
+github.com/go-errors/errors v1.4.0 h1:2OA7MFw38+e9na72T1xgkomPb6GzZzzxvJ5U630FoRM=
+github.com/go-errors/errors v1.4.0/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
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=
@@ -79,8 +83,8 @@ github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7
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.9.1 h1:kU4t84zWGGaU0Qsu49FbNtToUVrlSTkNOngW8aQmwvk=
-github.com/go-pg/pg/v10 v10.9.1/go.mod h1:rgmTPgHgl5EN2CNKKoMwC7QT62t8BqsdpEkUQuiZMQs=
+github.com/go-pg/pg/v10 v10.9.3 h1:xq2IT7DH/E6k8URkMrVY8iMF6gw5c+0fQglkdZJ9q0g=
+github.com/go-pg/pg/v10 v10.9.3/go.mod h1:oBFhvl5LgiEdTaZRjBfrq9fp5fUSmKZnh1pPa6JOHBQ=
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=
@@ -92,8 +96,8 @@ github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEK
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.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-playground/validator/v10 v10.6.1 h1:W6TRDXt4WcWp4c4nf/G+6BkGdhiIo0k417gfr+V6u4I=
+github.com/go-playground/validator/v10 v10.6.1/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=
@@ -182,8 +186,9 @@ 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=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
+github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
@@ -208,8 +213,8 @@ github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvw
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.12.0 h1:p4oGGk2M2UJc0wWN4lHFvIB71lxsh0T/UiKCCgFADY8=
-github.com/onsi/gomega v1.12.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=
+github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak=
+github.com/onsi/gomega v1.13.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=
@@ -230,8 +235,6 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
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/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=
@@ -241,8 +244,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 h1:1SWXcTphBQjYGWRRxLFIAR1LVtQEj4eR7xPtyeOVM/c=
github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203/go.mod h1:0Xw5cYMOYpgaWs+OOSx41ugycl2qvKTi9tlMMcZhFyY=
-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/superseriousbusiness/oauth2/v4 v4.3.0-SSB h1:dzMVC+oPTxFL5cv29egBrftlqIWPXQ6/VzkuoySwgm4=
+github.com/superseriousbusiness/oauth2/v4 v4.3.0-SSB/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.4.2/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8=
github.com/tidwall/btree v0.5.0 h1:IBfCtOj4uOMQcodv3wzYVo0zPqSJObm71mE039/dlXY=
@@ -274,11 +277,11 @@ github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q09
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/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
-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 v1.2.6 h1:tGiWC9HENWE2tqYycIqFTNorMmFRVhNwCpDOpWqnk8E=
+github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0=
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/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ=
+github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw=
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=
@@ -290,9 +293,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.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/msgpack/v5 v5.3.4 h1:qMKAwOV+meBw2Y8k9cVwAy7qErtYCwBzZ2ellBfvnqc=
+github.com/vmihailenco/msgpack/v5 v5.3.4/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=
@@ -315,16 +318,12 @@ 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.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=
@@ -333,9 +332,9 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
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-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/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
+golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/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=
@@ -363,8 +362,8 @@ golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-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/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo=
+golang.org/x/net v0.0.0-20210525063256-abc453219eb5/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=
@@ -389,10 +388,10 @@ 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-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/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea h1:+WiDlPBBaO+h9vPNZi8uJ3k4BkKQB7Iow3aqwHVA5hI=
+golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/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=
diff --git a/internal/action/action.go b/internal/action/action.go
@@ -1,31 +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 action
-
-import (
- "context"
-
- "github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/config"
-)
-
-// GTSAction defines one *action* that can be taken by the gotosocial cli command.
-// This can be either a long-running action (like server start) or something
-// shorter like db init or db inspect.
-type GTSAction func(context.Context, *config.Config, *logrus.Logger) error
diff --git a/internal/action/mock_GTSAction.go b/internal/action/mock_GTSAction.go
@@ -1,32 +0,0 @@
-// Code generated by mockery v2.7.4. DO NOT EDIT.
-
-package action
-
-import (
- context "context"
-
- config "github.com/superseriousbusiness/gotosocial/internal/config"
-
- logrus "github.com/sirupsen/logrus"
-
- mock "github.com/stretchr/testify/mock"
-)
-
-// MockGTSAction is an autogenerated mock type for the GTSAction type
-type MockGTSAction struct {
- mock.Mock
-}
-
-// Execute provides a mock function with given fields: _a0, _a1, _a2
-func (_m *MockGTSAction) Execute(_a0 context.Context, _a1 *config.Config, _a2 *logrus.Logger) error {
- ret := _m.Called(_a0, _a1, _a2)
-
- var r0 error
- if rf, ok := ret.Get(0).(func(context.Context, *config.Config, *logrus.Logger) error); ok {
- r0 = rf(_a0, _a1, _a2)
- } else {
- r0 = ret.Error(0)
- }
-
- return r0
-}
diff --git a/internal/api/client/account/account.go b/internal/api/client/account/account.go
@@ -26,7 +26,7 @@ import (
"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/processing"
"github.com/superseriousbusiness/gotosocial/internal/router"
)
@@ -61,7 +61,7 @@ const (
GetFollowingPath = BasePathWithID + "/following"
// GetRelationshipsPath is for showing an account's relationship with other accounts
GetRelationshipsPath = BasePath + "/relationships"
- // FollowPath is for POSTing new follows to, and updating existing follows
+ // PostFollowPath is for POSTing new follows to, and updating existing follows
PostFollowPath = BasePathWithID + "/follow"
// PostUnfollowPath is for POSTing an unfollow
PostUnfollowPath = BasePathWithID + "/unfollow"
@@ -70,12 +70,12 @@ const (
// Module implements the ClientAPIModule interface for account-related actions
type Module struct {
config *config.Config
- processor message.Processor
+ processor processing.Processor
log *logrus.Logger
}
// New returns a new account module
-func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.ClientModule {
+func New(config *config.Config, processor processing.Processor, log *logrus.Logger) api.ClientModule {
return &Module{
config: config,
processor: processor,
diff --git a/internal/api/client/account/account_test.go b/internal/api/client/account/account_test.go
@@ -4,13 +4,13 @@ import (
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/api/client/account"
+ "github.com/superseriousbusiness/gotosocial/internal/blob"
"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/message"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
+ "github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
)
@@ -22,9 +22,9 @@ type AccountStandardTestSuite struct {
db db.DB
log *logrus.Logger
tc typeutils.TypeConverter
- storage storage.Storage
+ storage blob.Storage
federator federation.Federator
- processor message.Processor
+ processor processing.Processor
// standard suite models
testTokens map[string]*oauth.Token
diff --git a/internal/api/client/account/following.go b/internal/api/client/account/following.go
@@ -25,7 +25,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
-// AccountFollowersGETHandler serves the followers of the requested account, if they're visible to the requester.
+// AccountFollowingGETHandler serves the following of the requested account, if they're visible to the requester.
func (m *Module) AccountFollowingGETHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
diff --git a/internal/api/client/admin/admin.go b/internal/api/client/admin/admin.go
@@ -24,7 +24,7 @@ import (
"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/processing"
"github.com/superseriousbusiness/gotosocial/internal/router"
)
@@ -38,12 +38,12 @@ const (
// Module implements the ClientAPIModule interface for admin-related actions (reports, emojis, etc)
type Module struct {
config *config.Config
- processor message.Processor
+ processor processing.Processor
log *logrus.Logger
}
// New returns a new admin module
-func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.ClientModule {
+func New(config *config.Config, processor processing.Processor, log *logrus.Logger) api.ClientModule {
return &Module{
config: config,
processor: processor,
diff --git a/internal/api/client/app/app.go b/internal/api/client/app/app.go
@@ -24,7 +24,7 @@ import (
"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/processing"
"github.com/superseriousbusiness/gotosocial/internal/router"
)
@@ -34,12 +34,12 @@ const BasePath = "/api/v1/apps"
// Module implements the ClientAPIModule interface for requests relating to registering/removing applications
type Module struct {
config *config.Config
- processor message.Processor
+ processor processing.Processor
log *logrus.Logger
}
// New returns a new auth module
-func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.ClientModule {
+func New(config *config.Config, processor processing.Processor, log *logrus.Logger) api.ClientModule {
return &Module{
config: config,
processor: processor,
diff --git a/internal/api/client/fileserver/fileserver.go b/internal/api/client/fileserver/fileserver.go
@@ -27,7 +27,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/message"
+ "github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/router"
)
@@ -46,13 +46,13 @@ const (
// The goal here is to serve requested media files if the gotosocial server is configured to use local storage.
type FileServer struct {
config *config.Config
- processor message.Processor
+ processor processing.Processor
log *logrus.Logger
storageBase string
}
// New returns a new fileServer module
-func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.ClientModule {
+func New(config *config.Config, processor processing.Processor, log *logrus.Logger) api.ClientModule {
return &FileServer{
config: config,
processor: processor,
diff --git a/internal/api/client/fileserver/servefile_test.go b/internal/api/client/fileserver/servefile_test.go
@@ -31,14 +31,14 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver"
+ "github.com/superseriousbusiness/gotosocial/internal/blob"
"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/media"
- "github.com/superseriousbusiness/gotosocial/internal/message"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
+ "github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -49,10 +49,10 @@ type ServeFileTestSuite struct {
config *config.Config
db db.DB
log *logrus.Logger
- storage storage.Storage
+ storage blob.Storage
federator federation.Federator
tc typeutils.TypeConverter
- processor message.Processor
+ processor processing.Processor
mediaHandler media.Handler
oauthServer oauth.Server
diff --git a/internal/api/client/followrequest/followrequest.go b/internal/api/client/followrequest/followrequest.go
@@ -24,7 +24,7 @@ import (
"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/processing"
"github.com/superseriousbusiness/gotosocial/internal/router"
)
@@ -46,12 +46,12 @@ const (
// Module implements the ClientAPIModule interface for every related to interacting with follow requests
type Module struct {
config *config.Config
- processor message.Processor
+ processor processing.Processor
log *logrus.Logger
}
// New returns a new follow request module
-func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.ClientModule {
+func New(config *config.Config, processor processing.Processor, log *logrus.Logger) api.ClientModule {
return &Module{
config: config,
processor: processor,
diff --git a/internal/api/client/instance/instance.go b/internal/api/client/instance/instance.go
@@ -6,7 +6,7 @@ import (
"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/processing"
"github.com/superseriousbusiness/gotosocial/internal/router"
)
@@ -18,12 +18,12 @@ const (
// Module implements the ClientModule interface
type Module struct {
config *config.Config
- processor message.Processor
+ processor processing.Processor
log *logrus.Logger
}
// New returns a new instance information module
-func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.ClientModule {
+func New(config *config.Config, processor processing.Processor, log *logrus.Logger) api.ClientModule {
return &Module{
config: config,
processor: processor,
diff --git a/internal/api/client/media/media.go b/internal/api/client/media/media.go
@@ -27,7 +27,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/message"
+ "github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/router"
)
@@ -43,12 +43,12 @@ const BasePathWithID = BasePath + "/:" + IDKey
// Module implements the ClientAPIModule interface for media
type Module struct {
config *config.Config
- processor message.Processor
+ processor processing.Processor
log *logrus.Logger
}
// New returns a new auth module
-func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.ClientModule {
+func New(config *config.Config, processor processing.Processor, log *logrus.Logger) api.ClientModule {
return &Module{
config: config,
processor: processor,
diff --git a/internal/api/client/media/mediacreate_test.go b/internal/api/client/media/mediacreate_test.go
@@ -34,14 +34,14 @@ import (
"github.com/stretchr/testify/suite"
mediamodule "github.com/superseriousbusiness/gotosocial/internal/api/client/media"
"github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/blob"
"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/media"
- "github.com/superseriousbusiness/gotosocial/internal/message"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
+ "github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -52,12 +52,12 @@ type MediaCreateTestSuite struct {
config *config.Config
db db.DB
log *logrus.Logger
- storage storage.Storage
+ storage blob.Storage
federator federation.Federator
tc typeutils.TypeConverter
mediaHandler media.Handler
oauthServer oauth.Server
- processor message.Processor
+ processor processing.Processor
// standard suite models
testTokens map[string]*oauth.Token
diff --git a/internal/api/client/notification/notification.go b/internal/api/client/notification/notification.go
@@ -24,7 +24,7 @@ import (
"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/processing"
"github.com/superseriousbusiness/gotosocial/internal/router"
)
@@ -39,19 +39,19 @@ const (
// MaxIDKey is the url query for setting a max notification ID to return
MaxIDKey = "max_id"
- // Limit key is for specifying maximum number of notifications to return.
+ // LimitKey is for specifying maximum number of notifications to return.
LimitKey = "limit"
)
// Module implements the ClientAPIModule interface for every related to posting/deleting/interacting with notifications
type Module struct {
config *config.Config
- processor message.Processor
+ processor processing.Processor
log *logrus.Logger
}
// New returns a new notification module
-func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.ClientModule {
+func New(config *config.Config, processor processing.Processor, log *logrus.Logger) api.ClientModule {
return &Module{
config: config,
processor: processor,
diff --git a/internal/api/client/notification/notificationsget.go b/internal/api/client/notification/notificationsget.go
@@ -27,6 +27,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
+// NotificationsGETHandler serves a list of notifications to the caller, with the desired query parameters
func (m *Module) NotificationsGETHandler(c *gin.Context) {
l := m.log.WithFields(logrus.Fields{
"func": "NotificationsGETHandler",
diff --git a/internal/api/client/search/search.go b/internal/api/client/search/search.go
@@ -24,12 +24,12 @@ import (
"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/processing"
"github.com/superseriousbusiness/gotosocial/internal/router"
)
const (
- // BasePath is the base path for serving v1 of the search API
+ // BasePathV1 is the base path for serving v1 of the search API
BasePathV1 = "/api/v1/search"
// BasePathV2 is the base path for serving v2 of the search API
@@ -67,12 +67,12 @@ const (
// Module implements the ClientAPIModule interface for everything related to searching
type Module struct {
config *config.Config
- processor message.Processor
+ processor processing.Processor
log *logrus.Logger
}
// New returns a new search module
-func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.ClientModule {
+func New(config *config.Config, processor processing.Processor, log *logrus.Logger) api.ClientModule {
return &Module{
config: config,
processor: processor,
diff --git a/internal/api/client/status/status.go b/internal/api/client/status/status.go
@@ -26,7 +26,7 @@ import (
"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/processing"
"github.com/superseriousbusiness/gotosocial/internal/router"
)
@@ -75,12 +75,12 @@ const (
// Module implements the ClientAPIModule interface for every related to posting/deleting/interacting with statuses
type Module struct {
config *config.Config
- processor message.Processor
+ processor processing.Processor
log *logrus.Logger
}
// New returns a new account module
-func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.ClientModule {
+func New(config *config.Config, processor processing.Processor, log *logrus.Logger) api.ClientModule {
return &Module{
config: config,
processor: processor,
diff --git a/internal/api/client/status/status_test.go b/internal/api/client/status/status_test.go
@@ -22,13 +22,13 @@ import (
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/api/client/status"
+ "github.com/superseriousbusiness/gotosocial/internal/blob"
"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/message"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
+ "github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
)
@@ -41,8 +41,8 @@ type StatusStandardTestSuite struct {
log *logrus.Logger
tc typeutils.TypeConverter
federator federation.Federator
- processor message.Processor
- storage storage.Storage
+ processor processing.Processor
+ storage blob.Storage
// standard suite models
testTokens map[string]*oauth.Token
diff --git a/internal/api/client/timeline/timeline.go b/internal/api/client/timeline/timeline.go
@@ -24,7 +24,7 @@ import (
"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/processing"
"github.com/superseriousbusiness/gotosocial/internal/router"
)
@@ -39,7 +39,7 @@ const (
SinceIDKey = "since_id"
// MinIDKey is the url query for returning results immediately newer than the given ID
MinIDKey = "min_id"
- // Limit key is for specifying maximum number of results to return.
+ // LimitKey is for specifying maximum number of results to return.
LimitKey = "limit"
// LocalKey is for specifying whether only local statuses should be returned
LocalKey = "local"
@@ -48,12 +48,12 @@ const (
// Module implements the ClientAPIModule interface for everything relating to viewing timelines
type Module struct {
config *config.Config
- processor message.Processor
+ processor processing.Processor
log *logrus.Logger
}
// New returns a new timeline module
-func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.ClientModule {
+func New(config *config.Config, processor processing.Processor, log *logrus.Logger) api.ClientModule {
return &Module{
config: config,
processor: processor,
diff --git a/internal/api/s2s/user/followers.go b/internal/api/s2s/user/followers.go
@@ -25,6 +25,7 @@ import (
"github.com/sirupsen/logrus"
)
+// FollowersGETHandler returns a collection of URIs for followers of the target user, formatted so that other AP servers can understand it.
func (m *Module) FollowersGETHandler(c *gin.Context) {
l := m.log.WithFields(logrus.Fields{
"func": "FollowersGETHandler",
diff --git a/internal/api/s2s/user/following.go b/internal/api/s2s/user/following.go
@@ -25,6 +25,7 @@ import (
"github.com/sirupsen/logrus"
)
+// FollowingGETHandler returns a collection of URIs for accounts that the target user follows, formatted so that other AP servers can understand it.
func (m *Module) FollowingGETHandler(c *gin.Context) {
l := m.log.WithFields(logrus.Fields{
"func": "FollowingGETHandler",
diff --git a/internal/api/s2s/user/inboxpost.go b/internal/api/s2s/user/inboxpost.go
@@ -23,7 +23,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/message"
+ "github.com/superseriousbusiness/gotosocial/internal/processing"
)
// InboxPOSTHandler deals with incoming POST requests to an actor's inbox.
@@ -42,7 +42,7 @@ func (m *Module) InboxPOSTHandler(c *gin.Context) {
posted, err := m.processor.InboxPost(c.Request.Context(), c.Writer, c.Request)
if err != nil {
- if withCode, ok := err.(message.ErrorWithCode); ok {
+ if withCode, ok := err.(processing.ErrorWithCode); ok {
l.Debug(withCode.Error())
c.JSON(withCode.Code(), withCode.Safe())
return
diff --git a/internal/api/s2s/user/statusget.go b/internal/api/s2s/user/statusget.go
@@ -7,6 +7,7 @@ import (
"github.com/sirupsen/logrus"
)
+// StatusGETHandler serves the target status as an activitystreams NOTE so that other AP servers can parse it.
func (m *Module) StatusGETHandler(c *gin.Context) {
l := m.log.WithFields(logrus.Fields{
"func": "StatusGETHandler",
diff --git a/internal/api/s2s/user/user.go b/internal/api/s2s/user/user.go
@@ -24,7 +24,7 @@ import (
"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/processing"
"github.com/superseriousbusiness/gotosocial/internal/router"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
@@ -60,12 +60,12 @@ var ActivityPubAcceptHeaders = []string{
// Module implements the FederationAPIModule interface
type Module struct {
config *config.Config
- processor message.Processor
+ processor processing.Processor
log *logrus.Logger
}
// New returns a new auth module
-func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.FederationModule {
+func New(config *config.Config, processor processing.Processor, log *logrus.Logger) api.FederationModule {
return &Module{
config: config,
processor: processor,
diff --git a/internal/api/s2s/user/user_test.go b/internal/api/s2s/user/user_test.go
@@ -4,13 +4,13 @@ import (
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/api/s2s/user"
+ "github.com/superseriousbusiness/gotosocial/internal/blob"
"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/message"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
+ "github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
)
@@ -23,8 +23,8 @@ type UserStandardTestSuite struct {
log *logrus.Logger
tc typeutils.TypeConverter
federator federation.Federator
- processor message.Processor
- storage storage.Storage
+ processor processing.Processor
+ storage blob.Storage
// standard suite models
testTokens map[string]*oauth.Token
diff --git a/internal/api/s2s/webfinger/webfinger.go b/internal/api/s2s/webfinger/webfinger.go
@@ -24,7 +24,7 @@ import (
"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/processing"
"github.com/superseriousbusiness/gotosocial/internal/router"
)
@@ -36,12 +36,12 @@ const (
// Module implements the FederationModule interface
type Module struct {
config *config.Config
- processor message.Processor
+ processor processing.Processor
log *logrus.Logger
}
// New returns a new webfinger module
-func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.FederationModule {
+func New(config *config.Config, processor processing.Processor, log *logrus.Logger) api.FederationModule {
return &Module{
config: config,
processor: processor,
diff --git a/internal/blob/inmem.go b/internal/blob/inmem.go
@@ -0,0 +1,55 @@
+package blob
+
+import (
+ "fmt"
+
+ "github.com/sirupsen/logrus"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
+)
+
+// NewInMem returns an in-memory implementation of the Storage interface.
+// This is good for testing and whatnot but ***SHOULD ABSOLUTELY NOT EVER
+// BE USED IN A PRODUCTION SETTING***, because A) everything will be wiped out
+// if you restart the server and B) if you store lots of images your RAM use
+// will absolutely go through the roof.
+func NewInMem(c *config.Config, log *logrus.Logger) (Storage, error) {
+ return &inMemStorage{
+ stored: make(map[string][]byte),
+ log: log,
+ }, nil
+}
+
+type inMemStorage struct {
+ stored map[string][]byte
+ log *logrus.Logger
+}
+
+func (s *inMemStorage) StoreFileAt(path string, data []byte) error {
+ l := s.log.WithField("func", "StoreFileAt")
+ l.Debugf("storing at path %s", path)
+ s.stored[path] = data
+ return nil
+}
+
+func (s *inMemStorage) RetrieveFileFrom(path string) ([]byte, error) {
+ l := s.log.WithField("func", "RetrieveFileFrom")
+ l.Debugf("retrieving from path %s", path)
+ d, ok := s.stored[path]
+ if !ok || len(d) == 0 {
+ return nil, fmt.Errorf("no data found at path %s", path)
+ }
+ return d, nil
+}
+
+func (s *inMemStorage) ListKeys() ([]string, error) {
+ keys := []string{}
+ for k := range s.stored {
+ keys = append(keys, k)
+ }
+ return keys, nil
+}
+
+func (s *inMemStorage) RemoveFileAt(path string) error {
+ delete(s.stored, path)
+ return nil
+}
diff --git a/internal/blob/local.go b/internal/blob/local.go
@@ -0,0 +1,70 @@
+package blob
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/sirupsen/logrus"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
+)
+
+// NewLocal returns an implementation of the Storage interface that uses
+// the local filesystem for storing and retrieving files, attachments, etc.
+func NewLocal(c *config.Config, log *logrus.Logger) (Storage, error) {
+ return &localStorage{
+ config: c,
+ log: log,
+ }, nil
+}
+
+type localStorage struct {
+ config *config.Config
+ log *logrus.Logger
+}
+
+func (s *localStorage) StoreFileAt(path string, data []byte) error {
+ l := s.log.WithField("func", "StoreFileAt")
+ l.Debugf("storing at path %s", path)
+ components := strings.Split(path, "/")
+ dir := strings.Join(components[0:len(components)-1], "/")
+ if err := os.MkdirAll(dir, 0777); err != nil {
+ return fmt.Errorf("error writing file at %s: %s", path, err)
+ }
+ if err := os.WriteFile(path, data, 0777); err != nil {
+ return fmt.Errorf("error writing file at %s: %s", path, err)
+ }
+ return nil
+}
+
+func (s *localStorage) RetrieveFileFrom(path string) ([]byte, error) {
+ l := s.log.WithField("func", "RetrieveFileFrom")
+ l.Debugf("retrieving from path %s", path)
+ b, err := os.ReadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("error reading file at %s: %s", path, err)
+ }
+ return b, nil
+}
+
+func (s *localStorage) ListKeys() ([]string, error) {
+ keys := []string{}
+ err := filepath.Walk(s.config.StorageConfig.BasePath, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if !info.IsDir() {
+ keys = append(keys, path)
+ }
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+ return keys, nil
+}
+
+func (s *localStorage) RemoveFileAt(path string) error {
+ return os.Remove(path)
+}
diff --git a/internal/blob/storage.go b/internal/blob/storage.go
@@ -0,0 +1,29 @@
+/*
+ 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 blob
+
+// Storage is an interface for storing and retrieving blobs
+// such as images, videos, and any other attachments/documents
+// that shouldn't be stored in a database.
+type Storage interface {
+ StoreFileAt(path string, data []byte) error
+ RetrieveFileFrom(path string) ([]byte, error)
+ ListKeys() ([]string, error)
+ RemoveFileAt(path string) error
+}
diff --git a/internal/cliactions/action.go b/internal/cliactions/action.go
@@ -0,0 +1,31 @@
+/*
+ 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 cliactions
+
+import (
+ "context"
+
+ "github.com/sirupsen/logrus"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
+)
+
+// GTSAction defines one *action* that can be taken by the gotosocial cli command.
+// This can be either a long-running action (like server start) or something
+// shorter like db init or db inspect.
+type GTSAction func(context.Context, *config.Config, *logrus.Logger) error
diff --git a/internal/cliactions/admin/account/account.go b/internal/cliactions/admin/account/account.go
@@ -0,0 +1,210 @@
+/*
+ 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 account
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/sirupsen/logrus"
+ "github.com/superseriousbusiness/gotosocial/internal/cliactions"
+ "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/util"
+)
+
+// Create creates a new account in the database using the provided flags.
+var Create cliactions.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
+ dbConn, err := pg.NewPostgresService(ctx, c, log)
+ if err != nil {
+ return fmt.Errorf("error creating dbservice: %s", err)
+ }
+
+ username, ok := c.AccountCLIFlags[config.UsernameFlag]
+ if !ok {
+ return errors.New("no username set")
+ }
+ if err := util.ValidateUsername(username); err != nil {
+ return err
+ }
+
+ email, ok := c.AccountCLIFlags[config.EmailFlag]
+ if !ok {
+ return errors.New("no email set")
+ }
+ if err := util.ValidateEmail(email); err != nil {
+ return err
+ }
+
+ password, ok := c.AccountCLIFlags[config.PasswordFlag]
+ if !ok {
+ return errors.New("no password set")
+ }
+ if err := util.ValidateNewPassword(password); err != nil {
+ return err
+ }
+
+ _, err = dbConn.NewSignup(username, "", false, email, password, nil, "", "")
+ if err != nil {
+ return err
+ }
+
+ return dbConn.Stop(ctx)
+}
+
+// Confirm sets a user to Approved, sets Email to the current UnconfirmedEmail value, and sets ConfirmedAt to now.
+var Confirm cliactions.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
+ dbConn, err := pg.NewPostgresService(ctx, c, log)
+ if err != nil {
+ return fmt.Errorf("error creating dbservice: %s", err)
+ }
+
+ username, ok := c.AccountCLIFlags[config.UsernameFlag]
+ if !ok {
+ return errors.New("no username set")
+ }
+ if err := util.ValidateUsername(username); err != nil {
+ return err
+ }
+
+ a := >smodel.Account{}
+ if err := dbConn.GetLocalAccountByUsername(username, a); err != nil {
+ return err
+ }
+
+ u := >smodel.User{}
+ if err := dbConn.GetWhere([]db.Where{{Key: "account_id", Value: a.ID}}, u); err != nil {
+ return err
+ }
+
+ u.Approved = true
+ u.Email = u.UnconfirmedEmail
+ u.ConfirmedAt = time.Now()
+ if err := dbConn.UpdateByID(u.ID, u); err != nil {
+ return err
+ }
+
+ return dbConn.Stop(ctx)
+}
+
+// Promote sets a user to admin.
+var Promote cliactions.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
+ dbConn, err := pg.NewPostgresService(ctx, c, log)
+ if err != nil {
+ return fmt.Errorf("error creating dbservice: %s", err)
+ }
+
+ username, ok := c.AccountCLIFlags[config.UsernameFlag]
+ if !ok {
+ return errors.New("no username set")
+ }
+ if err := util.ValidateUsername(username); err != nil {
+ return err
+ }
+
+ a := >smodel.Account{}
+ if err := dbConn.GetLocalAccountByUsername(username, a); err != nil {
+ return err
+ }
+
+ u := >smodel.User{}
+ if err := dbConn.GetWhere([]db.Where{{Key: "account_id", Value: a.ID}}, u); err != nil {
+ return err
+ }
+ u.Admin = true
+ if err := dbConn.UpdateByID(u.ID, u); err != nil {
+ return err
+ }
+
+ return dbConn.Stop(ctx)
+}
+
+// Demote sets admin on a user to false.
+var Demote cliactions.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
+ dbConn, err := pg.NewPostgresService(ctx, c, log)
+ if err != nil {
+ return fmt.Errorf("error creating dbservice: %s", err)
+ }
+
+ username, ok := c.AccountCLIFlags[config.UsernameFlag]
+ if !ok {
+ return errors.New("no username set")
+ }
+ if err := util.ValidateUsername(username); err != nil {
+ return err
+ }
+
+ a := >smodel.Account{}
+ if err := dbConn.GetLocalAccountByUsername(username, a); err != nil {
+ return err
+ }
+
+ u := >smodel.User{}
+ if err := dbConn.GetWhere([]db.Where{{Key: "account_id", Value: a.ID}}, u); err != nil {
+ return err
+ }
+ u.Admin = false
+ if err := dbConn.UpdateByID(u.ID, u); err != nil {
+ return err
+ }
+
+ return dbConn.Stop(ctx)
+}
+
+// Disable sets Disabled to true on a user.
+var Disable cliactions.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
+ dbConn, err := pg.NewPostgresService(ctx, c, log)
+ if err != nil {
+ return fmt.Errorf("error creating dbservice: %s", err)
+ }
+
+ username, ok := c.AccountCLIFlags[config.UsernameFlag]
+ if !ok {
+ return errors.New("no username set")
+ }
+ if err := util.ValidateUsername(username); err != nil {
+ return err
+ }
+
+ a := >smodel.Account{}
+ if err := dbConn.GetLocalAccountByUsername(username, a); err != nil {
+ return err
+ }
+
+ u := >smodel.User{}
+ if err := dbConn.GetWhere([]db.Where{{Key: "account_id", Value: a.ID}}, u); err != nil {
+ return err
+ }
+ u.Disabled = true
+ if err := dbConn.UpdateByID(u.ID, u); err != nil {
+ return err
+ }
+
+ return dbConn.Stop(ctx)
+}
+
+// Suspend suspends the target account, cleanly removing all of its media, followers, following, likes, statuses, etc.
+var Suspend cliactions.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
+ // TODO
+ return nil
+}
diff --git a/internal/cliactions/server/server.go b/internal/cliactions/server/server.go
@@ -0,0 +1,179 @@
+package server
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "os"
+ "os/signal"
+ "syscall"
+
+ "github.com/sirupsen/logrus"
+ "github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/api/client/account"
+ "github.com/superseriousbusiness/gotosocial/internal/api/client/admin"
+ "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/notification"
+ "github.com/superseriousbusiness/gotosocial/internal/api/client/search"
+ "github.com/superseriousbusiness/gotosocial/internal/api/client/status"
+ "github.com/superseriousbusiness/gotosocial/internal/api/client/timeline"
+ "github.com/superseriousbusiness/gotosocial/internal/api/s2s/user"
+ "github.com/superseriousbusiness/gotosocial/internal/api/s2s/webfinger"
+ "github.com/superseriousbusiness/gotosocial/internal/api/security"
+ "github.com/superseriousbusiness/gotosocial/internal/blob"
+ "github.com/superseriousbusiness/gotosocial/internal/cliactions"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/db/pg"
+ "github.com/superseriousbusiness/gotosocial/internal/federation"
+ "github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb"
+ "github.com/superseriousbusiness/gotosocial/internal/gotosocial"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/media"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+ "github.com/superseriousbusiness/gotosocial/internal/processing"
+ "github.com/superseriousbusiness/gotosocial/internal/router"
+ "github.com/superseriousbusiness/gotosocial/internal/transport"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+)
+
+var models []interface{} = []interface{}{
+ >smodel.Account{},
+ >smodel.Application{},
+ >smodel.Block{},
+ >smodel.DomainBlock{},
+ >smodel.EmailDomainBlock{},
+ >smodel.Follow{},
+ >smodel.FollowRequest{},
+ >smodel.MediaAttachment{},
+ >smodel.Mention{},
+ >smodel.Status{},
+ >smodel.StatusFave{},
+ >smodel.StatusBookmark{},
+ >smodel.StatusMute{},
+ >smodel.Tag{},
+ >smodel.User{},
+ >smodel.Emoji{},
+ >smodel.Instance{},
+ >smodel.Notification{},
+ &oauth.Token{},
+ &oauth.Client{},
+}
+
+// Start creates and starts a gotosocial server
+var Start cliactions.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
+ dbService, err := pg.NewPostgresService(ctx, c, log)
+ if err != nil {
+ return fmt.Errorf("error creating dbservice: %s", err)
+ }
+
+ federatingDB := federatingdb.New(dbService, c, log)
+
+ router, err := router.New(c, log)
+ if err != nil {
+ return fmt.Errorf("error creating router: %s", err)
+ }
+
+ storageBackend, err := blob.NewLocal(c, log)
+ if err != nil {
+ return fmt.Errorf("error creating storage backend: %s", err)
+ }
+
+ // build converters and util
+ typeConverter := typeutils.NewConverter(c, dbService)
+
+ // build backend handlers
+ mediaHandler := media.New(c, dbService, storageBackend, log)
+ oauthServer := oauth.New(dbService, log)
+ transportController := transport.NewController(c, &federation.Clock{}, http.DefaultClient, log)
+ federator := federation.NewFederator(dbService, federatingDB, transportController, c, log, typeConverter)
+ processor := processing.NewProcessor(c, typeConverter, federator, oauthServer, mediaHandler, storageBackend, dbService, log)
+ if err := processor.Start(); err != nil {
+ return fmt.Errorf("error starting processor: %s", err)
+ }
+
+ // build client api modules
+ authModule := auth.New(c, dbService, oauthServer, log)
+ 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)
+ timelineModule := timeline.New(c, processor, log)
+ notificationModule := notification.New(c, processor, log)
+ searchModule := search.New(c, processor, log)
+ mm := mediaModule.New(c, processor, log)
+ fileServerModule := fileserver.New(c, processor, log)
+ adminModule := admin.New(c, processor, log)
+ statusModule := status.New(c, processor, log)
+ securityModule := security.New(c, log)
+
+ apis := []api.ClientModule{
+ // modules with middleware go first
+ securityModule,
+ authModule,
+
+ // now everything else
+ accountModule,
+ instanceModule,
+ appsModule,
+ followRequestsModule,
+ mm,
+ fileServerModule,
+ adminModule,
+ statusModule,
+ webfingerModule,
+ usersModule,
+ timelineModule,
+ notificationModule,
+ searchModule,
+ }
+
+ for _, m := range apis {
+ if err := m.Route(router); err != nil {
+ return fmt.Errorf("routing error: %s", err)
+ }
+ }
+
+ for _, m := range models {
+ if err := dbService.CreateTable(m); err != nil {
+ return fmt.Errorf("table creation error: %s", err)
+ }
+ }
+
+ if err := dbService.CreateInstanceAccount(); err != nil {
+ return fmt.Errorf("error creating instance account: %s", err)
+ }
+
+ if err := dbService.CreateInstanceInstance(); err != nil {
+ return fmt.Errorf("error creating instance instance: %s", err)
+ }
+
+ gts, err := gotosocial.NewServer(dbService, router, federator, c)
+ if err != nil {
+ return fmt.Errorf("error creating gotosocial service: %s", err)
+ }
+
+ if err := gts.Start(ctx); err != nil {
+ return fmt.Errorf("error starting gotosocial service: %s", err)
+ }
+
+ // catch shutdown signals from the operating system
+ sigs := make(chan os.Signal, 1)
+ signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)
+ sig := <-sigs
+ log.Infof("received signal %s, shutting down", sig)
+
+ // close down all running services in order
+ if err := gts.Stop(ctx); err != nil {
+ return fmt.Errorf("error closing gotosocial service: %s", err)
+ }
+
+ log.Info("done! exiting...")
+ return nil
+}
diff --git a/internal/clitools/admin/account/account.go b/internal/clitools/admin/account/account.go
@@ -1,209 +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 account
-
-import (
- "context"
- "errors"
- "fmt"
- "time"
-
- "github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/action"
- "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/util"
-)
-
-// Create creates a new account in the database using the provided flags.
-var Create action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
- dbConn, err := pg.NewPostgresService(ctx, c, log)
- if err != nil {
- return fmt.Errorf("error creating dbservice: %s", err)
- }
-
- username, ok := c.AccountCLIFlags[config.UsernameFlag]
- if !ok {
- return errors.New("no username set")
- }
- if err := util.ValidateUsername(username); err != nil {
- return err
- }
-
- email, ok := c.AccountCLIFlags[config.EmailFlag]
- if !ok {
- return errors.New("no email set")
- }
- if err := util.ValidateEmail(email); err != nil {
- return err
- }
-
- password, ok := c.AccountCLIFlags[config.PasswordFlag]
- if !ok {
- return errors.New("no password set")
- }
- if err := util.ValidateNewPassword(password); err != nil {
- return err
- }
-
- _, err = dbConn.NewSignup(username, "", false, email, password, nil, "", "")
- if err != nil {
- return err
- }
-
- return dbConn.Stop(ctx)
-}
-
-// Confirm sets a user to Approved, sets Email to the current UnconfirmedEmail value, and sets ConfirmedAt to now.
-var Confirm action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
- dbConn, err := pg.NewPostgresService(ctx, c, log)
- if err != nil {
- return fmt.Errorf("error creating dbservice: %s", err)
- }
-
- username, ok := c.AccountCLIFlags[config.UsernameFlag]
- if !ok {
- return errors.New("no username set")
- }
- if err := util.ValidateUsername(username); err != nil {
- return err
- }
-
- a := >smodel.Account{}
- if err := dbConn.GetLocalAccountByUsername(username, a); err != nil {
- return err
- }
-
- u := >smodel.User{}
- if err := dbConn.GetWhere([]db.Where{{Key: "account_id", Value: a.ID}}, u); err != nil {
- return err
- }
-
- u.Approved = true
- u.Email = u.UnconfirmedEmail
- u.ConfirmedAt = time.Now()
- if err := dbConn.UpdateByID(u.ID, u); err != nil {
- return err
- }
-
- return dbConn.Stop(ctx)
-}
-
-// Promote sets a user to admin.
-var Promote action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
- dbConn, err := pg.NewPostgresService(ctx, c, log)
- if err != nil {
- return fmt.Errorf("error creating dbservice: %s", err)
- }
-
- username, ok := c.AccountCLIFlags[config.UsernameFlag]
- if !ok {
- return errors.New("no username set")
- }
- if err := util.ValidateUsername(username); err != nil {
- return err
- }
-
- a := >smodel.Account{}
- if err := dbConn.GetLocalAccountByUsername(username, a); err != nil {
- return err
- }
-
- u := >smodel.User{}
- if err := dbConn.GetWhere([]db.Where{{Key: "account_id", Value: a.ID}}, u); err != nil {
- return err
- }
- u.Admin = true
- if err := dbConn.UpdateByID(u.ID, u); err != nil {
- return err
- }
-
- return dbConn.Stop(ctx)
-}
-
-// Demote sets admin on a user to false.
-var Demote action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
- dbConn, err := pg.NewPostgresService(ctx, c, log)
- if err != nil {
- return fmt.Errorf("error creating dbservice: %s", err)
- }
-
- username, ok := c.AccountCLIFlags[config.UsernameFlag]
- if !ok {
- return errors.New("no username set")
- }
- if err := util.ValidateUsername(username); err != nil {
- return err
- }
-
- a := >smodel.Account{}
- if err := dbConn.GetLocalAccountByUsername(username, a); err != nil {
- return err
- }
-
- u := >smodel.User{}
- if err := dbConn.GetWhere([]db.Where{{Key: "account_id", Value: a.ID}}, u); err != nil {
- return err
- }
- u.Admin = false
- if err := dbConn.UpdateByID(u.ID, u); err != nil {
- return err
- }
-
- return dbConn.Stop(ctx)
-}
-
-// Disable sets Disabled to true on a user.
-var Disable action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
- dbConn, err := pg.NewPostgresService(ctx, c, log)
- if err != nil {
- return fmt.Errorf("error creating dbservice: %s", err)
- }
-
- username, ok := c.AccountCLIFlags[config.UsernameFlag]
- if !ok {
- return errors.New("no username set")
- }
- if err := util.ValidateUsername(username); err != nil {
- return err
- }
-
- a := >smodel.Account{}
- if err := dbConn.GetLocalAccountByUsername(username, a); err != nil {
- return err
- }
-
- u := >smodel.User{}
- if err := dbConn.GetWhere([]db.Where{{Key: "account_id", Value: a.ID}}, u); err != nil {
- return err
- }
- u.Disabled = true
- if err := dbConn.UpdateByID(u.ID, u); err != nil {
- return err
- }
-
- return dbConn.Stop(ctx)
-}
-
-var Suspend action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
- // TODO
- return nil
-}
diff --git a/internal/config/config.go b/internal/config/config.go
@@ -26,6 +26,7 @@ import (
"gopkg.in/yaml.v2"
)
+// Flags and usage strings for configuration.
const (
UsernameFlag = "username"
UsernameUsage = "the username to create/delete/etc"
diff --git a/internal/db/actions.go b/internal/db/actions.go
@@ -1,37 +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"
-
- "github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/action"
- "github.com/superseriousbusiness/gotosocial/internal/config"
-)
-
-// Initialize will initialize the database given in the config for use with GoToSocial
-var Initialize action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
- // db, err := New(ctx, c, log)
- // if err != nil {
- // return err
- // }
- return nil
- // return db.CreateSchema(ctx)
-}
diff --git a/internal/db/db.go b/internal/db/db.go
@@ -30,34 +30,10 @@ const (
DBTypePostgres string = "POSTGRES"
)
-// ErrNoEntries is to be returned from the DB interface when no entries are found for a given query.
-type ErrNoEntries struct{}
-
-func (e ErrNoEntries) Error() string {
- return "no entries"
-}
-
-// ErrAlreadyExists is to be returned from the DB interface when an entry already exists for a given query or its constraints.
-type ErrAlreadyExists struct{}
-
-func (e ErrAlreadyExists) Error() string {
- return "already exists"
-}
-
-type Where struct {
- Key string
- Value interface{}
- CaseInsensitive bool
-}
-
// DB provides methods for interacting with an underlying database or other storage mechanism (for now, just postgres).
// Note that in all of the functions below, the passed interface should be a pointer or a slice, which will then be populated
// by whatever is returned from the database.
type DB interface {
- // Federation returns an interface that's compatible with go-fed, for performing federation storage/retrieval functions.
- // See: https://pkg.go.dev/github.com/go-fed/activity@v1.0.0/pub?utm_source=gopls#Database
- // Federation() federatingdb.FederatingDB
-
/*
BASIC DB FUNCTIONALITY
*/
@@ -269,10 +245,6 @@ type DB interface {
// StatusBookmarkedBy checks if a given status has been bookmarked by a given account ID
StatusBookmarkedBy(status *gtsmodel.Status, accountID string) (bool, error)
- // FaveStatus faves the given status, using accountID as the faver.
- // The returned fave will be nil if the status was already faved.
- // FaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error)
-
// UnfaveStatus unfaves the given status, using accountID as the unfaver (sure, that's a word).
// The returned fave will be nil if the status was already not faved.
UnfaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error)
@@ -285,6 +257,7 @@ type DB interface {
// It will use the given filters and try to return as many statuses up to the limit as possible.
GetHomeTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error)
+ // GetNotificationsForAccount returns a list of notifications that pertain to the given accountID.
GetNotificationsForAccount(accountID string, limit int, maxID string) ([]*gtsmodel.Notification, error)
/*
diff --git a/internal/db/error.go b/internal/db/error.go
@@ -0,0 +1,33 @@
+/*
+ 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
+
+// ErrNoEntries is to be returned from the DB interface when no entries are found for a given query.
+type ErrNoEntries struct{}
+
+func (e ErrNoEntries) Error() string {
+ return "no entries"
+}
+
+// ErrAlreadyExists is to be returned from the DB interface when an entry already exists for a given query or its constraints.
+type ErrAlreadyExists struct{}
+
+func (e ErrAlreadyExists) Error() string {
+ return "already exists"
+}
diff --git a/internal/db/params.go b/internal/db/params.go
@@ -0,0 +1,30 @@
+/*
+ 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
+
+// Where allows the caller of the DB to specify Where parameters.
+type Where struct {
+ // The table to search on.
+ Key string
+ // The value that must be set.
+ Value interface{}
+ // Whether the value (if a string) should be case sensitive or not.
+ // Defaults to false.
+ CaseInsensitive bool
+}
diff --git a/internal/federation/federator_test.go b/internal/federation/federator_test.go
@@ -35,11 +35,11 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/blob"
"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/storage"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/testrig"
@@ -50,7 +50,7 @@ type ProtocolTestSuite struct {
config *config.Config
db db.DB
log *logrus.Logger
- storage storage.Storage
+ storage blob.Storage
typeConverter typeutils.TypeConverter
accounts map[string]*gtsmodel.Account
activities map[string]testrig.ActivityWithSignature
diff --git a/internal/gotosocial/actions.go b/internal/gotosocial/actions.go
@@ -1,196 +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 gotosocial
-
-import (
- "context"
- "fmt"
- "net/http"
- "os"
- "os/signal"
- "syscall"
-
- "github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/action"
- "github.com/superseriousbusiness/gotosocial/internal/api"
- "github.com/superseriousbusiness/gotosocial/internal/api/client/account"
- "github.com/superseriousbusiness/gotosocial/internal/api/client/admin"
- "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/notification"
- "github.com/superseriousbusiness/gotosocial/internal/api/client/search"
- "github.com/superseriousbusiness/gotosocial/internal/api/client/status"
- "github.com/superseriousbusiness/gotosocial/internal/api/client/timeline"
- "github.com/superseriousbusiness/gotosocial/internal/api/s2s/user"
- "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/pg"
- "github.com/superseriousbusiness/gotosocial/internal/federation"
- "github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/message"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/router"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
- "github.com/superseriousbusiness/gotosocial/internal/transport"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
-)
-
-var models []interface{} = []interface{}{
- >smodel.Account{},
- >smodel.Application{},
- >smodel.Block{},
- >smodel.DomainBlock{},
- >smodel.EmailDomainBlock{},
- >smodel.Follow{},
- >smodel.FollowRequest{},
- >smodel.MediaAttachment{},
- >smodel.Mention{},
- >smodel.Status{},
- >smodel.StatusFave{},
- >smodel.StatusBookmark{},
- >smodel.StatusMute{},
- >smodel.Tag{},
- >smodel.User{},
- >smodel.Emoji{},
- >smodel.Instance{},
- >smodel.Notification{},
- &oauth.Token{},
- &oauth.Client{},
-}
-
-// Run creates and starts a gotosocial server
-var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
- dbService, err := pg.NewPostgresService(ctx, c, log)
- if err != nil {
- return fmt.Errorf("error creating dbservice: %s", err)
- }
-
- federatingDB := federatingdb.New(dbService, c, log)
-
- router, err := router.New(c, log)
- if err != nil {
- return fmt.Errorf("error creating router: %s", err)
- }
-
- storageBackend, err := storage.NewLocal(c, log)
- if err != nil {
- return fmt.Errorf("error creating storage backend: %s", err)
- }
-
- // build converters and util
- typeConverter := typeutils.NewConverter(c, dbService)
-
- // build backend handlers
- mediaHandler := media.New(c, dbService, storageBackend, log)
- oauthServer := oauth.New(dbService, log)
- transportController := transport.NewController(c, &federation.Clock{}, http.DefaultClient, log)
- federator := federation.NewFederator(dbService, federatingDB, transportController, c, log, typeConverter)
- processor := message.NewProcessor(c, typeConverter, federator, oauthServer, mediaHandler, storageBackend, dbService, log)
- if err := processor.Start(); err != nil {
- return fmt.Errorf("error starting processor: %s", err)
- }
-
- // build client api modules
- authModule := auth.New(c, dbService, oauthServer, log)
- 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)
- timelineModule := timeline.New(c, processor, log)
- notificationModule := notification.New(c, processor, log)
- searchModule := search.New(c, processor, log)
- mm := mediaModule.New(c, processor, log)
- fileServerModule := fileserver.New(c, processor, log)
- adminModule := admin.New(c, processor, log)
- statusModule := status.New(c, processor, log)
- securityModule := security.New(c, log)
-
- apis := []api.ClientModule{
- // modules with middleware go first
- securityModule,
- authModule,
-
- // now everything else
- accountModule,
- instanceModule,
- appsModule,
- followRequestsModule,
- mm,
- fileServerModule,
- adminModule,
- statusModule,
- webfingerModule,
- usersModule,
- timelineModule,
- notificationModule,
- searchModule,
- }
-
- for _, m := range apis {
- if err := m.Route(router); err != nil {
- return fmt.Errorf("routing error: %s", err)
- }
- }
-
- for _, m := range models {
- if err := dbService.CreateTable(m); err != nil {
- return fmt.Errorf("table creation error: %s", err)
- }
- }
-
- if err := dbService.CreateInstanceAccount(); err != nil {
- return fmt.Errorf("error creating instance account: %s", err)
- }
-
- if err := dbService.CreateInstanceInstance(); err != nil {
- return fmt.Errorf("error creating instance instance: %s", err)
- }
-
- gts, err := New(dbService, router, federator, c)
- if err != nil {
- return fmt.Errorf("error creating gotosocial service: %s", err)
- }
-
- if err := gts.Start(ctx); err != nil {
- return fmt.Errorf("error starting gotosocial service: %s", err)
- }
-
- // catch shutdown signals from the operating system
- sigs := make(chan os.Signal, 1)
- signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)
- sig := <-sigs
- log.Infof("received signal %s, shutting down", sig)
-
- // close down all running services in order
- if err := gts.Stop(ctx); err != nil {
- return fmt.Errorf("error closing gotosocial service: %s", err)
- }
-
- log.Info("done! exiting...")
- return nil
-}
diff --git a/internal/gotosocial/gotosocial.go b/internal/gotosocial/gotosocial.go
@@ -27,17 +27,22 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/router"
)
-// Gotosocial is the 'main' function of the gotosocial server, and the place where everything hangs together.
+// Server is the 'main' function of the gotosocial server, and the place where everything hangs together.
// The logic of stopping and starting the entire server is contained here.
-type Gotosocial interface {
+type Server interface {
+ // Start starts up the gotosocial server. If something goes wrong
+ // while starting the server, then an error will be returned.
Start(context.Context) error
+ // Stop closes down the gotosocial server, first closing the router
+ // then the database. If something goes wrong while stopping, an
+ // error will be returned.
Stop(context.Context) error
}
-// New returns a new gotosocial server, initialized with the given configuration.
+// NewServer returns a new gotosocial server, initialized with the given configuration.
// An error will be returned the caller if something goes wrong during initialization
// eg., no db or storage connection, port for router already in use, etc.
-func New(db db.DB, apiRouter router.Router, federator federation.Federator, config *config.Config) (Gotosocial, error) {
+func NewServer(db db.DB, apiRouter router.Router, federator federation.Federator, config *config.Config) (Server, error) {
return &gotosocial{
db: db,
apiRouter: apiRouter,
@@ -61,6 +66,9 @@ func (gts *gotosocial) Start(ctx context.Context) error {
return nil
}
+// Stop closes down the gotosocial server, first closing the router
+// then the database. If something goes wrong while stopping, an
+// error will be returned.
func (gts *gotosocial) Stop(ctx context.Context) error {
if err := gts.apiRouter.Stop(ctx); err != nil {
return err
diff --git a/internal/gtsmodel/notification.go b/internal/gtsmodel/notification.go
@@ -61,7 +61,7 @@ const (
NotificationMention NotificationType = "mention"
// NotificationReblog -- someone boosted one of your statuses
NotificationReblog NotificationType = "reblog"
- // NotifiationFave -- someone faved/liked one of your statuses
+ // NotificationFave -- someone faved/liked one of your statuses
NotificationFave NotificationType = "favourite"
// NotificationPoll -- a poll you voted in or created has ended
NotificationPoll NotificationType = "poll"
diff --git a/internal/media/handler.go b/internal/media/handler.go
@@ -28,10 +28,10 @@ import (
"github.com/google/uuid"
"github.com/sirupsen/logrus"
+ "github.com/superseriousbusiness/gotosocial/internal/blob"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
"github.com/superseriousbusiness/gotosocial/internal/transport"
)
@@ -93,12 +93,12 @@ type Handler interface {
type mediaHandler struct {
config *config.Config
db db.DB
- storage storage.Storage
+ storage blob.Storage
log *logrus.Logger
}
// New returns a new handler with the given config, db, storage, and logger
-func New(config *config.Config, database db.DB, storage storage.Storage, log *logrus.Logger) Handler {
+func New(config *config.Config, database db.DB, storage blob.Storage, log *logrus.Logger) Handler {
return &mediaHandler{
config: config,
db: database,
diff --git a/internal/media/handler_test.go b/internal/media/handler_test.go
@@ -1,173 +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 media
-
-import (
- "context"
- "io/ioutil"
- "testing"
-
- "github.com/sirupsen/logrus"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/mock"
- "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"
-)
-
-type MediaTestSuite struct {
- suite.Suite
- config *config.Config
- log *logrus.Logger
- db db.DB
- mediaHandler *mediaHandler
- mockStorage *storage.MockStorage
-}
-
-/*
- TEST INFRASTRUCTURE
-*/
-
-// SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout
-func (suite *MediaTestSuite) SetupSuite() {
- // some of our subsequent entities need a log so create this here
- log := logrus.New()
- log.SetLevel(logrus.TraceLevel)
- suite.log = log
-
- // Direct config to local postgres instance
- c := config.Empty()
- c.Protocol = "http"
- c.Host = "localhost"
- c.DBConfig = &config.DBConfig{
- Type: "postgres",
- Address: "localhost",
- Port: 5432,
- User: "postgres",
- Password: "postgres",
- Database: "postgres",
- ApplicationName: "gotosocial",
- }
- c.MediaConfig = &config.MediaConfig{
- MaxImageSize: 2 << 20,
- }
- c.StorageConfig = &config.StorageConfig{
- Backend: "local",
- BasePath: "/tmp",
- ServeProtocol: "http",
- ServeHost: "localhost",
- ServeBasePath: "/fileserver/media",
- }
- suite.config = c
- // use an actual database for this, because it's just easier than mocking one out
- database, err := pg.NewPostgresService(context.Background(), c, log)
- if err != nil {
- suite.FailNow(err.Error())
- }
- suite.db = database
-
- suite.mockStorage = &storage.MockStorage{}
- // We don't need storage to do anything for these tests, so just simulate a success and do nothing
- suite.mockStorage.On("StoreFileAt", mock.AnythingOfType("string"), mock.AnythingOfType("[]uint8")).Return(nil)
-
- // and finally here's the thing we're actually testing!
- suite.mediaHandler = &mediaHandler{
- config: suite.config,
- db: suite.db,
- storage: suite.mockStorage,
- log: log,
- }
-}
-
-func (suite *MediaTestSuite) TearDownSuite() {
- if err := suite.db.Stop(context.Background()); err != nil {
- logrus.Panicf("error closing db connection: %s", err)
- }
-}
-
-// SetupTest creates a db connection and creates necessary tables before each test
-func (suite *MediaTestSuite) SetupTest() {
- // create all the tables we might need in thie suite
- models := []interface{}{
- >smodel.Account{},
- >smodel.MediaAttachment{},
- }
- for _, m := range models {
- if err := suite.db.CreateTable(m); err != nil {
- logrus.Panicf("db connection error: %s", err)
- }
- }
-
- err := suite.db.CreateInstanceAccount()
- if err != nil {
- logrus.Panic(err)
- }
-}
-
-// TearDownTest drops tables to make sure there's no data in the db
-func (suite *MediaTestSuite) TearDownTest() {
-
- // remove all the tables we might have used so it's clear for the next test
- models := []interface{}{
- >smodel.Account{},
- >smodel.MediaAttachment{},
- }
- for _, m := range models {
- if err := suite.db.DropTable(m); err != nil {
- logrus.Panicf("error dropping table: %s", err)
- }
- }
-}
-
-/*
- ACTUAL TESTS
-*/
-
-func (suite *MediaTestSuite) TestSetHeaderOrAvatarForAccountID() {
- // load test image
- f, err := ioutil.ReadFile("./test/test-jpeg.jpg")
- assert.Nil(suite.T(), err)
-
- ma, err := suite.mediaHandler.ProcessHeaderOrAvatar(f, "weeeeeee", "header", "")
- assert.Nil(suite.T(), err)
- suite.log.Debugf("%+v", ma)
-
- // attachment should have....
- assert.Equal(suite.T(), "weeeeeee", ma.AccountID)
- assert.Equal(suite.T(), "LjCZnlvyRkRn_NvzRjWF?urqV@f9", ma.Blurhash)
- //TODO: add more checks here, cba right now!
-}
-
-func (suite *MediaTestSuite) TestProcessLocalEmoji() {
- f, err := ioutil.ReadFile("./test/rainbow-original.png")
- assert.NoError(suite.T(), err)
-
- emoji, err := suite.mediaHandler.ProcessLocalEmoji(f, "rainbow")
- assert.NoError(suite.T(), err)
- suite.log.Debugf("%+v", emoji)
-}
-
-// TODO: add tests for sad path, gif, png....
-
-func TestMediaTestSuite(t *testing.T) {
- suite.Run(t, new(MediaTestSuite))
-}
diff --git a/internal/message/accountprocess.go b/internal/message/accountprocess.go
@@ -1,553 +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 message
-
-import (
- "errors"
- "fmt"
-
- "github.com/google/uuid"
- 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"
- "github.com/superseriousbusiness/gotosocial/internal/util"
-)
-
-// accountCreate does the dirty work of making an account and user in the database.
-// It then returns a token to the caller, for use with the new account, as per the
-// spec here: https://docs.joinmastodon.org/methods/accounts/
-func (p *processor) AccountCreate(authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, error) {
- l := p.log.WithField("func", "accountCreate")
-
- if err := p.db.IsEmailAvailable(form.Email); err != nil {
- return nil, err
- }
-
- if err := p.db.IsUsernameAvailable(form.Username); err != nil {
- return nil, err
- }
-
- // don't store a reason if we don't require one
- reason := form.Reason
- if !p.config.AccountsConfig.ReasonRequired {
- reason = ""
- }
-
- l.Trace("creating new username and account")
- user, err := p.db.NewSignup(form.Username, reason, p.config.AccountsConfig.RequireApproval, form.Email, form.Password, form.IP, form.Locale, authed.Application.ID)
- if err != nil {
- return nil, fmt.Errorf("error creating new signup in the database: %s", err)
- }
-
- l.Tracef("generating a token for user %s with account %s and application %s", user.ID, user.AccountID, authed.Application.ID)
- accessToken, err := p.oauthServer.GenerateUserAccessToken(authed.Token, authed.Application.ClientSecret, user.ID)
- if err != nil {
- return nil, fmt.Errorf("error creating new access token for user %s: %s", user.ID, err)
- }
-
- return &apimodel.Token{
- AccessToken: accessToken.GetAccess(),
- TokenType: "Bearer",
- Scope: accessToken.GetScope(),
- CreatedAt: accessToken.GetAccessCreateAt().Unix(),
- }, nil
-}
-
-func (p *processor) AccountGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Account, error) {
- targetAccount := >smodel.Account{}
- if err := p.db.GetByID(targetAccountID, targetAccount); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- return nil, errors.New("account not found")
- }
- return nil, fmt.Errorf("db error: %s", err)
- }
-
- // lazily dereference things on the account if it hasn't been done yet
- var requestingUsername string
- if authed.Account != nil {
- requestingUsername = authed.Account.Username
- }
- if err := p.dereferenceAccountFields(targetAccount, requestingUsername, false); err != nil {
- p.log.WithField("func", "AccountGet").Debugf("dereferencing account: %s", err)
- }
-
- var mastoAccount *apimodel.Account
- var err error
- if authed.Account != nil && targetAccount.ID == authed.Account.ID {
- mastoAccount, err = p.tc.AccountToMastoSensitive(targetAccount)
- } else {
- mastoAccount, err = p.tc.AccountToMastoPublic(targetAccount)
- }
- if err != nil {
- return nil, fmt.Errorf("error converting account: %s", err)
- }
- return mastoAccount, nil
-}
-
-func (p *processor) AccountUpdate(authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) {
- l := p.log.WithField("func", "AccountUpdate")
-
- if form.Discoverable != nil {
- if err := p.db.UpdateOneByID(authed.Account.ID, "discoverable", *form.Discoverable, >smodel.Account{}); err != nil {
- return nil, fmt.Errorf("error updating discoverable: %s", err)
- }
- }
-
- if form.Bot != nil {
- if err := p.db.UpdateOneByID(authed.Account.ID, "bot", *form.Bot, >smodel.Account{}); err != nil {
- return nil, fmt.Errorf("error updating bot: %s", err)
- }
- }
-
- if form.DisplayName != nil {
- if err := util.ValidateDisplayName(*form.DisplayName); err != nil {
- return nil, err
- }
- if err := p.db.UpdateOneByID(authed.Account.ID, "display_name", *form.DisplayName, >smodel.Account{}); err != nil {
- return nil, err
- }
- }
-
- if form.Note != nil {
- if err := util.ValidateNote(*form.Note); err != nil {
- return nil, err
- }
- if err := p.db.UpdateOneByID(authed.Account.ID, "note", *form.Note, >smodel.Account{}); err != nil {
- return nil, err
- }
- }
-
- if form.Avatar != nil && form.Avatar.Size != 0 {
- avatarInfo, err := p.updateAccountAvatar(form.Avatar, authed.Account.ID)
- if err != nil {
- return nil, err
- }
- l.Tracef("new avatar info for account %s is %+v", authed.Account.ID, avatarInfo)
- }
-
- if form.Header != nil && form.Header.Size != 0 {
- headerInfo, err := p.updateAccountHeader(form.Header, authed.Account.ID)
- if err != nil {
- return nil, err
- }
- l.Tracef("new header info for account %s is %+v", authed.Account.ID, headerInfo)
- }
-
- if form.Locked != nil {
- if err := p.db.UpdateOneByID(authed.Account.ID, "locked", *form.Locked, >smodel.Account{}); err != nil {
- return nil, err
- }
- }
-
- if form.Source != nil {
- if form.Source.Language != nil {
- if err := util.ValidateLanguage(*form.Source.Language); err != nil {
- return nil, err
- }
- if err := p.db.UpdateOneByID(authed.Account.ID, "language", *form.Source.Language, >smodel.Account{}); err != nil {
- return nil, err
- }
- }
-
- if form.Source.Sensitive != nil {
- if err := p.db.UpdateOneByID(authed.Account.ID, "locked", *form.Locked, >smodel.Account{}); err != nil {
- return nil, err
- }
- }
-
- if form.Source.Privacy != nil {
- if err := util.ValidatePrivacy(*form.Source.Privacy); err != nil {
- return nil, err
- }
- if err := p.db.UpdateOneByID(authed.Account.ID, "privacy", *form.Source.Privacy, >smodel.Account{}); err != nil {
- return nil, err
- }
- }
- }
-
- // fetch the account with all updated values set
- updatedAccount := >smodel.Account{}
- if err := p.db.GetByID(authed.Account.ID, updatedAccount); err != nil {
- return nil, fmt.Errorf("could not fetch updated account %s: %s", authed.Account.ID, err)
- }
-
- p.fromClientAPI <- gtsmodel.FromClientAPI{
- APObjectType: gtsmodel.ActivityStreamsProfile,
- APActivityType: gtsmodel.ActivityStreamsUpdate,
- GTSModel: updatedAccount,
- OriginAccount: updatedAccount,
- }
-
- acctSensitive, err := p.tc.AccountToMastoSensitive(updatedAccount)
- if err != nil {
- return nil, fmt.Errorf("could not convert account into mastosensitive account: %s", err)
- }
- return acctSensitive, nil
-}
-
-func (p *processor) AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, ErrorWithCode) {
- targetAccount := >smodel.Account{}
- if err := p.db.GetByID(targetAccountID, targetAccount); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- return nil, NewErrorNotFound(fmt.Errorf("no entry found for account id %s", targetAccountID))
- }
- return nil, NewErrorInternalError(err)
- }
-
- statuses := []gtsmodel.Status{}
- apiStatuses := []apimodel.Status{}
- if err := p.db.GetStatusesByTimeDescending(targetAccountID, &statuses, limit, excludeReplies, maxID, pinned, mediaOnly); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- return apiStatuses, nil
- }
- return nil, NewErrorInternalError(err)
- }
-
- for _, s := range statuses {
- relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(&s)
- if err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("error getting relevant statuses: %s", err))
- }
-
- visible, err := p.db.StatusVisible(&s, targetAccount, authed.Account, relevantAccounts)
- if err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("error checking status visibility: %s", err))
- }
- if !visible {
- continue
- }
-
- var boostedStatus *gtsmodel.Status
- if s.BoostOfID != "" {
- bs := >smodel.Status{}
- if err := p.db.GetByID(s.BoostOfID, bs); err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("error getting boosted status: %s", err))
- }
- boostedRelevantAccounts, err := p.db.PullRelevantAccountsFromStatus(bs)
- if err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("error getting relevant accounts from boosted status: %s", err))
- }
-
- boostedVisible, err := p.db.StatusVisible(bs, relevantAccounts.BoostedAccount, authed.Account, boostedRelevantAccounts)
- if err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("error checking boosted status visibility: %s", err))
- }
-
- if boostedVisible {
- boostedStatus = bs
- }
- }
-
- apiStatus, err := p.tc.StatusToMasto(&s, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostedStatus)
- if err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("error converting status to masto: %s", err))
- }
-
- apiStatuses = append(apiStatuses, *apiStatus)
- }
-
- return apiStatuses, nil
-}
-
-func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode) {
- blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
-
- if blocked {
- return nil, NewErrorNotFound(fmt.Errorf("block exists between accounts"))
- }
-
- followers := []gtsmodel.Follow{}
- accounts := []apimodel.Account{}
- if err := p.db.GetFollowersByAccountID(targetAccountID, &followers); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- return accounts, nil
- }
- return nil, NewErrorInternalError(err)
- }
-
- for _, f := range followers {
- blocked, err := p.db.Blocked(authed.Account.ID, f.AccountID)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
- if blocked {
- continue
- }
-
- a := >smodel.Account{}
- if err := p.db.GetByID(f.AccountID, a); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- continue
- }
- return nil, NewErrorInternalError(err)
- }
-
- // derefence account fields in case we haven't done it already
- if err := p.dereferenceAccountFields(a, authed.Account.Username, false); err != nil {
- // don't bail if we can't fetch them, we'll try another time
- p.log.WithField("func", "AccountFollowersGet").Debugf("error dereferencing account fields: %s", err)
- }
-
- account, err := p.tc.AccountToMastoPublic(a)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
- accounts = append(accounts, *account)
- }
- return accounts, nil
-}
-
-func (p *processor) AccountFollowingGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode) {
- blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
-
- if blocked {
- return nil, NewErrorNotFound(fmt.Errorf("block exists between accounts"))
- }
-
- following := []gtsmodel.Follow{}
- accounts := []apimodel.Account{}
- if err := p.db.GetFollowingByAccountID(targetAccountID, &following); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- return accounts, nil
- }
- return nil, NewErrorInternalError(err)
- }
-
- for _, f := range following {
- blocked, err := p.db.Blocked(authed.Account.ID, f.AccountID)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
- if blocked {
- continue
- }
-
- a := >smodel.Account{}
- if err := p.db.GetByID(f.TargetAccountID, a); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- continue
- }
- return nil, NewErrorInternalError(err)
- }
-
- // derefence account fields in case we haven't done it already
- if err := p.dereferenceAccountFields(a, authed.Account.Username, false); err != nil {
- // don't bail if we can't fetch them, we'll try another time
- p.log.WithField("func", "AccountFollowingGet").Debugf("error dereferencing account fields: %s", err)
- }
-
- account, err := p.tc.AccountToMastoPublic(a)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
- accounts = append(accounts, *account)
- }
- return accounts, nil
-}
-
-func (p *processor) AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode) {
- if authed == nil || authed.Account == nil {
- return nil, NewErrorForbidden(errors.New("not authed"))
- }
-
- gtsR, err := p.db.GetRelationship(authed.Account.ID, targetAccountID)
- if err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("error getting relationship: %s", err))
- }
-
- r, err := p.tc.RelationshipToMasto(gtsR)
- if err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("error converting relationship: %s", err))
- }
-
- return r, nil
-}
-
-func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, ErrorWithCode) {
- // if there's a block between the accounts we shouldn't create the request ofc
- blocked, err := p.db.Blocked(authed.Account.ID, form.TargetAccountID)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
- if blocked {
- return nil, NewErrorNotFound(fmt.Errorf("accountfollowcreate: block exists between accounts"))
- }
-
- // make sure the target account actually exists in our db
- targetAcct := >smodel.Account{}
- if err := p.db.GetByID(form.TargetAccountID, targetAcct); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- return nil, NewErrorNotFound(fmt.Errorf("accountfollowcreate: account %s not found in the db: %s", form.TargetAccountID, err))
- }
- }
-
- // check if a follow exists already
- follows, err := p.db.Follows(authed.Account, targetAcct)
- if err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow in db: %s", err))
- }
- if follows {
- // already follows so just return the relationship
- return p.AccountRelationshipGet(authed, form.TargetAccountID)
- }
-
- // check if a follow exists already
- followRequested, err := p.db.FollowRequested(authed.Account, targetAcct)
- if err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow request in db: %s", err))
- }
- if followRequested {
- // already follow requested so just return the relationship
- return p.AccountRelationshipGet(authed, form.TargetAccountID)
- }
-
- // make the follow request
-
- newFollowID := uuid.NewString()
-
- fr := >smodel.FollowRequest{
- ID: newFollowID,
- AccountID: authed.Account.ID,
- TargetAccountID: form.TargetAccountID,
- ShowReblogs: true,
- URI: util.GenerateURIForFollow(authed.Account.Username, p.config.Protocol, p.config.Host, newFollowID),
- Notify: false,
- }
- if form.Reblogs != nil {
- fr.ShowReblogs = *form.Reblogs
- }
- if form.Notify != nil {
- fr.Notify = *form.Notify
- }
-
- // whack it in the database
- if err := p.db.Put(fr); err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error creating follow request in db: %s", err))
- }
-
- // if it's a local account that's not locked we can just straight up accept the follow request
- if !targetAcct.Locked && targetAcct.Domain == "" {
- if _, err := p.db.AcceptFollowRequest(authed.Account.ID, form.TargetAccountID); err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error accepting folow request for local unlocked account: %s", err))
- }
- // return the new relationship
- return p.AccountRelationshipGet(authed, form.TargetAccountID)
- }
-
- // otherwise we leave the follow request as it is and we handle the rest of the process asynchronously
- p.fromClientAPI <- gtsmodel.FromClientAPI{
- APObjectType: gtsmodel.ActivityStreamsFollow,
- APActivityType: gtsmodel.ActivityStreamsCreate,
- GTSModel: fr,
- OriginAccount: authed.Account,
- TargetAccount: targetAcct,
- }
-
- // return whatever relationship results from this
- return p.AccountRelationshipGet(authed, form.TargetAccountID)
-}
-
-func (p *processor) AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode) {
- // if there's a block between the accounts we shouldn't do anything
- blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
- if blocked {
- return nil, NewErrorNotFound(fmt.Errorf("AccountFollowRemove: block exists between accounts"))
- }
-
- // make sure the target account actually exists in our db
- targetAcct := >smodel.Account{}
- if err := p.db.GetByID(targetAccountID, targetAcct); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- return nil, NewErrorNotFound(fmt.Errorf("AccountFollowRemove: account %s not found in the db: %s", targetAccountID, err))
- }
- }
-
- // check if a follow request exists, and remove it if it does (storing the URI for later)
- var frChanged bool
- var frURI string
- fr := >smodel.FollowRequest{}
- if err := p.db.GetWhere([]db.Where{
- {Key: "account_id", Value: authed.Account.ID},
- {Key: "target_account_id", Value: targetAccountID},
- }, fr); err == nil {
- frURI = fr.URI
- if err := p.db.DeleteByID(fr.ID, fr); err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow request from db: %s", err))
- }
- frChanged = true
- }
-
- // now do the same thing for any existing follow
- var fChanged bool
- var fURI string
- f := >smodel.Follow{}
- if err := p.db.GetWhere([]db.Where{
- {Key: "account_id", Value: authed.Account.ID},
- {Key: "target_account_id", Value: targetAccountID},
- }, f); err == nil {
- fURI = f.URI
- if err := p.db.DeleteByID(f.ID, f); err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow from db: %s", err))
- }
- fChanged = true
- }
-
- // follow request status changed so send the UNDO activity to the channel for async processing
- if frChanged {
- p.fromClientAPI <- gtsmodel.FromClientAPI{
- APObjectType: gtsmodel.ActivityStreamsFollow,
- APActivityType: gtsmodel.ActivityStreamsUndo,
- GTSModel: >smodel.Follow{
- AccountID: authed.Account.ID,
- TargetAccountID: targetAccountID,
- URI: frURI,
- },
- OriginAccount: authed.Account,
- TargetAccount: targetAcct,
- }
- }
-
- // follow status changed so send the UNDO activity to the channel for async processing
- if fChanged {
- p.fromClientAPI <- gtsmodel.FromClientAPI{
- APObjectType: gtsmodel.ActivityStreamsFollow,
- APActivityType: gtsmodel.ActivityStreamsUndo,
- GTSModel: >smodel.Follow{
- AccountID: authed.Account.ID,
- TargetAccountID: targetAccountID,
- URI: fURI,
- },
- OriginAccount: authed.Account,
- TargetAccount: targetAcct,
- }
- }
-
- // return whatever relationship results from all this
- return p.AccountRelationshipGet(authed, targetAccountID)
-}
diff --git a/internal/message/adminprocess.go b/internal/message/adminprocess.go
@@ -1,66 +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 message
-
-import (
- "bytes"
- "errors"
- "fmt"
- "io"
-
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
-)
-
-func (p *processor) AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) {
- if !authed.User.Admin {
- return nil, fmt.Errorf("user %s not an admin", authed.User.ID)
- }
-
- // open the emoji and extract the bytes from it
- f, err := form.Image.Open()
- if err != nil {
- return nil, fmt.Errorf("error opening emoji: %s", err)
- }
- buf := new(bytes.Buffer)
- size, err := io.Copy(buf, f)
- if err != nil {
- return nil, fmt.Errorf("error reading emoji: %s", err)
- }
- if size == 0 {
- return nil, errors.New("could not read provided emoji: size 0 bytes")
- }
-
- // allow the mediaHandler to work its magic of processing the emoji bytes, and putting them in whatever storage backend we're using
- emoji, err := p.mediaHandler.ProcessLocalEmoji(buf.Bytes(), form.Shortcode)
- if err != nil {
- return nil, fmt.Errorf("error reading emoji: %s", err)
- }
-
- mastoEmoji, err := p.tc.EmojiToMasto(emoji)
- if err != nil {
- return nil, fmt.Errorf("error converting emoji to mastotype: %s", err)
- }
-
- if err := p.db.Put(emoji); err != nil {
- return nil, fmt.Errorf("database error while processing emoji: %s", err)
- }
-
- return &mastoEmoji, nil
-}
diff --git a/internal/message/appprocess.go b/internal/message/appprocess.go
@@ -1,77 +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 message
-
-import (
- "github.com/google/uuid"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
-)
-
-func (p *processor) AppCreate(authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error) {
- // set default 'read' for scopes if it's not set, this follows the default of the mastodon api https://docs.joinmastodon.org/methods/apps/
- var scopes string
- if form.Scopes == "" {
- scopes = "read"
- } else {
- scopes = form.Scopes
- }
-
- // generate new IDs for this application and its associated client
- clientID := uuid.NewString()
- clientSecret := uuid.NewString()
- vapidKey := uuid.NewString()
-
- // generate the application to put in the database
- app := >smodel.Application{
- Name: form.ClientName,
- Website: form.Website,
- RedirectURI: form.RedirectURIs,
- ClientID: clientID,
- ClientSecret: clientSecret,
- Scopes: scopes,
- VapidKey: vapidKey,
- }
-
- // chuck it in the db
- if err := p.db.Put(app); err != nil {
- return nil, err
- }
-
- // now we need to model an oauth client from the application that the oauth library can use
- oc := &oauth.Client{
- ID: clientID,
- Secret: clientSecret,
- Domain: form.RedirectURIs,
- UserID: "", // This client isn't yet associated with a specific user, it's just an app client right now
- }
-
- // chuck it in the db
- if err := p.db.Put(oc); err != nil {
- return nil, err
- }
-
- mastoApp, err := p.tc.AppToMastoSensitive(app)
- if err != nil {
- return nil, err
- }
-
- return mastoApp, nil
-}
diff --git a/internal/message/error.go b/internal/message/error.go
@@ -1,124 +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 message
-
-import (
- "errors"
- "net/http"
- "strings"
-)
-
-// ErrorWithCode wraps an internal error with an http code, and a 'safe' version of
-// the error that can be served to clients without revealing internal business logic.
-//
-// A typical use of this error would be to first log the Original error, then return
-// the Safe error and the StatusCode to an API caller.
-type ErrorWithCode interface {
- // Error returns the original internal error for debugging within the GoToSocial logs.
- // This should *NEVER* be returned to a client as it may contain sensitive information.
- Error() string
- // Safe returns the API-safe version of the error for serialization towards a client.
- // There's not much point logging this internally because it won't contain much helpful information.
- Safe() string
- // Code returns the status code for serving to a client.
- Code() int
-}
-
-type errorWithCode struct {
- original error
- safe error
- code int
-}
-
-func (e errorWithCode) Error() string {
- return e.original.Error()
-}
-
-func (e errorWithCode) Safe() string {
- return e.safe.Error()
-}
-
-func (e errorWithCode) Code() int {
- return e.code
-}
-
-// NewErrorBadRequest returns an ErrorWithCode 400 with the given original error and optional help text.
-func NewErrorBadRequest(original error, helpText ...string) ErrorWithCode {
- safe := "bad request"
- if helpText != nil {
- safe = safe + ": " + strings.Join(helpText, ": ")
- }
- return errorWithCode{
- original: original,
- safe: errors.New(safe),
- code: http.StatusBadRequest,
- }
-}
-
-// NewErrorNotAuthorized returns an ErrorWithCode 401 with the given original error and optional help text.
-func NewErrorNotAuthorized(original error, helpText ...string) ErrorWithCode {
- safe := "not authorized"
- if helpText != nil {
- safe = safe + ": " + strings.Join(helpText, ": ")
- }
- return errorWithCode{
- original: original,
- safe: errors.New(safe),
- code: http.StatusUnauthorized,
- }
-}
-
-// NewErrorForbidden returns an ErrorWithCode 403 with the given original error and optional help text.
-func NewErrorForbidden(original error, helpText ...string) ErrorWithCode {
- safe := "forbidden"
- if helpText != nil {
- safe = safe + ": " + strings.Join(helpText, ": ")
- }
- return errorWithCode{
- original: original,
- safe: errors.New(safe),
- code: http.StatusForbidden,
- }
-}
-
-// NewErrorNotFound returns an ErrorWithCode 404 with the given original error and optional help text.
-func NewErrorNotFound(original error, helpText ...string) ErrorWithCode {
- safe := "404 not found"
- if helpText != nil {
- safe = safe + ": " + strings.Join(helpText, ": ")
- }
- return errorWithCode{
- original: original,
- safe: errors.New(safe),
- code: http.StatusNotFound,
- }
-}
-
-// NewErrorInternalError returns an ErrorWithCode 500 with the given original error and optional help text.
-func NewErrorInternalError(original error, helpText ...string) ErrorWithCode {
- safe := "internal server error"
- if helpText != nil {
- safe = safe + ": " + strings.Join(helpText, ": ")
- }
- return errorWithCode{
- original: original,
- safe: errors.New(safe),
- code: http.StatusInternalServerError,
- }
-}
diff --git a/internal/message/fediprocess.go b/internal/message/fediprocess.go
@@ -1,282 +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 message
-
-import (
- "context"
- "fmt"
- "net/http"
- "net/url"
-
- "github.com/go-fed/activity/streams"
- 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
-// username to perform the validation. It will *also* dereference the originator of the request and return it as a gtsmodel account
-// for further processing. NOTE that this function will have the side effect of putting the dereferenced account into the database,
-// and passing it into the processor through a channel for further asynchronous processing.
-func (p *processor) authenticateAndDereferenceFediRequest(username string, r *http.Request) (*gtsmodel.Account, error) {
-
- // first authenticate
- requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(username, r)
- if err != nil {
- return nil, fmt.Errorf("couldn't authenticate request for username %s: %s", username, err)
- }
-
- // OK now we can do the dereferencing part
- // we might already have an entry for this account so check that first
- requestingAccount := >smodel.Account{}
-
- err = p.db.GetWhere([]db.Where{{Key: "uri", Value: requestingAccountURI.String()}}, requestingAccount)
- if err == nil {
- // we do have it yay, return it
- return requestingAccount, nil
- }
-
- if _, ok := err.(db.ErrNoEntries); !ok {
- // something has actually gone wrong so bail
- return nil, fmt.Errorf("database error getting account with uri %s: %s", requestingAccountURI.String(), err)
- }
-
- // we just don't have an entry for this account yet
- // what we do now should depend on our chosen federation method
- // for now though, we'll just dereference it
- // TODO: slow-fed
- requestingPerson, err := p.federator.DereferenceRemoteAccount(username, requestingAccountURI)
- if err != nil {
- return nil, fmt.Errorf("couldn't dereference %s: %s", requestingAccountURI.String(), err)
- }
-
- // convert it to our internal account representation
- requestingAccount, err = p.tc.ASRepresentationToAccount(requestingPerson, false)
- if err != nil {
- return nil, fmt.Errorf("couldn't convert dereferenced uri %s to gtsmodel account: %s", requestingAccountURI.String(), err)
- }
-
- // shove it in the database for later
- if err := p.db.Put(requestingAccount); err != nil {
- return nil, fmt.Errorf("database error inserting account with uri %s: %s", requestingAccountURI.String(), err)
- }
-
- // put it in our channel to queue it for async processing
- p.FromFederator() <- gtsmodel.FromFederator{
- APObjectType: gtsmodel.ActivityStreamsProfile,
- APActivityType: gtsmodel.ActivityStreamsCreate,
- GTSModel: requestingAccount,
- }
-
- return requestingAccount, nil
-}
-
-func (p *processor) GetFediUser(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) {
- // get the account the request is referring to
- requestedAccount := >smodel.Account{}
- if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
- return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
- }
-
- // authenticate the request
- requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
- if err != nil {
- return nil, NewErrorNotAuthorized(err)
- }
-
- blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
-
- if blocked {
- return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
- }
-
- requestedPerson, err := p.tc.AccountToAS(requestedAccount)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
-
- data, err := streams.Serialize(requestedPerson)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
-
- return data, nil
-}
-
-func (p *processor) GetFediFollowers(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) {
- // get the account the request is referring to
- requestedAccount := >smodel.Account{}
- if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
- return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
- }
-
- // authenticate the request
- requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
- if err != nil {
- return nil, NewErrorNotAuthorized(err)
- }
-
- blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
-
- if blocked {
- return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
- }
-
- requestedAccountURI, err := url.Parse(requestedAccount.URI)
- if err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err))
- }
-
- requestedFollowers, err := p.federator.FederatingDB().Followers(context.Background(), requestedAccountURI)
- if err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("error fetching followers for uri %s: %s", requestedAccountURI.String(), err))
- }
-
- data, err := streams.Serialize(requestedFollowers)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
-
- return data, nil
-}
-
-func (p *processor) GetFediFollowing(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) {
- // get the account the request is referring to
- requestedAccount := >smodel.Account{}
- if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
- return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
- }
-
- // authenticate the request
- requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
- if err != nil {
- return nil, NewErrorNotAuthorized(err)
- }
-
- blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
-
- if blocked {
- return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
- }
-
- requestedAccountURI, err := url.Parse(requestedAccount.URI)
- if err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err))
- }
-
- requestedFollowing, err := p.federator.FederatingDB().Following(context.Background(), requestedAccountURI)
- if err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("error fetching following for uri %s: %s", requestedAccountURI.String(), err))
- }
-
- data, err := streams.Serialize(requestedFollowing)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
-
- return data, nil
-}
-
-func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, ErrorWithCode) {
- // get the account the request is referring to
- requestedAccount := >smodel.Account{}
- if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
- return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
- }
-
- // authenticate the request
- requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
- if err != nil {
- return nil, NewErrorNotAuthorized(err)
- }
-
- blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
-
- if blocked {
- return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
- }
-
- s := >smodel.Status{}
- if err := p.db.GetWhere([]db.Where{
- {Key: "id", Value: requestedStatusID},
- {Key: "account_id", Value: requestedAccount.ID},
- }, s); err != nil {
- return nil, NewErrorNotFound(fmt.Errorf("database error getting status with id %s and account id %s: %s", requestedStatusID, requestedAccount.ID, err))
- }
-
- asStatus, err := p.tc.StatusToAS(s)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
-
- data, err := streams.Serialize(asStatus)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
-
- return data, nil
-}
-
-func (p *processor) GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, ErrorWithCode) {
- // get the account the request is referring to
- requestedAccount := >smodel.Account{}
- if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
- return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
- }
-
- // return the webfinger representation
- return &apimodel.WebfingerAccountResponse{
- Subject: fmt.Sprintf("acct:%s@%s", requestedAccount.Username, p.config.Host),
- Aliases: []string{
- requestedAccount.URI,
- requestedAccount.URL,
- },
- Links: []apimodel.WebfingerLink{
- {
- Rel: "http://webfinger.net/rel/profile-page",
- Type: "text/html",
- Href: requestedAccount.URL,
- },
- {
- Rel: "self",
- Type: "application/activity+json",
- Href: requestedAccount.URI,
- },
- },
- }, 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/fromclientapiprocess.go b/internal/message/fromclientapiprocess.go
@@ -1,313 +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 message
-
-import (
- "context"
- "errors"
- "fmt"
- "net/url"
-
- "github.com/go-fed/activity/streams"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
-)
-
-func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error {
- switch clientMsg.APActivityType {
- case gtsmodel.ActivityStreamsCreate:
- // CREATE
- switch clientMsg.APObjectType {
- case gtsmodel.ActivityStreamsNote:
- // CREATE NOTE
- status, ok := clientMsg.GTSModel.(*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 != nil && status.VisibilityAdvanced.Federated {
- return p.federateStatus(status)
- }
- return nil
- case gtsmodel.ActivityStreamsFollow:
- // CREATE FOLLOW REQUEST
- followRequest, ok := clientMsg.GTSModel.(*gtsmodel.FollowRequest)
- if !ok {
- return errors.New("followrequest was not parseable as *gtsmodel.FollowRequest")
- }
-
- if err := p.notifyFollowRequest(followRequest, clientMsg.TargetAccount); err != nil {
- return err
- }
-
- return p.federateFollow(followRequest, clientMsg.OriginAccount, clientMsg.TargetAccount)
- case gtsmodel.ActivityStreamsLike:
- // CREATE LIKE/FAVE
- fave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave)
- if !ok {
- return errors.New("fave was not parseable as *gtsmodel.StatusFave")
- }
-
- if err := p.notifyFave(fave, clientMsg.TargetAccount); err != nil {
- return err
- }
-
- return p.federateFave(fave, clientMsg.OriginAccount, clientMsg.TargetAccount)
-
- case gtsmodel.ActivityStreamsAnnounce:
- // CREATE BOOST/ANNOUNCE
- boostWrapperStatus, ok := clientMsg.GTSModel.(*gtsmodel.Status)
- if !ok {
- return errors.New("boost was not parseable as *gtsmodel.Status")
- }
-
- if err := p.notifyAnnounce(boostWrapperStatus); err != nil {
- return err
- }
-
- return p.federateAnnounce(boostWrapperStatus, clientMsg.OriginAccount, clientMsg.TargetAccount)
- }
- case gtsmodel.ActivityStreamsUpdate:
- // UPDATE
- switch clientMsg.APObjectType {
- case gtsmodel.ActivityStreamsProfile, gtsmodel.ActivityStreamsPerson:
- // UPDATE ACCOUNT/PROFILE
- account, ok := clientMsg.GTSModel.(*gtsmodel.Account)
- if !ok {
- return errors.New("account was not parseable as *gtsmodel.Account")
- }
-
- return p.federateAccountUpdate(account, clientMsg.OriginAccount)
- }
- case gtsmodel.ActivityStreamsAccept:
- // ACCEPT
- switch clientMsg.APObjectType {
- case gtsmodel.ActivityStreamsFollow:
- // ACCEPT FOLLOW
- follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow)
- if !ok {
- return errors.New("accept was not parseable as *gtsmodel.Follow")
- }
-
- if err := p.notifyFollow(follow, clientMsg.TargetAccount); err != nil {
- return err
- }
-
- return p.federateAcceptFollowRequest(follow, clientMsg.OriginAccount, clientMsg.TargetAccount)
- }
- case gtsmodel.ActivityStreamsUndo:
- // UNDO
- switch clientMsg.APObjectType {
- case gtsmodel.ActivityStreamsFollow:
- // UNDO FOLLOW
- follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow)
- if !ok {
- return errors.New("undo was not parseable as *gtsmodel.Follow")
- }
- return p.federateUnfollow(follow, clientMsg.OriginAccount, clientMsg.TargetAccount)
- }
- }
- return nil
-}
-
-func (p *processor) federateStatus(status *gtsmodel.Status) error {
- asStatus, err := p.tc.StatusToAS(status)
- if err != nil {
- return fmt.Errorf("federateStatus: error converting status to as format: %s", err)
- }
-
- outboxIRI, err := url.Parse(status.GTSAuthorAccount.OutboxURI)
- if err != nil {
- return fmt.Errorf("federateStatus: error parsing outboxURI %s: %s", status.GTSAuthorAccount.OutboxURI, err)
- }
-
- _, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, asStatus)
- return err
-}
-
-func (p *processor) federateFollow(followRequest *gtsmodel.FollowRequest, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
- // if both accounts are local there's nothing to do here
- if originAccount.Domain == "" && targetAccount.Domain == "" {
- return nil
- }
-
- follow := p.tc.FollowRequestToFollow(followRequest)
-
- asFollow, err := p.tc.FollowToAS(follow, originAccount, targetAccount)
- if err != nil {
- return fmt.Errorf("federateFollow: error converting follow to as format: %s", err)
- }
-
- outboxIRI, err := url.Parse(originAccount.OutboxURI)
- if err != nil {
- return fmt.Errorf("federateFollow: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
- }
-
- _, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, asFollow)
- return err
-}
-
-func (p *processor) federateUnfollow(follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
- // if both accounts are local there's nothing to do here
- if originAccount.Domain == "" && targetAccount.Domain == "" {
- return nil
- }
-
- // recreate the follow
- asFollow, err := p.tc.FollowToAS(follow, originAccount, targetAccount)
- if err != nil {
- return fmt.Errorf("federateUnfollow: error converting follow to as format: %s", err)
- }
-
- targetAccountURI, err := url.Parse(targetAccount.URI)
- if err != nil {
- return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err)
- }
-
- // create an Undo and set the appropriate actor on it
- undo := streams.NewActivityStreamsUndo()
- undo.SetActivityStreamsActor(asFollow.GetActivityStreamsActor())
-
- // Set the recreated follow as the 'object' property.
- undoObject := streams.NewActivityStreamsObjectProperty()
- undoObject.AppendActivityStreamsFollow(asFollow)
- undo.SetActivityStreamsObject(undoObject)
-
- // Set the To of the undo as the target of the recreated follow
- undoTo := streams.NewActivityStreamsToProperty()
- undoTo.AppendIRI(targetAccountURI)
- undo.SetActivityStreamsTo(undoTo)
-
- outboxIRI, err := url.Parse(originAccount.OutboxURI)
- if err != nil {
- return fmt.Errorf("federateUnfollow: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
- }
-
- // send off the Undo
- _, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, undo)
- return err
-}
-
-func (p *processor) federateAcceptFollowRequest(follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
- // if both accounts are local there's nothing to do here
- if originAccount.Domain == "" && targetAccount.Domain == "" {
- return nil
- }
-
- // recreate the AS follow
- asFollow, err := p.tc.FollowToAS(follow, originAccount, targetAccount)
- if err != nil {
- return fmt.Errorf("federateUnfollow: error converting follow to as format: %s", err)
- }
-
- acceptingAccountURI, err := url.Parse(targetAccount.URI)
- if err != nil {
- return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err)
- }
-
- requestingAccountURI, err := url.Parse(originAccount.URI)
- if err != nil {
- return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err)
- }
-
- // create an Accept
- accept := streams.NewActivityStreamsAccept()
-
- // set the accepting actor on it
- acceptActorProp := streams.NewActivityStreamsActorProperty()
- acceptActorProp.AppendIRI(acceptingAccountURI)
- accept.SetActivityStreamsActor(acceptActorProp)
-
- // Set the recreated follow as the 'object' property.
- acceptObject := streams.NewActivityStreamsObjectProperty()
- acceptObject.AppendActivityStreamsFollow(asFollow)
- accept.SetActivityStreamsObject(acceptObject)
-
- // Set the To of the accept as the originator of the follow
- acceptTo := streams.NewActivityStreamsToProperty()
- acceptTo.AppendIRI(requestingAccountURI)
- accept.SetActivityStreamsTo(acceptTo)
-
- outboxIRI, err := url.Parse(targetAccount.OutboxURI)
- if err != nil {
- return fmt.Errorf("federateAcceptFollowRequest: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
- }
-
- // send off the accept using the accepter's outbox
- _, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, accept)
- return err
-}
-
-func (p *processor) federateFave(fave *gtsmodel.StatusFave, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
- // if both accounts are local there's nothing to do here
- if originAccount.Domain == "" && targetAccount.Domain == "" {
- return nil
- }
-
- // create the AS fave
- asFave, err := p.tc.FaveToAS(fave)
- if err != nil {
- return fmt.Errorf("federateFave: error converting fave to as format: %s", err)
- }
-
- outboxIRI, err := url.Parse(originAccount.OutboxURI)
- if err != nil {
- return fmt.Errorf("federateFave: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
- }
- _, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, asFave)
- return err
-}
-
-func (p *processor) federateAnnounce(boostWrapperStatus *gtsmodel.Status, boostingAccount *gtsmodel.Account, boostedAccount *gtsmodel.Account) error {
- announce, err := p.tc.BoostToAS(boostWrapperStatus, boostingAccount, boostedAccount)
- if err != nil {
- return fmt.Errorf("federateAnnounce: error converting status to announce: %s", err)
- }
-
- outboxIRI, err := url.Parse(boostingAccount.OutboxURI)
- if err != nil {
- return fmt.Errorf("federateAnnounce: error parsing outboxURI %s: %s", boostingAccount.OutboxURI, err)
- }
-
- _, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, announce)
- return err
-}
-
-func (p *processor) federateAccountUpdate(updatedAccount *gtsmodel.Account, originAccount *gtsmodel.Account) error {
- person, err := p.tc.AccountToAS(updatedAccount)
- if err != nil {
- return fmt.Errorf("federateAccountUpdate: error converting account to person: %s", err)
- }
-
- update, err := p.tc.WrapPersonInUpdate(person, originAccount)
- if err != nil {
- return fmt.Errorf("federateAccountUpdate: error wrapping person in update: %s", err)
- }
-
- outboxIRI, err := url.Parse(originAccount.OutboxURI)
- if err != nil {
- return fmt.Errorf("federateAnnounce: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
- }
-
- _, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, update)
- return err
-}
diff --git a/internal/message/fromcommonprocess.go b/internal/message/fromcommonprocess.go
@@ -1,164 +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 message
-
-import (
- "fmt"
-
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
-)
-
-func (p *processor) notifyStatus(status *gtsmodel.Status) error {
- // if there are no mentions in this status then just bail
- if len(status.Mentions) == 0 {
- return nil
- }
-
- if status.GTSMentions == nil {
- // there are mentions but they're not fully populated on the status yet so do this
- menchies := []*gtsmodel.Mention{}
- for _, m := range status.Mentions {
- gtsm := >smodel.Mention{}
- if err := p.db.GetByID(m, gtsm); err != nil {
- return fmt.Errorf("notifyStatus: error getting mention with id %s from the db: %s", m, err)
- }
- menchies = append(menchies, gtsm)
- }
- status.GTSMentions = menchies
- }
-
- // now we have mentions as full gtsmodel.Mention structs on the status we can continue
- for _, m := range status.GTSMentions {
- // make sure this is a local account, otherwise we don't need to create a notification for it
- if m.GTSAccount == nil {
- a := >smodel.Account{}
- if err := p.db.GetByID(m.TargetAccountID, a); err != nil {
- // we don't have the account or there's been an error
- return fmt.Errorf("notifyStatus: error getting account with id %s from the db: %s", m.TargetAccountID, err)
- }
- m.GTSAccount = a
- }
- if m.GTSAccount.Domain != "" {
- // not a local account so skip it
- continue
- }
-
- // make sure a notif doesn't already exist for this mention
- err := p.db.GetWhere([]db.Where{
- {Key: "notification_type", Value: gtsmodel.NotificationMention},
- {Key: "target_account_id", Value: m.TargetAccountID},
- {Key: "origin_account_id", Value: status.AccountID},
- {Key: "status_id", Value: status.ID},
- }, >smodel.Notification{})
- if err == nil {
- // notification exists already so just continue
- continue
- }
- if _, ok := err.(db.ErrNoEntries); !ok {
- // there's a real error in the db
- return fmt.Errorf("notifyStatus: error checking existence of notification for mention with id %s : %s", m.ID, err)
- }
-
- // if we've reached this point we know the mention is for a local account, and the notification doesn't exist, so create it
- notif := >smodel.Notification{
- NotificationType: gtsmodel.NotificationMention,
- TargetAccountID: m.TargetAccountID,
- OriginAccountID: status.AccountID,
- StatusID: status.ID,
- }
-
- if err := p.db.Put(notif); err != nil {
- return fmt.Errorf("notifyStatus: error putting notification in database: %s", err)
- }
- }
-
- return nil
-}
-
-func (p *processor) notifyFollowRequest(followRequest *gtsmodel.FollowRequest, receivingAccount *gtsmodel.Account) error {
- // return if this isn't a local account
- if receivingAccount.Domain != "" {
- return nil
- }
-
- notif := >smodel.Notification{
- NotificationType: gtsmodel.NotificationFollowRequest,
- TargetAccountID: followRequest.TargetAccountID,
- OriginAccountID: followRequest.AccountID,
- }
-
- if err := p.db.Put(notif); err != nil {
- return fmt.Errorf("notifyFollowRequest: error putting notification in database: %s", err)
- }
-
- return nil
-}
-
-func (p *processor) notifyFollow(follow *gtsmodel.Follow, receivingAccount *gtsmodel.Account) error {
- // return if this isn't a local account
- if receivingAccount.Domain != "" {
- return nil
- }
-
- // first remove the follow request notification
- if err := p.db.DeleteWhere([]db.Where{
- {Key: "notification_type", Value: gtsmodel.NotificationFollowRequest},
- {Key: "target_account_id", Value: follow.TargetAccountID},
- {Key: "origin_account_id", Value: follow.AccountID},
- }, >smodel.Notification{}); err != nil {
- return fmt.Errorf("notifyFollow: error removing old follow request notification from database: %s", err)
- }
-
- // now create the new follow notification
- notif := >smodel.Notification{
- NotificationType: gtsmodel.NotificationFollow,
- TargetAccountID: follow.TargetAccountID,
- OriginAccountID: follow.AccountID,
- }
- if err := p.db.Put(notif); err != nil {
- return fmt.Errorf("notifyFollow: error putting notification in database: %s", err)
- }
-
- return nil
-}
-
-func (p *processor) notifyFave(fave *gtsmodel.StatusFave, receivingAccount *gtsmodel.Account) error {
- // return if this isn't a local account
- if receivingAccount.Domain != "" {
- return nil
- }
-
- notif := >smodel.Notification{
- NotificationType: gtsmodel.NotificationFave,
- TargetAccountID: fave.TargetAccountID,
- OriginAccountID: fave.AccountID,
- StatusID: fave.StatusID,
- }
-
- if err := p.db.Put(notif); err != nil {
- return fmt.Errorf("notifyFave: error putting notification in database: %s", err)
- }
-
- return nil
-}
-
-func (p *processor) notifyAnnounce(status *gtsmodel.Status) error {
- return nil
-}
diff --git a/internal/message/fromfederatorprocess.go b/internal/message/fromfederatorprocess.go
@@ -1,433 +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 message
-
-import (
- "errors"
- "fmt"
- "net/url"
-
- "github.com/google/uuid"
- "github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
-)
-
-func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) error {
- l := p.log.WithFields(logrus.Fields{
- "func": "processFromFederator",
- "federatorMsg": fmt.Sprintf("%+v", federatorMsg),
- })
-
- l.Debug("entering function PROCESS FROM FEDERATOR")
-
- switch federatorMsg.APActivityType {
- case gtsmodel.ActivityStreamsCreate:
- // CREATE
- switch federatorMsg.APObjectType {
- case gtsmodel.ActivityStreamsNote:
- // CREATE A STATUS
- incomingStatus, ok := federatorMsg.GTSModel.(*gtsmodel.Status)
- if !ok {
- return errors.New("note was not parseable as *gtsmodel.Status")
- }
-
- l.Debug("will now derefence incoming status")
- if err := p.dereferenceStatusFields(incomingStatus, federatorMsg.ReceivingAccount.Username); err != nil {
- return fmt.Errorf("error dereferencing status from federator: %s", err)
- }
- if err := p.db.UpdateByID(incomingStatus.ID, incomingStatus); err != nil {
- return fmt.Errorf("error updating dereferenced status in the db: %s", err)
- }
-
- if err := p.notifyStatus(incomingStatus); err != nil {
- return err
- }
- case gtsmodel.ActivityStreamsProfile:
- // CREATE AN ACCOUNT
- incomingAccount, ok := federatorMsg.GTSModel.(*gtsmodel.Account)
- if !ok {
- return errors.New("profile was not parseable as *gtsmodel.Account")
- }
-
- l.Debug("will now derefence incoming account")
- if err := p.dereferenceAccountFields(incomingAccount, "", false); err != nil {
- return fmt.Errorf("error dereferencing account from federator: %s", err)
- }
- if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil {
- return fmt.Errorf("error updating dereferenced account in the db: %s", err)
- }
- case gtsmodel.ActivityStreamsLike:
- // CREATE A FAVE
- incomingFave, ok := federatorMsg.GTSModel.(*gtsmodel.StatusFave)
- if !ok {
- return errors.New("like was not parseable as *gtsmodel.StatusFave")
- }
-
- if err := p.notifyFave(incomingFave, federatorMsg.ReceivingAccount); err != nil {
- return err
- }
- case gtsmodel.ActivityStreamsFollow:
- // CREATE A FOLLOW REQUEST
- incomingFollowRequest, ok := federatorMsg.GTSModel.(*gtsmodel.FollowRequest)
- if !ok {
- return errors.New("incomingFollowRequest was not parseable as *gtsmodel.FollowRequest")
- }
-
- if err := p.notifyFollowRequest(incomingFollowRequest, federatorMsg.ReceivingAccount); err != nil {
- return err
- }
- case gtsmodel.ActivityStreamsAnnounce:
- // CREATE AN ANNOUNCE
- incomingAnnounce, ok := federatorMsg.GTSModel.(*gtsmodel.Status)
- if !ok {
- return errors.New("announce was not parseable as *gtsmodel.Status")
- }
-
- if err := p.dereferenceAnnounce(incomingAnnounce, federatorMsg.ReceivingAccount.Username); err != nil {
- return fmt.Errorf("error dereferencing announce from federator: %s", err)
- }
-
- if err := p.db.Put(incomingAnnounce); err != nil {
- if _, ok := err.(db.ErrAlreadyExists); !ok {
- return fmt.Errorf("error adding dereferenced announce to the db: %s", err)
- }
- }
-
- if err := p.notifyAnnounce(incomingAnnounce); err != nil {
- return err
- }
- }
- case gtsmodel.ActivityStreamsUpdate:
- // UPDATE
- switch federatorMsg.APObjectType {
- case gtsmodel.ActivityStreamsProfile:
- // UPDATE AN ACCOUNT
- incomingAccount, ok := federatorMsg.GTSModel.(*gtsmodel.Account)
- if !ok {
- return errors.New("profile was not parseable as *gtsmodel.Account")
- }
-
- l.Debug("will now derefence incoming account")
- if err := p.dereferenceAccountFields(incomingAccount, federatorMsg.ReceivingAccount.Username, true); err != nil {
- return fmt.Errorf("error dereferencing account from federator: %s", err)
- }
- if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil {
- return fmt.Errorf("error updating dereferenced account in the db: %s", err)
- }
- }
- case gtsmodel.ActivityStreamsDelete:
- // DELETE
- switch federatorMsg.APObjectType {
- case gtsmodel.ActivityStreamsNote:
- // DELETE A STATUS
- // TODO: handle side effects of status deletion here:
- // 1. delete all media associated with status
- // 2. delete boosts of status
- // 3. etc etc etc
- case gtsmodel.ActivityStreamsProfile:
- // DELETE A PROFILE/ACCOUNT
- // TODO: handle side effects of account deletion here: delete all objects, statuses, media etc associated with account
- }
- case gtsmodel.ActivityStreamsAccept:
- // ACCEPT
- switch federatorMsg.APObjectType {
- case gtsmodel.ActivityStreamsFollow:
- // ACCEPT A FOLLOW
- follow, ok := federatorMsg.GTSModel.(*gtsmodel.Follow)
- if !ok {
- return errors.New("follow was not parseable as *gtsmodel.Follow")
- }
-
- if err := p.notifyFollow(follow, federatorMsg.ReceivingAccount); err != nil {
- return err
- }
- }
- }
-
- return nil
-}
-
-// dereferenceStatusFields fetches all the information we temporarily pinned to an incoming
-// federated status, back in the federating db's Create function.
-//
-// When a status comes in from the federation API, there are certain fields that
-// haven't been dereferenced yet, because we needed to provide a snappy synchronous
-// response to the caller. By the time it reaches this function though, it's being
-// processed asynchronously, so we have all the time in the world to fetch the various
-// bits and bobs that are attached to the status, and properly flesh it out, before we
-// send the status to any timelines and notify people.
-//
-// Things to dereference and fetch here:
-//
-// 1. Media attachments.
-// 2. Hashtags.
-// 3. Emojis.
-// 4. Mentions.
-// 5. Posting account.
-// 6. Replied-to-status.
-//
-// SIDE EFFECTS:
-// This function will deference all of the above, insert them in the database as necessary,
-// and attach them to the status. The status itself will not be added to the database yet,
-// that's up the caller to do.
-func (p *processor) dereferenceStatusFields(status *gtsmodel.Status, requestingUsername string) error {
- l := p.log.WithFields(logrus.Fields{
- "func": "dereferenceStatusFields",
- "status": fmt.Sprintf("%+v", status),
- })
- l.Debug("entering function")
-
- t, err := p.federator.GetTransportForUser(requestingUsername)
- if err != nil {
- return fmt.Errorf("error creating transport: %s", err)
- }
-
- // the status should have an ID by now, but just in case it doesn't let's generate one here
- // because we'll need it further down
- if status.ID == "" {
- status.ID = uuid.NewString()
- }
-
- // 1. Media attachments.
- //
- // At this point we should know:
- // * the media type of the file we're looking for (a.File.ContentType)
- // * the blurhash (a.Blurhash)
- // * the file type (a.Type)
- // * the remote URL (a.RemoteURL)
- // This should be enough to pass along to the media processor.
- attachmentIDs := []string{}
- for _, a := range status.GTSMediaAttachments {
- l.Debugf("dereferencing attachment: %+v", a)
-
- // it might have been processed elsewhere so check first if it's already in the database or not
- maybeAttachment := >smodel.MediaAttachment{}
- err := p.db.GetWhere([]db.Where{{Key: "remote_url", Value: a.RemoteURL}}, maybeAttachment)
- if err == nil {
- // we already have it in the db, dereferenced, no need to do it again
- l.Debugf("attachment already exists with id %s", maybeAttachment.ID)
- attachmentIDs = append(attachmentIDs, maybeAttachment.ID)
- continue
- }
- if _, ok := err.(db.ErrNoEntries); !ok {
- // we have a real error
- return fmt.Errorf("error checking db for existence of attachment with remote url %s: %s", a.RemoteURL, err)
- }
- // it just doesn't exist yet so carry on
- l.Debug("attachment doesn't exist yet, calling ProcessRemoteAttachment", a)
- deferencedAttachment, err := p.mediaHandler.ProcessRemoteAttachment(t, a, status.AccountID)
- if err != nil {
- p.log.Errorf("error dereferencing status attachment: %s", err)
- continue
- }
- l.Debugf("dereferenced attachment: %+v", deferencedAttachment)
- deferencedAttachment.StatusID = status.ID
- deferencedAttachment.Description = a.Description
- if err := p.db.Put(deferencedAttachment); err != nil {
- return fmt.Errorf("error inserting dereferenced attachment with remote url %s: %s", a.RemoteURL, err)
- }
- attachmentIDs = append(attachmentIDs, deferencedAttachment.ID)
- }
- status.Attachments = attachmentIDs
-
- // 2. Hashtags
-
- // 3. Emojis
-
- // 4. Mentions
- // At this point, mentions should have the namestring and mentionedAccountURI set on them.
- //
- // We should dereference any accounts mentioned here which we don't have in our db yet, by their URI.
- mentions := []string{}
- for _, m := range status.GTSMentions {
- uri, err := url.Parse(m.MentionedAccountURI)
- if err != nil {
- l.Debugf("error parsing mentioned account uri %s: %s", m.MentionedAccountURI, err)
- continue
- }
-
- m.StatusID = status.ID
- m.OriginAccountID = status.GTSAuthorAccount.ID
- m.OriginAccountURI = status.GTSAuthorAccount.URI
-
- targetAccount := >smodel.Account{}
- if err := p.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, targetAccount); err != nil {
- // proper error
- if _, ok := err.(db.ErrNoEntries); !ok {
- return fmt.Errorf("db error checking for account with uri %s", uri.String())
- }
-
- // we just don't have it yet, so we should go get it....
- accountable, err := p.federator.DereferenceRemoteAccount(requestingUsername, uri)
- if err != nil {
- // we can't dereference it so just skip it
- l.Debugf("error dereferencing remote account with uri %s: %s", uri.String(), err)
- continue
- }
-
- targetAccount, err = p.tc.ASRepresentationToAccount(accountable, false)
- if err != nil {
- l.Debugf("error converting remote account with uri %s into gts model: %s", uri.String(), err)
- continue
- }
-
- if err := p.db.Put(targetAccount); err != nil {
- return fmt.Errorf("db error inserting account with uri %s", uri.String())
- }
- }
-
- // by this point, we know the targetAccount exists in our database with an ID :)
- m.TargetAccountID = targetAccount.ID
- if err := p.db.Put(m); err != nil {
- return fmt.Errorf("error creating mention: %s", err)
- }
- mentions = append(mentions, m.ID)
- }
- status.Mentions = mentions
-
- return nil
-}
-
-func (p *processor) dereferenceAccountFields(account *gtsmodel.Account, requestingUsername string, refresh bool) error {
- l := p.log.WithFields(logrus.Fields{
- "func": "dereferenceAccountFields",
- "requestingUsername": requestingUsername,
- })
-
- t, err := p.federator.GetTransportForUser(requestingUsername)
- if err != nil {
- return fmt.Errorf("error getting transport for user: %s", err)
- }
-
- // fetch the header and avatar
- if err := p.fetchHeaderAndAviForAccount(account, t, refresh); err != nil {
- // if this doesn't work, just skip it -- we can do it later
- l.Debugf("error fetching header/avi for account: %s", err)
- }
-
- if err := p.db.UpdateByID(account.ID, account); err != nil {
- return fmt.Errorf("error updating account in database: %s", err)
- }
-
- return nil
-}
-
-func (p *processor) dereferenceAnnounce(announce *gtsmodel.Status, requestingUsername string) error {
- if announce.GTSBoostedStatus == nil || announce.GTSBoostedStatus.URI == "" {
- // we can't do anything unfortunately
- return errors.New("dereferenceAnnounce: no URI to dereference")
- }
-
- // check if we already have the boosted status in the database
- boostedStatus := >smodel.Status{}
- err := p.db.GetWhere([]db.Where{{Key: "uri", Value: announce.GTSBoostedStatus.URI}}, boostedStatus)
- if err == nil {
- // nice, we already have it so we don't actually need to dereference it from remote
- announce.Content = boostedStatus.Content
- announce.ContentWarning = boostedStatus.ContentWarning
- announce.ActivityStreamsType = boostedStatus.ActivityStreamsType
- announce.Sensitive = boostedStatus.Sensitive
- announce.Language = boostedStatus.Language
- announce.Text = boostedStatus.Text
- announce.BoostOfID = boostedStatus.ID
- announce.Visibility = boostedStatus.Visibility
- announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced
- announce.GTSBoostedStatus = boostedStatus
- return nil
- }
-
- // we don't have it so we need to dereference it
- remoteStatusID, err := url.Parse(announce.GTSBoostedStatus.URI)
- if err != nil {
- return fmt.Errorf("dereferenceAnnounce: error parsing url %s: %s", announce.GTSBoostedStatus.URI, err)
- }
-
- statusable, err := p.federator.DereferenceRemoteStatus(requestingUsername, remoteStatusID)
- if err != nil {
- return fmt.Errorf("dereferenceAnnounce: error dereferencing remote status with id %s: %s", announce.GTSBoostedStatus.URI, err)
- }
-
- // make sure we have the author account in the db
- attributedToProp := statusable.GetActivityStreamsAttributedTo()
- for iter := attributedToProp.Begin(); iter != attributedToProp.End(); iter = iter.Next() {
- accountURI := iter.GetIRI()
- if accountURI == nil {
- continue
- }
-
- if err := p.db.GetWhere([]db.Where{{Key: "uri", Value: accountURI.String()}}, >smodel.Account{}); err == nil {
- // we already have it, fine
- continue
- }
-
- // we don't have the boosted status author account yet so dereference it
- accountable, err := p.federator.DereferenceRemoteAccount(requestingUsername, accountURI)
- if err != nil {
- return fmt.Errorf("dereferenceAnnounce: error dereferencing remote account with id %s: %s", accountURI.String(), err)
- }
- account, err := p.tc.ASRepresentationToAccount(accountable, false)
- if err != nil {
- return fmt.Errorf("dereferenceAnnounce: error converting dereferenced account with id %s into account : %s", accountURI.String(), err)
- }
-
- // insert the dereferenced account so it gets an ID etc
- if err := p.db.Put(account); err != nil {
- return fmt.Errorf("dereferenceAnnounce: error putting dereferenced account with id %s into database : %s", accountURI.String(), err)
- }
-
- if err := p.dereferenceAccountFields(account, requestingUsername, false); err != nil {
- return fmt.Errorf("dereferenceAnnounce: error dereferencing fields on account with id %s : %s", accountURI.String(), err)
- }
- }
-
- // now convert the statusable into something we can understand
- boostedStatus, err = p.tc.ASStatusToStatus(statusable)
- if err != nil {
- return fmt.Errorf("dereferenceAnnounce: error converting dereferenced statusable with id %s into status : %s", announce.GTSBoostedStatus.URI, err)
- }
-
- // put it in the db already so it gets an ID generated for it
- if err := p.db.Put(boostedStatus); err != nil {
- return fmt.Errorf("dereferenceAnnounce: error putting dereferenced status with id %s into the db: %s", announce.GTSBoostedStatus.URI, err)
- }
-
- // now dereference additional fields straight away (we're already async here so we have time)
- if err := p.dereferenceStatusFields(boostedStatus, requestingUsername); err != nil {
- return fmt.Errorf("dereferenceAnnounce: error dereferencing status fields for status with id %s: %s", announce.GTSBoostedStatus.URI, err)
- }
-
- // update with the newly dereferenced fields
- if err := p.db.UpdateByID(boostedStatus.ID, boostedStatus); err != nil {
- return fmt.Errorf("dereferenceAnnounce: error updating dereferenced status in the db: %s", err)
- }
-
- // we have everything we need!
- announce.Content = boostedStatus.Content
- announce.ContentWarning = boostedStatus.ContentWarning
- announce.ActivityStreamsType = boostedStatus.ActivityStreamsType
- announce.Sensitive = boostedStatus.Sensitive
- announce.Language = boostedStatus.Language
- announce.Text = boostedStatus.Text
- announce.BoostOfID = boostedStatus.ID
- announce.Visibility = boostedStatus.Visibility
- announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced
- announce.GTSBoostedStatus = boostedStatus
- return nil
-}
diff --git a/internal/message/frprocess.go b/internal/message/frprocess.go
@@ -1,90 +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 message
-
-import (
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
-)
-
-func (p *processor) FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, ErrorWithCode) {
- frs := []gtsmodel.FollowRequest{}
- if err := p.db.GetFollowRequestsForAccountID(auth.Account.ID, &frs); err != nil {
- if _, ok := err.(db.ErrNoEntries); !ok {
- return nil, NewErrorInternalError(err)
- }
- }
-
- accts := []apimodel.Account{}
- for _, fr := range frs {
- acct := >smodel.Account{}
- if err := p.db.GetByID(fr.AccountID, acct); err != nil {
- return nil, NewErrorInternalError(err)
- }
- mastoAcct, err := p.tc.AccountToMastoPublic(acct)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
- accts = append(accts, *mastoAcct)
- }
- return accts, nil
-}
-
-func (p *processor) FollowRequestAccept(auth *oauth.Auth, accountID string) (*apimodel.Relationship, ErrorWithCode) {
- follow, err := p.db.AcceptFollowRequest(accountID, auth.Account.ID)
- if err != nil {
- return nil, NewErrorNotFound(err)
- }
-
- originAccount := >smodel.Account{}
- if err := p.db.GetByID(follow.AccountID, originAccount); err != nil {
- return nil, NewErrorInternalError(err)
- }
-
- targetAccount := >smodel.Account{}
- if err := p.db.GetByID(follow.TargetAccountID, targetAccount); err != nil {
- return nil, NewErrorInternalError(err)
- }
-
- p.fromClientAPI <- gtsmodel.FromClientAPI{
- APObjectType: gtsmodel.ActivityStreamsFollow,
- APActivityType: gtsmodel.ActivityStreamsAccept,
- GTSModel: follow,
- OriginAccount: originAccount,
- TargetAccount: targetAccount,
- }
-
- gtsR, err := p.db.GetRelationship(auth.Account.ID, accountID)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
-
- r, err := p.tc.RelationshipToMasto(gtsR)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
-
- return r, nil
-}
-
-func (p *processor) FollowRequestDeny(auth *oauth.Auth) ErrorWithCode {
- return nil
-}
diff --git a/internal/message/instanceprocess.go b/internal/message/instanceprocess.go
@@ -1,41 +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 message
-
-import (
- "fmt"
-
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
-)
-
-func (p *processor) InstanceGet(domain string) (*apimodel.Instance, ErrorWithCode) {
- i := >smodel.Instance{}
- if err := p.db.GetWhere([]db.Where{{Key: "domain", Value: domain}}, i); err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("db error fetching instance %s: %s", p.config.Host, err))
- }
-
- ai, err := p.tc.InstanceToMasto(i)
- if err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("error converting instance to api representation: %s", err))
- }
-
- return ai, nil
-}
diff --git a/internal/message/mediaprocess.go b/internal/message/mediaprocess.go
@@ -1,285 +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 message
-
-import (
- "bytes"
- "errors"
- "fmt"
- "io"
- "strconv"
- "strings"
-
- 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/media"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
-)
-
-func (p *processor) MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) {
- // First check this user/account is permitted to create media
- // There's no point continuing otherwise.
- //
- // TODO: move this check to the oauth.Authed function and do it for all accounts
- if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
- return nil, errors.New("not authorized to post new media")
- }
-
- // open the attachment and extract the bytes from it
- f, err := form.File.Open()
- if err != nil {
- return nil, fmt.Errorf("error opening attachment: %s", err)
- }
- buf := new(bytes.Buffer)
- size, err := io.Copy(buf, f)
- if err != nil {
- return nil, fmt.Errorf("error reading attachment: %s", err)
-
- }
- if size == 0 {
- return nil, errors.New("could not read provided attachment: size 0 bytes")
- }
-
- // allow the mediaHandler to work its magic of processing the attachment bytes, and putting them in whatever storage backend we're using
- attachment, err := p.mediaHandler.ProcessAttachment(buf.Bytes(), authed.Account.ID, "")
- if err != nil {
- return nil, fmt.Errorf("error reading attachment: %s", err)
- }
-
- // now we need to add extra fields that the attachment processor doesn't know (from the form)
- // TODO: handle this inside mediaHandler.ProcessAttachment (just pass more params to it)
-
- // first description
- attachment.Description = form.Description
-
- // now parse the focus parameter
- focusx, focusy, err := parseFocus(form.Focus)
- if err != nil {
- return nil, err
- }
- attachment.FileMeta.Focus.X = focusx
- attachment.FileMeta.Focus.Y = focusy
-
- // prepare the frontend representation now -- if there are any errors here at least we can bail without
- // having already put something in the database and then having to clean it up again (eugh)
- mastoAttachment, err := p.tc.AttachmentToMasto(attachment)
- if err != nil {
- return nil, fmt.Errorf("error parsing media attachment to frontend type: %s", err)
- }
-
- // now we can confidently put the attachment in the database
- if err := p.db.Put(attachment); err != nil {
- return nil, fmt.Errorf("error storing media attachment in db: %s", err)
- }
-
- return &mastoAttachment, nil
-}
-
-func (p *processor) MediaGet(authed *oauth.Auth, mediaAttachmentID string) (*apimodel.Attachment, ErrorWithCode) {
- attachment := >smodel.MediaAttachment{}
- if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- // attachment doesn't exist
- return nil, NewErrorNotFound(errors.New("attachment doesn't exist in the db"))
- }
- return nil, NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err))
- }
-
- if attachment.AccountID != authed.Account.ID {
- return nil, NewErrorNotFound(errors.New("attachment not owned by requesting account"))
- }
-
- a, err := p.tc.AttachmentToMasto(attachment)
- if err != nil {
- return nil, NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err))
- }
-
- return &a, nil
-}
-
-func (p *processor) MediaUpdate(authed *oauth.Auth, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, ErrorWithCode) {
- attachment := >smodel.MediaAttachment{}
- if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- // attachment doesn't exist
- return nil, NewErrorNotFound(errors.New("attachment doesn't exist in the db"))
- }
- return nil, NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err))
- }
-
- if attachment.AccountID != authed.Account.ID {
- return nil, NewErrorNotFound(errors.New("attachment not owned by requesting account"))
- }
-
- if form.Description != nil {
- attachment.Description = *form.Description
- if err := p.db.UpdateByID(mediaAttachmentID, attachment); err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("database error updating description: %s", err))
- }
- }
-
- if form.Focus != nil {
- focusx, focusy, err := parseFocus(*form.Focus)
- if err != nil {
- return nil, NewErrorBadRequest(err)
- }
- attachment.FileMeta.Focus.X = focusx
- attachment.FileMeta.Focus.Y = focusy
- if err := p.db.UpdateByID(mediaAttachmentID, attachment); err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("database error updating focus: %s", err))
- }
- }
-
- a, err := p.tc.AttachmentToMasto(attachment)
- if err != nil {
- return nil, NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err))
- }
-
- return &a, nil
-}
-
-func (p *processor) FileGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) {
- // parse the form fields
- mediaSize, err := media.ParseMediaSize(form.MediaSize)
- if err != nil {
- return nil, NewErrorNotFound(fmt.Errorf("media size %s not valid", form.MediaSize))
- }
-
- mediaType, err := media.ParseMediaType(form.MediaType)
- if err != nil {
- return nil, NewErrorNotFound(fmt.Errorf("media type %s not valid", form.MediaType))
- }
-
- spl := strings.Split(form.FileName, ".")
- if len(spl) != 2 || spl[0] == "" || spl[1] == "" {
- return nil, NewErrorNotFound(fmt.Errorf("file name %s not parseable", form.FileName))
- }
- wantedMediaID := spl[0]
-
- // get the account that owns the media and make sure it's not suspended
- acct := >smodel.Account{}
- if err := p.db.GetByID(form.AccountID, acct); err != nil {
- return nil, NewErrorNotFound(fmt.Errorf("account with id %s could not be selected from the db: %s", form.AccountID, err))
- }
- if !acct.SuspendedAt.IsZero() {
- return nil, NewErrorNotFound(fmt.Errorf("account with id %s is suspended", form.AccountID))
- }
-
- // make sure the requesting account and the media account don't block each other
- if authed.Account != nil {
- blocked, err := p.db.Blocked(authed.Account.ID, form.AccountID)
- if err != nil {
- return nil, NewErrorNotFound(fmt.Errorf("block status could not be established between accounts %s and %s: %s", form.AccountID, authed.Account.ID, err))
- }
- if blocked {
- return nil, NewErrorNotFound(fmt.Errorf("block exists between accounts %s and %s", form.AccountID, authed.Account.ID))
- }
- }
-
- // the way we store emojis is a little different from the way we store other attachments,
- // so we need to take different steps depending on the media type being requested
- content := &apimodel.Content{}
- var storagePath string
- switch mediaType {
- case media.Emoji:
- e := >smodel.Emoji{}
- if err := p.db.GetByID(wantedMediaID, e); err != nil {
- return nil, NewErrorNotFound(fmt.Errorf("emoji %s could not be taken from the db: %s", wantedMediaID, err))
- }
- if e.Disabled {
- return nil, NewErrorNotFound(fmt.Errorf("emoji %s has been disabled", wantedMediaID))
- }
- switch mediaSize {
- case media.Original:
- content.ContentType = e.ImageContentType
- storagePath = e.ImagePath
- case media.Static:
- content.ContentType = e.ImageStaticContentType
- storagePath = e.ImageStaticPath
- default:
- return nil, NewErrorNotFound(fmt.Errorf("media size %s not recognized for emoji", mediaSize))
- }
- case media.Attachment, media.Header, media.Avatar:
- a := >smodel.MediaAttachment{}
- if err := p.db.GetByID(wantedMediaID, a); err != nil {
- return nil, NewErrorNotFound(fmt.Errorf("attachment %s could not be taken from the db: %s", wantedMediaID, err))
- }
- if a.AccountID != form.AccountID {
- return nil, NewErrorNotFound(fmt.Errorf("attachment %s is not owned by %s", wantedMediaID, form.AccountID))
- }
- switch mediaSize {
- case media.Original:
- content.ContentType = a.File.ContentType
- storagePath = a.File.Path
- case media.Small:
- content.ContentType = a.Thumbnail.ContentType
- storagePath = a.Thumbnail.Path
- default:
- return nil, NewErrorNotFound(fmt.Errorf("media size %s not recognized for attachment", mediaSize))
- }
- }
-
- bytes, err := p.storage.RetrieveFileFrom(storagePath)
- if err != nil {
- return nil, NewErrorNotFound(fmt.Errorf("error retrieving from storage: %s", err))
- }
-
- content.ContentLength = int64(len(bytes))
- content.Content = bytes
- return content, nil
-}
-
-func parseFocus(focus string) (focusx, focusy float32, err error) {
- if focus == "" {
- return
- }
- spl := strings.Split(focus, ",")
- if len(spl) != 2 {
- err = fmt.Errorf("improperly formatted focus %s", focus)
- return
- }
- xStr := spl[0]
- yStr := spl[1]
- if xStr == "" || yStr == "" {
- err = fmt.Errorf("improperly formatted focus %s", focus)
- return
- }
- fx, err := strconv.ParseFloat(xStr, 32)
- if err != nil {
- err = fmt.Errorf("improperly formatted focus %s: %s", focus, err)
- return
- }
- if fx > 1 || fx < -1 {
- err = fmt.Errorf("improperly formatted focus %s", focus)
- return
- }
- focusx = float32(fx)
- fy, err := strconv.ParseFloat(yStr, 32)
- if err != nil {
- err = fmt.Errorf("improperly formatted focus %s: %s", focus, err)
- return
- }
- if fy > 1 || fy < -1 {
- err = fmt.Errorf("improperly formatted focus %s", focus)
- return
- }
- focusy = float32(fy)
- return
-}
diff --git a/internal/message/notificationsprocess.go b/internal/message/notificationsprocess.go
@@ -1,24 +0,0 @@
-package message
-
-import (
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
-)
-
-func (p *processor) NotificationsGet(authed *oauth.Auth, limit int, maxID string) ([]*apimodel.Notification, ErrorWithCode) {
- notifs, err := p.db.GetNotificationsForAccount(authed.Account.ID, limit, maxID)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
-
- mastoNotifs := []*apimodel.Notification{}
- for _, n := range notifs {
- mastoNotif, err := p.tc.NotificationToMasto(n)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
- mastoNotifs = append(mastoNotifs, mastoNotif)
- }
-
- return mastoNotifs, nil
-}
diff --git a/internal/message/processor.go b/internal/message/processor.go
@@ -1,255 +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 message
-
-import (
- "context"
- "net/http"
-
- "github.com/sirupsen/logrus"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "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/media"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
-)
-
-// Processor should be passed to api modules (see internal/apimodule/...). It is used for
-// passing messages back and forth from the client API and the federating interface, via channels.
-// It also contains logic for filtering which messages should end up where.
-// It is designed to be used asynchronously: the client API and the federating API should just be able to
-// fire messages into the processor and not wait for a reply before proceeding with other work. This allows
-// for clean distribution of messages without slowing down the client API and harming the user experience.
-type Processor interface {
- // ToClientAPI returns a channel for putting in messages that need to go to the gts client API.
- // ToClientAPI() chan gtsmodel.ToClientAPI
- // FromClientAPI returns a channel for putting messages in that come from the client api going to the processor
- FromClientAPI() chan gtsmodel.FromClientAPI
- // ToFederator returns a channel for putting in messages that need to go to the federator (activitypub).
- // ToFederator() chan gtsmodel.ToFederator
- // FromFederator returns a channel for putting messages in that come from the federator (activitypub) going into the processor
- FromFederator() chan gtsmodel.FromFederator
- // Start starts the Processor, reading from its channels and passing messages back and forth.
- Start() error
- // Stop stops the processor cleanly, finishing handling any remaining messages before closing down.
- Stop() error
-
- /*
- CLIENT API-FACING PROCESSING FUNCTIONS
- These functions are intended to be called when the API client needs an immediate (ie., synchronous) reply
- to an HTTP request. As such, they will only do the bare-minimum of work necessary to give a properly
- formed reply. For more intensive (and time-consuming) calls, where you don't require an immediate
- response, pass work to the processor using a channel instead.
- */
-
- // AccountCreate processes the given form for creating a new account, returning an oauth token for that account if successful.
- AccountCreate(authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, error)
- // AccountGet processes the given request for account information.
- AccountGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Account, error)
- // AccountUpdate processes the update of an account with the given form
- AccountUpdate(authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error)
- // AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
- // the account given in authed.
- AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, ErrorWithCode)
- // AccountFollowersGet fetches a list of the target account's followers.
- AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode)
- // AccountFollowingGet fetches a list of the accounts that target account is following.
- AccountFollowingGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode)
- // AccountRelationshipGet returns a relationship model describing the relationship of the targetAccount to the Authed account.
- AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode)
- // AccountFollowCreate handles a follow request to an account, either remote or local.
- AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, ErrorWithCode)
- // AccountFollowRemove handles the removal of a follow/follow request to an account, either remote or local.
- AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode)
-
- // AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form.
- AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error)
-
- // AppCreate processes the creation of a new API application
- AppCreate(authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error)
-
- // 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) (*apimodel.Relationship, ErrorWithCode)
-
- // InstanceGet retrieves instance information for serving at api/v1/instance
- InstanceGet(domain string) (*apimodel.Instance, ErrorWithCode)
-
- // MediaCreate handles the creation of a media attachment, using the given form.
- MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error)
- // MediaGet handles the GET of a media attachment with the given ID
- MediaGet(authed *oauth.Auth, attachmentID string) (*apimodel.Attachment, ErrorWithCode)
- // MediaUpdate handles the PUT of a media attachment with the given ID and form
- MediaUpdate(authed *oauth.Auth, attachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, ErrorWithCode)
-
- // NotificationsGet
- NotificationsGet(authed *oauth.Auth, limit int, maxID string) ([]*apimodel.Notification, ErrorWithCode)
-
- // SearchGet performs a search with the given params, resolving/dereferencing remotely as desired
- SearchGet(authed *oauth.Auth, searchQuery *apimodel.SearchQuery) (*apimodel.SearchResult, ErrorWithCode)
-
- // StatusCreate processes the given form to create a new status, returning the api model representation of that status if it's OK.
- StatusCreate(authed *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, error)
- // StatusDelete processes the delete of a given status, returning the deleted status if the delete goes through.
- StatusDelete(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error)
- // StatusFave processes the faving of a given status, returning the updated status if the fave goes through.
- StatusFave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error)
- // StatusBoost processes the boost/reblog of a given status, returning the newly-created boost if all is well.
- StatusBoost(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, ErrorWithCode)
- // StatusFavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings.
- StatusFavedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, error)
- // StatusGet gets the given status, taking account of privacy settings and blocks etc.
- StatusGet(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error)
- // StatusUnfave processes the unfaving of a given status, returning the updated status if the fave goes through.
- StatusUnfave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error)
-
- // HomeTimelineGet returns statuses from the home timeline, with the given filters/parameters.
- HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) ([]apimodel.Status, ErrorWithCode)
-
- /*
- FEDERATION API-FACING PROCESSING FUNCTIONS
- These functions are intended to be called when the federating client needs an immediate (ie., synchronous) reply
- to an HTTP request. As such, they will only do the bare-minimum of work necessary to give a properly
- formed reply. For more intensive (and time-consuming) calls, where you don't require an immediate
- response, pass work to the processor using a channel instead.
- */
-
- // GetFediUser handles the getting of a fedi/activitypub representation of a user/account, performing appropriate authentication
- // before returning a JSON serializable interface to the caller.
- GetFediUser(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode)
-
- // GetFediFollowers handles the getting of a fedi/activitypub representation of a user/account's followers, performing appropriate
- // authentication before returning a JSON serializable interface to the caller.
- GetFediFollowers(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode)
-
- // GetFediFollowing handles the getting of a fedi/activitypub representation of a user/account's following, performing appropriate
- // authentication before returning a JSON serializable interface to the caller.
- GetFediFollowing(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode)
-
- // GetFediStatus handles the getting of a fedi/activitypub representation of a particular status, performing appropriate
- // authentication before returning a JSON serializable interface to the caller.
- GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, ErrorWithCode)
-
- // 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
-type processor struct {
- // federator pub.FederatingActor
- // toClientAPI chan gtsmodel.ToClientAPI
- fromClientAPI chan gtsmodel.FromClientAPI
- // toFederator chan gtsmodel.ToFederator
- fromFederator chan gtsmodel.FromFederator
- federator federation.Federator
- stop chan interface{}
- log *logrus.Logger
- config *config.Config
- tc typeutils.TypeConverter
- oauthServer oauth.Server
- mediaHandler media.Handler
- storage storage.Storage
- db db.DB
-}
-
-// NewProcessor returns a new Processor that uses the given federator and logger
-func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator federation.Federator, oauthServer oauth.Server, mediaHandler media.Handler, storage storage.Storage, db db.DB, log *logrus.Logger) Processor {
- return &processor{
- // toClientAPI: make(chan gtsmodel.ToClientAPI, 100),
- fromClientAPI: make(chan gtsmodel.FromClientAPI, 100),
- // toFederator: make(chan gtsmodel.ToFederator, 100),
- fromFederator: make(chan gtsmodel.FromFederator, 100),
- federator: federator,
- stop: make(chan interface{}),
- log: log,
- config: config,
- tc: tc,
- oauthServer: oauthServer,
- mediaHandler: mediaHandler,
- storage: storage,
- db: db,
- }
-}
-
-// func (p *processor) ToClientAPI() chan gtsmodel.ToClientAPI {
-// return p.toClientAPI
-// }
-
-func (p *processor) FromClientAPI() chan gtsmodel.FromClientAPI {
- return p.fromClientAPI
-}
-
-// func (p *processor) ToFederator() chan gtsmodel.ToFederator {
-// return p.toFederator
-// }
-
-func (p *processor) FromFederator() chan gtsmodel.FromFederator {
- return p.fromFederator
-}
-
-// Start starts the Processor, reading from its channels and passing messages back and forth.
-func (p *processor) Start() error {
- go func() {
- DistLoop:
- for {
- select {
- 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.fromFederator:
- p.log.Infof("received message FROM federator: %+v", federatorMsg)
- if err := p.processFromFederator(federatorMsg); err != nil {
- p.log.Error(err)
- }
- case <-p.stop:
- break DistLoop
- }
- }
- }()
- return nil
-}
-
-// Stop stops the processor cleanly, finishing handling any remaining messages before closing down.
-// TODO: empty message buffer properly before stopping otherwise we'll lose federating messages.
-func (p *processor) Stop() error {
- close(p.stop)
- return nil
-}
diff --git a/internal/message/processorutil.go b/internal/message/processorutil.go
@@ -1,357 +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 message
-
-import (
- "bytes"
- "errors"
- "fmt"
- "io"
- "mime/multipart"
-
- 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/media"
- "github.com/superseriousbusiness/gotosocial/internal/transport"
- "github.com/superseriousbusiness/gotosocial/internal/util"
-)
-
-func (p *processor) processVisibility(form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error {
- // by default all flags are set to true
- gtsAdvancedVis := >smodel.VisibilityAdvanced{
- Federated: true,
- Boostable: true,
- Replyable: true,
- Likeable: true,
- }
-
- var gtsBasicVis gtsmodel.Visibility
- // Advanced takes priority if it's set.
- // If it's not set, take whatever masto visibility is set.
- // If *that's* not set either, then just take the account default.
- // If that's also not set, take the default for the whole instance.
- if form.VisibilityAdvanced != nil {
- gtsBasicVis = gtsmodel.Visibility(*form.VisibilityAdvanced)
- } else if form.Visibility != "" {
- gtsBasicVis = p.tc.MastoVisToVis(form.Visibility)
- } else if accountDefaultVis != "" {
- gtsBasicVis = accountDefaultVis
- } else {
- gtsBasicVis = gtsmodel.VisibilityDefault
- }
-
- switch gtsBasicVis {
- case gtsmodel.VisibilityPublic:
- // for public, there's no need to change any of the advanced flags from true regardless of what the user filled out
- break
- case gtsmodel.VisibilityUnlocked:
- // for unlocked the user can set any combination of flags they like so look at them all to see if they're set and then apply them
- if form.Federated != nil {
- gtsAdvancedVis.Federated = *form.Federated
- }
-
- if form.Boostable != nil {
- gtsAdvancedVis.Boostable = *form.Boostable
- }
-
- if form.Replyable != nil {
- gtsAdvancedVis.Replyable = *form.Replyable
- }
-
- if form.Likeable != nil {
- gtsAdvancedVis.Likeable = *form.Likeable
- }
-
- case gtsmodel.VisibilityFollowersOnly, gtsmodel.VisibilityMutualsOnly:
- // for followers or mutuals only, boostable will *always* be false, but the other fields can be set so check and apply them
- gtsAdvancedVis.Boostable = false
-
- if form.Federated != nil {
- gtsAdvancedVis.Federated = *form.Federated
- }
-
- if form.Replyable != nil {
- gtsAdvancedVis.Replyable = *form.Replyable
- }
-
- if form.Likeable != nil {
- gtsAdvancedVis.Likeable = *form.Likeable
- }
-
- case gtsmodel.VisibilityDirect:
- // direct is pretty easy: there's only one possible setting so return it
- gtsAdvancedVis.Federated = true
- gtsAdvancedVis.Boostable = false
- gtsAdvancedVis.Federated = true
- gtsAdvancedVis.Likeable = true
- }
-
- status.Visibility = gtsBasicVis
- status.VisibilityAdvanced = gtsAdvancedVis
- return nil
-}
-
-func (p *processor) processReplyToID(form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error {
- if form.InReplyToID == "" {
- return nil
- }
-
- // If this status is a reply to another status, we need to do a bit of work to establish whether or not this status can be posted:
- //
- // 1. Does the replied status exist in the database?
- // 2. Is the replied status marked as replyable?
- // 3. Does a block exist between either the current account or the account that posted the status it's replying to?
- //
- // If this is all OK, then we fetch the repliedStatus and the repliedAccount for later processing.
- repliedStatus := >smodel.Status{}
- repliedAccount := >smodel.Account{}
- // check replied status exists + is replyable
- if err := p.db.GetByID(form.InReplyToID, repliedStatus); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- return fmt.Errorf("status with id %s not replyable because it doesn't exist", form.InReplyToID)
- }
- return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
- }
-
- if repliedStatus.VisibilityAdvanced != nil {
- if !repliedStatus.VisibilityAdvanced.Replyable {
- return fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID)
- }
- }
-
- // check replied account is known to us
- if err := p.db.GetByID(repliedStatus.AccountID, repliedAccount); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- return fmt.Errorf("status with id %s not replyable because account id %s is not known", form.InReplyToID, repliedStatus.AccountID)
- }
- return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
- }
- // check if a block exists
- if blocked, err := p.db.Blocked(thisAccountID, repliedAccount.ID); err != nil {
- if _, ok := err.(db.ErrNoEntries); !ok {
- return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
- }
- } else if blocked {
- return fmt.Errorf("status with id %s not replyable", form.InReplyToID)
- }
- status.InReplyToID = repliedStatus.ID
- status.InReplyToAccountID = repliedAccount.ID
-
- return nil
-}
-
-func (p *processor) processMediaIDs(form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error {
- if form.MediaIDs == nil {
- return nil
- }
-
- gtsMediaAttachments := []*gtsmodel.MediaAttachment{}
- attachments := []string{}
- for _, mediaID := range form.MediaIDs {
- // check these attachments exist
- a := >smodel.MediaAttachment{}
- if err := p.db.GetByID(mediaID, a); err != nil {
- return fmt.Errorf("invalid media type or media not found for media id %s", mediaID)
- }
- // check they belong to the requesting account id
- if a.AccountID != thisAccountID {
- return fmt.Errorf("media with id %s does not belong to account %s", mediaID, thisAccountID)
- }
- // check they're not already used in a status
- if a.StatusID != "" || a.ScheduledStatusID != "" {
- return fmt.Errorf("media with id %s is already attached to a status", mediaID)
- }
- gtsMediaAttachments = append(gtsMediaAttachments, a)
- attachments = append(attachments, a.ID)
- }
- status.GTSMediaAttachments = gtsMediaAttachments
- status.Attachments = attachments
- return nil
-}
-
-func (p *processor) processLanguage(form *apimodel.AdvancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error {
- if form.Language != "" {
- status.Language = form.Language
- } else {
- status.Language = accountDefaultLanguage
- }
- if status.Language == "" {
- return errors.New("no language given either in status create form or account default")
- }
- return nil
-}
-
-func (p *processor) processMentions(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
- menchies := []string{}
- 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)
- }
- for _, menchie := range gtsMenchies {
- if err := p.db.Put(menchie); err != nil {
- return fmt.Errorf("error putting mentions in db: %s", err)
- }
- menchies = append(menchies, menchie.ID)
- }
- // add full populated gts menchies to the status for passing them around conveniently
- status.GTSMentions = gtsMenchies
- // add just the ids of the mentioned accounts to the status for putting in the db
- status.Mentions = menchies
- return nil
-}
-
-func (p *processor) processTags(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
- tags := []string{}
- 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)
- }
- for _, tag := range gtsTags {
- if err := p.db.Upsert(tag, "name"); err != nil {
- return fmt.Errorf("error putting tags in db: %s", err)
- }
- tags = append(tags, tag.ID)
- }
- // add full populated gts tags to the status for passing them around conveniently
- status.GTSTags = gtsTags
- // add just the ids of the used tags to the status for putting in the db
- status.Tags = tags
- return nil
-}
-
-func (p *processor) processEmojis(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
- emojis := []string{}
- 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)
- }
- for _, e := range gtsEmojis {
- emojis = append(emojis, e.ID)
- }
- // add full populated gts emojis to the status for passing them around conveniently
- status.GTSEmojis = gtsEmojis
- // add just the ids of the used emojis to the status for putting in the db
- status.Emojis = emojis
- return nil
-}
-
-/*
- HELPER FUNCTIONS
-*/
-
-// TODO: try to combine the below two functions because this is a lot of code repetition.
-
-// updateAccountAvatar does the dirty work of checking the avatar part of an account update form,
-// parsing and checking the image, and doing the necessary updates in the database for this to become
-// the account's new avatar image.
-func (p *processor) updateAccountAvatar(avatar *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) {
- var err error
- if int(avatar.Size) > p.config.MediaConfig.MaxImageSize {
- err = fmt.Errorf("avatar with size %d exceeded max image size of %d bytes", avatar.Size, p.config.MediaConfig.MaxImageSize)
- return nil, err
- }
- f, err := avatar.Open()
- if err != nil {
- return nil, fmt.Errorf("could not read provided avatar: %s", err)
- }
-
- // extract the bytes
- buf := new(bytes.Buffer)
- size, err := io.Copy(buf, f)
- if err != nil {
- return nil, fmt.Errorf("could not read provided avatar: %s", err)
- }
- if size == 0 {
- return nil, errors.New("could not read provided avatar: size 0 bytes")
- }
-
- // do the setting
- avatarInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Avatar, "")
- if err != nil {
- return nil, fmt.Errorf("error processing avatar: %s", err)
- }
-
- return avatarInfo, f.Close()
-}
-
-// updateAccountHeader does the dirty work of checking the header part of an account update form,
-// parsing and checking the image, and doing the necessary updates in the database for this to become
-// the account's new header image.
-func (p *processor) updateAccountHeader(header *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) {
- var err error
- if int(header.Size) > p.config.MediaConfig.MaxImageSize {
- err = fmt.Errorf("header with size %d exceeded max image size of %d bytes", header.Size, p.config.MediaConfig.MaxImageSize)
- return nil, err
- }
- f, err := header.Open()
- if err != nil {
- return nil, fmt.Errorf("could not read provided header: %s", err)
- }
-
- // extract the bytes
- buf := new(bytes.Buffer)
- size, err := io.Copy(buf, f)
- if err != nil {
- return nil, fmt.Errorf("could not read provided header: %s", err)
- }
- if size == 0 {
- return nil, errors.New("could not read provided header: size 0 bytes")
- }
-
- // do the setting
- headerInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Header, "")
- if err != nil {
- return nil, fmt.Errorf("error processing header: %s", err)
- }
-
- return headerInfo, f.Close()
-}
-
-// fetchHeaderAndAviForAccount fetches the header and avatar for a remote account, using a transport
-// on behalf of requestingUsername.
-//
-// targetAccount's AvatarMediaAttachmentID and HeaderMediaAttachmentID will be updated as necessary.
-//
-// SIDE EFFECTS: remote header and avatar will be stored in local storage, and the database will be updated
-// to reflect the creation of these new attachments.
-func (p *processor) fetchHeaderAndAviForAccount(targetAccount *gtsmodel.Account, t transport.Transport, refresh bool) error {
- if targetAccount.AvatarRemoteURL != "" && (targetAccount.AvatarMediaAttachmentID == "" || refresh) {
- a, err := p.mediaHandler.ProcessRemoteHeaderOrAvatar(t, >smodel.MediaAttachment{
- RemoteURL: targetAccount.AvatarRemoteURL,
- Avatar: true,
- }, targetAccount.ID)
- if err != nil {
- return fmt.Errorf("error processing avatar for user: %s", err)
- }
- targetAccount.AvatarMediaAttachmentID = a.ID
- }
-
- if targetAccount.HeaderRemoteURL != "" && (targetAccount.HeaderMediaAttachmentID == "" || refresh) {
- a, err := p.mediaHandler.ProcessRemoteHeaderOrAvatar(t, >smodel.MediaAttachment{
- RemoteURL: targetAccount.HeaderRemoteURL,
- Header: true,
- }, targetAccount.ID)
- if err != nil {
- return fmt.Errorf("error processing header for user: %s", err)
- }
- targetAccount.HeaderMediaAttachmentID = a.ID
- }
- return nil
-}
diff --git a/internal/message/searchprocess.go b/internal/message/searchprocess.go
@@ -1,295 +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 message
-
-import (
- "errors"
- "fmt"
- "net/url"
- "strings"
-
- "github.com/sirupsen/logrus"
- 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"
- "github.com/superseriousbusiness/gotosocial/internal/util"
-)
-
-func (p *processor) SearchGet(authed *oauth.Auth, searchQuery *apimodel.SearchQuery) (*apimodel.SearchResult, ErrorWithCode) {
- l := p.log.WithFields(logrus.Fields{
- "func": "SearchGet",
- "query": searchQuery.Query,
- })
-
- results := &apimodel.SearchResult{
- Accounts: []apimodel.Account{},
- Statuses: []apimodel.Status{},
- Hashtags: []apimodel.Tag{},
- }
- foundAccounts := []*gtsmodel.Account{}
- foundStatuses := []*gtsmodel.Status{}
- // foundHashtags := []*gtsmodel.Tag{}
-
- // convert the query to lowercase and trim leading/trailing spaces
- query := strings.ToLower(strings.TrimSpace(searchQuery.Query))
-
- var foundOne bool
- // check if the query is something like @whatever_username@example.org -- this means it's a remote account
- if !foundOne && util.IsMention(searchQuery.Query) {
- l.Debug("search term is a mention, looking it up...")
- foundAccount, err := p.searchAccountByMention(authed, searchQuery.Query, searchQuery.Resolve)
- if err == nil && foundAccount != nil {
- foundAccounts = append(foundAccounts, foundAccount)
- foundOne = true
- l.Debug("got an account by searching by mention")
- }
- }
-
- // check if the query is a URI and just do a lookup for that, straight up
- if uri, err := url.Parse(query); err == nil && !foundOne {
- // 1. check if it's a status
- if foundStatus, err := p.searchStatusByURI(authed, uri, searchQuery.Resolve); err == nil && foundStatus != nil {
- foundStatuses = append(foundStatuses, foundStatus)
- foundOne = true
- l.Debug("got a status by searching by URI")
- }
-
- // 2. check if it's an account
- if foundAccount, err := p.searchAccountByURI(authed, uri, searchQuery.Resolve); err == nil && foundAccount != nil {
- foundAccounts = append(foundAccounts, foundAccount)
- foundOne = true
- l.Debug("got an account by searching by URI")
- }
- }
-
- if !foundOne {
- // we haven't found anything yet so search for text now
- l.Debug("nothing found by mention or by URI, will fall back to searching by text now")
- }
-
- /*
- FROM HERE ON we have our search results, it's just a matter of filtering them according to what this user is allowed to see,
- and then converting them into our frontend format.
- */
- for _, foundAccount := range foundAccounts {
- // make sure there's no block in either direction between the account and the requester
- if blocked, err := p.db.Blocked(authed.Account.ID, foundAccount.ID); err == nil && !blocked {
- // all good, convert it and add it to the results
- if acctMasto, err := p.tc.AccountToMastoPublic(foundAccount); err == nil && acctMasto != nil {
- results.Accounts = append(results.Accounts, *acctMasto)
- }
- }
- }
-
- for _, foundStatus := range foundStatuses {
- statusOwner := >smodel.Account{}
- if err := p.db.GetByID(foundStatus.AccountID, statusOwner); err != nil {
- continue
- }
-
- relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(foundStatus)
- if err != nil {
- continue
- }
- if visible, err := p.db.StatusVisible(foundStatus, statusOwner, authed.Account, relevantAccounts); !visible || err != nil {
- continue
- }
-
- statusMasto, err := p.tc.StatusToMasto(foundStatus, statusOwner, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, nil)
- if err != nil {
- continue
- }
-
- results.Statuses = append(results.Statuses, *statusMasto)
- }
-
- return results, nil
-}
-
-func (p *processor) searchStatusByURI(authed *oauth.Auth, uri *url.URL, resolve bool) (*gtsmodel.Status, error) {
-
- maybeStatus := >smodel.Status{}
- if err := p.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String(), CaseInsensitive: true}}, maybeStatus); err == nil {
- // we have it and it's a status
- return maybeStatus, nil
- } else if err := p.db.GetWhere([]db.Where{{Key: "url", Value: uri.String(), CaseInsensitive: true}}, maybeStatus); err == nil {
- // we have it and it's a status
- return maybeStatus, nil
- }
-
- // we don't have it locally so dereference it if we're allowed to
- if resolve {
- statusable, err := p.federator.DereferenceRemoteStatus(authed.Account.Username, uri)
- if err == nil {
- // it IS a status!
-
- // extract the status owner's IRI from the statusable
- var statusOwnerURI *url.URL
- statusAttributedTo := statusable.GetActivityStreamsAttributedTo()
- for i := statusAttributedTo.Begin(); i != statusAttributedTo.End(); i = i.Next() {
- if i.IsIRI() {
- statusOwnerURI = i.GetIRI()
- break
- }
- }
- if statusOwnerURI == nil {
- return nil, errors.New("couldn't extract ownerAccountURI from statusable")
- }
-
- // make sure the status owner exists in the db by searching for it
- _, err := p.searchAccountByURI(authed, statusOwnerURI, resolve)
- if err != nil {
- return nil, err
- }
-
- // we have the status owner, we have the dereferenced status, so now we should finish dereferencing the status properly
-
- // first turn it into a gtsmodel.Status
- status, err := p.tc.ASStatusToStatus(statusable)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
-
- // put it in the DB so it gets a UUID
- if err := p.db.Put(status); err != nil {
- return nil, fmt.Errorf("error putting status in the db: %s", err)
- }
-
- // properly dereference everything in the status (media attachments etc)
- if err := p.dereferenceStatusFields(status, authed.Account.Username); err != nil {
- return nil, fmt.Errorf("error dereferencing status fields: %s", err)
- }
-
- // update with the nicely dereferenced status
- if err := p.db.UpdateByID(status.ID, status); err != nil {
- return nil, fmt.Errorf("error updating status in the db: %s", err)
- }
-
- return status, nil
- }
- }
- return nil, nil
-}
-
-func (p *processor) searchAccountByURI(authed *oauth.Auth, uri *url.URL, resolve bool) (*gtsmodel.Account, error) {
- maybeAccount := >smodel.Account{}
- if err := p.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String(), CaseInsensitive: true}}, maybeAccount); err == nil {
- // we have it and it's an account
- return maybeAccount, nil
- } else if err = p.db.GetWhere([]db.Where{{Key: "url", Value: uri.String(), CaseInsensitive: true}}, maybeAccount); err == nil {
- // we have it and it's an account
- return maybeAccount, nil
- }
- if resolve {
- // we don't have it locally so try and dereference it
- accountable, err := p.federator.DereferenceRemoteAccount(authed.Account.Username, uri)
- if err != nil {
- return nil, fmt.Errorf("searchAccountByURI: error dereferencing account with uri %s: %s", uri.String(), err)
- }
-
- // it IS an account!
- account, err := p.tc.ASRepresentationToAccount(accountable, false)
- if err != nil {
- return nil, fmt.Errorf("searchAccountByURI: error dereferencing account with uri %s: %s", uri.String(), err)
- }
-
- if err := p.db.Put(account); err != nil {
- return nil, fmt.Errorf("searchAccountByURI: error inserting account with uri %s: %s", uri.String(), err)
- }
-
- if err := p.dereferenceAccountFields(account, authed.Account.Username, false); err != nil {
- return nil, fmt.Errorf("searchAccountByURI: error further dereferencing account with uri %s: %s", uri.String(), err)
- }
-
- return account, nil
- }
- return nil, nil
-}
-
-func (p *processor) searchAccountByMention(authed *oauth.Auth, mention string, resolve bool) (*gtsmodel.Account, error) {
- // query is for a remote account
- username, domain, err := util.ExtractMentionParts(mention)
- if err != nil {
- return nil, fmt.Errorf("searchAccountByMention: error extracting mention parts: %s", err)
- }
-
- // if it's a local account we can skip a whole bunch of stuff
- maybeAcct := >smodel.Account{}
- if domain == p.config.Host {
- if err = p.db.GetLocalAccountByUsername(username, maybeAcct); err != nil {
- return nil, fmt.Errorf("searchAccountByMention: error getting local account by username: %s", err)
- }
- return maybeAcct, nil
- }
-
- // it's not a local account so first we'll check if it's in the database already...
- where := []db.Where{
- {Key: "username", Value: username, CaseInsensitive: true},
- {Key: "domain", Value: domain, CaseInsensitive: true},
- }
- err = p.db.GetWhere(where, maybeAcct)
- if err == nil {
- // we've got it stored locally already!
- return maybeAcct, nil
- }
-
- if _, ok := err.(db.ErrNoEntries); !ok {
- // if it's not errNoEntries there's been a real database error so bail at this point
- return nil, fmt.Errorf("searchAccountByMention: database error: %s", err)
- }
-
- // we got a db.ErrNoEntries, so we just don't have the account locally stored -- check if we can dereference it
- if resolve {
- // we're allowed to resolve it so let's try
-
- // first we need to webfinger the remote account to convert the username and domain into the activitypub URI for the account
- acctURI, err := p.federator.FingerRemoteAccount(authed.Account.Username, username, domain)
- if err != nil {
- // something went wrong doing the webfinger lookup so we can't process the request
- return nil, fmt.Errorf("searchAccountByMention: error fingering remote account with username %s and domain %s: %s", username, domain, err)
- }
-
- // dereference the account based on the URI we retrieved from the webfinger lookup
- accountable, err := p.federator.DereferenceRemoteAccount(authed.Account.Username, acctURI)
- if err != nil {
- // something went wrong doing the dereferencing so we can't process the request
- return nil, fmt.Errorf("searchAccountByMention: error dereferencing remote account with uri %s: %s", acctURI.String(), err)
- }
-
- // convert the dereferenced account to the gts model of that account
- foundAccount, err := p.tc.ASRepresentationToAccount(accountable, false)
- if err != nil {
- // something went wrong doing the conversion to a gtsmodel.Account so we can't process the request
- return nil, fmt.Errorf("searchAccountByMention: error converting account with uri %s: %s", acctURI.String(), err)
- }
-
- // put this new account in our database
- if err := p.db.Put(foundAccount); err != nil {
- return nil, fmt.Errorf("searchAccountByMention: error inserting account with uri %s: %s", acctURI.String(), err)
- }
-
- // properly dereference all the fields on the account immediately
- if err := p.dereferenceAccountFields(foundAccount, authed.Account.Username, true); err != nil {
- return nil, fmt.Errorf("searchAccountByMention: error dereferencing fields on account with uri %s: %s", acctURI.String(), err)
- }
- }
-
- return nil, nil
-}
diff --git a/internal/message/statusprocess.go b/internal/message/statusprocess.go
@@ -1,481 +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 message
-
-import (
- "errors"
- "fmt"
- "time"
-
- "github.com/google/uuid"
- 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"
- "github.com/superseriousbusiness/gotosocial/internal/util"
-)
-
-func (p *processor) StatusCreate(auth *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, error) {
- uris := util.GenerateURIsForAccount(auth.Account.Username, p.config.Protocol, p.config.Host)
- thisStatusID := uuid.NewString()
- thisStatusURI := fmt.Sprintf("%s/%s", uris.StatusesURI, thisStatusID)
- thisStatusURL := fmt.Sprintf("%s/%s", uris.StatusesURL, thisStatusID)
- newStatus := >smodel.Status{
- ID: thisStatusID,
- URI: thisStatusURI,
- URL: thisStatusURL,
- Content: util.HTMLFormat(form.Status),
- CreatedAt: time.Now(),
- UpdatedAt: time.Now(),
- Local: true,
- AccountID: auth.Account.ID,
- ContentWarning: form.SpoilerText,
- ActivityStreamsType: gtsmodel.ActivityStreamsNote,
- Sensitive: form.Sensitive,
- Language: form.Language,
- CreatedWithApplicationID: auth.Application.ID,
- Text: form.Status,
- }
-
- // check if replyToID is ok
- if err := p.processReplyToID(form, auth.Account.ID, newStatus); err != nil {
- return nil, err
- }
-
- // check if mediaIDs are ok
- if err := p.processMediaIDs(form, auth.Account.ID, newStatus); err != nil {
- return nil, err
- }
-
- // check if visibility settings are ok
- if err := p.processVisibility(form, auth.Account.Privacy, newStatus); err != nil {
- return nil, err
- }
-
- // handle language settings
- if err := p.processLanguage(form, auth.Account.Language, newStatus); err != nil {
- return nil, err
- }
-
- // handle mentions
- if err := p.processMentions(form, auth.Account.ID, newStatus); err != nil {
- return nil, err
- }
-
- if err := p.processTags(form, auth.Account.ID, newStatus); err != nil {
- return nil, err
- }
-
- if err := p.processEmojis(form, auth.Account.ID, newStatus); err != nil {
- return nil, err
- }
-
- // put the new status in the database, generating an ID for it in the process
- if err := p.db.Put(newStatus); err != nil {
- return nil, err
- }
-
- // change the status ID of the media attachments to the new status
- for _, a := range newStatus.GTSMediaAttachments {
- a.StatusID = newStatus.ID
- a.UpdatedAt = time.Now()
- if err := p.db.UpdateByID(a.ID, a); err != nil {
- return nil, err
- }
- }
-
- // put the new status in the appropriate channel for async processing
- p.fromClientAPI <- gtsmodel.FromClientAPI{
- APObjectType: newStatus.ActivityStreamsType,
- APActivityType: gtsmodel.ActivityStreamsCreate,
- GTSModel: 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)
-}
-
-func (p *processor) StatusDelete(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) {
- l := p.log.WithField("func", "StatusDelete")
- l.Tracef("going to search for target status %s", targetStatusID)
- targetStatus := >smodel.Status{}
- if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
- return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err)
- }
-
- if targetStatus.AccountID != authed.Account.ID {
- return nil, errors.New("status doesn't belong to requesting account")
- }
-
- l.Trace("going to get relevant accounts")
- relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
- if err != nil {
- return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
- }
-
- var boostOfStatus *gtsmodel.Status
- if targetStatus.BoostOfID != "" {
- boostOfStatus = >smodel.Status{}
- if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
- return nil, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)
- }
- }
-
- mastoStatus, err := p.tc.StatusToMasto(targetStatus, authed.Account, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)
- if err != nil {
- return nil, fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)
- }
-
- if err := p.db.DeleteByID(targetStatus.ID, targetStatus); err != nil {
- return nil, fmt.Errorf("error deleting status from the database: %s", err)
- }
-
- return mastoStatus, nil
-}
-
-func (p *processor) StatusFave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) {
- l := p.log.WithField("func", "StatusFave")
- l.Tracef("going to search for target status %s", targetStatusID)
- targetStatus := >smodel.Status{}
- if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
- return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err)
- }
-
- l.Tracef("going to search for target account %s", targetStatus.AccountID)
- targetAccount := >smodel.Account{}
- if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
- return nil, fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)
- }
-
- l.Trace("going to get relevant accounts")
- relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
- if err != nil {
- return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
- }
-
- var boostOfStatus *gtsmodel.Status
- if targetStatus.BoostOfID != "" {
- boostOfStatus = >smodel.Status{}
- if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
- return nil, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)
- }
- }
-
- l.Trace("going to see if status is visible")
- visible, err := p.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
- if err != nil {
- return nil, fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)
- }
-
- if !visible {
- return nil, errors.New("status is not visible")
- }
-
- // is the status faveable?
- if targetStatus.VisibilityAdvanced != nil {
- if !targetStatus.VisibilityAdvanced.Likeable {
- return nil, errors.New("status is not faveable")
- }
- }
-
- // first check if the status is already faved, if so we don't need to do anything
- newFave := true
- gtsFave := >smodel.Status{}
- if err := p.db.GetWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: authed.Account.ID}}, gtsFave); err == nil {
- // we already have a fave for this status
- newFave = false
- }
-
- if newFave {
- thisFaveID := uuid.NewString()
-
- // we need to create a new fave in the database
- gtsFave := >smodel.StatusFave{
- ID: thisFaveID,
- AccountID: authed.Account.ID,
- TargetAccountID: targetAccount.ID,
- StatusID: targetStatus.ID,
- URI: util.GenerateURIForLike(authed.Account.Username, p.config.Protocol, p.config.Host, thisFaveID),
- GTSStatus: targetStatus,
- GTSTargetAccount: targetAccount,
- GTSFavingAccount: authed.Account,
- }
-
- if err := p.db.Put(gtsFave); err != nil {
- return nil, err
- }
-
- // send the new fave through the processor channel for federation etc
- p.fromClientAPI <- gtsmodel.FromClientAPI{
- APObjectType: gtsmodel.ActivityStreamsLike,
- APActivityType: gtsmodel.ActivityStreamsCreate,
- GTSModel: gtsFave,
- OriginAccount: authed.Account,
- TargetAccount: targetAccount,
- }
- }
-
- // return the mastodon representation of the target status
- mastoStatus, err := p.tc.StatusToMasto(targetStatus, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)
- if err != nil {
- return nil, fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)
- }
-
- return mastoStatus, nil
-}
-
-func (p *processor) StatusBoost(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, ErrorWithCode) {
- l := p.log.WithField("func", "StatusBoost")
-
- l.Tracef("going to search for target status %s", targetStatusID)
- targetStatus := >smodel.Status{}
- if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
- return nil, NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
- }
-
- l.Tracef("going to search for target account %s", targetStatus.AccountID)
- targetAccount := >smodel.Account{}
- if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
- return nil, NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
- }
-
- l.Trace("going to get relevant accounts")
- relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
- if err != nil {
- return nil, NewErrorNotFound(fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err))
- }
-
- l.Trace("going to see if status is visible")
- visible, err := p.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
- if err != nil {
- return nil, NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
- }
-
- if !visible {
- return nil, NewErrorNotFound(errors.New("status is not visible"))
- }
-
- if targetStatus.VisibilityAdvanced != nil {
- if !targetStatus.VisibilityAdvanced.Boostable {
- return nil, NewErrorForbidden(errors.New("status is not boostable"))
- }
- }
-
- // it's visible! it's boostable! so let's boost the FUCK out of it
- boostWrapperStatus, err := p.tc.StatusToBoost(targetStatus, authed.Account)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
-
- boostWrapperStatus.CreatedWithApplicationID = authed.Application.ID
- boostWrapperStatus.GTSBoostedAccount = targetAccount
-
- // put the boost in the database
- if err := p.db.Put(boostWrapperStatus); err != nil {
- return nil, NewErrorInternalError(err)
- }
-
- // send it to the processor for async processing
- p.fromClientAPI <- gtsmodel.FromClientAPI{
- APObjectType: gtsmodel.ActivityStreamsAnnounce,
- APActivityType: gtsmodel.ActivityStreamsCreate,
- GTSModel: boostWrapperStatus,
- OriginAccount: authed.Account,
- TargetAccount: targetAccount,
- }
-
- // return the frontend representation of the new status to the submitter
- mastoStatus, err := p.tc.StatusToMasto(boostWrapperStatus, authed.Account, authed.Account, targetAccount, nil, targetStatus)
- if err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
- }
-
- return mastoStatus, nil
-}
-
-func (p *processor) StatusFavedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, error) {
- l := p.log.WithField("func", "StatusFavedBy")
-
- l.Tracef("going to search for target status %s", targetStatusID)
- targetStatus := >smodel.Status{}
- if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
- return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err)
- }
-
- l.Tracef("going to search for target account %s", targetStatus.AccountID)
- targetAccount := >smodel.Account{}
- if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
- return nil, fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)
- }
-
- l.Trace("going to get relevant accounts")
- relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
- if err != nil {
- return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
- }
-
- l.Trace("going to see if status is visible")
- visible, err := p.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
- if err != nil {
- return nil, fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)
- }
-
- if !visible {
- return nil, errors.New("status is not visible")
- }
-
- // get ALL accounts that faved a status -- doesn't take account of blocks and mutes and stuff
- favingAccounts, err := p.db.WhoFavedStatus(targetStatus)
- if err != nil {
- return nil, fmt.Errorf("error seeing who faved status: %s", err)
- }
-
- // filter the list so the user doesn't see accounts they blocked or which blocked them
- filteredAccounts := []*gtsmodel.Account{}
- for _, acc := range favingAccounts {
- blocked, err := p.db.Blocked(authed.Account.ID, acc.ID)
- if err != nil {
- return nil, fmt.Errorf("error checking blocks: %s", err)
- }
- if !blocked {
- filteredAccounts = append(filteredAccounts, acc)
- }
- }
-
- // TODO: filter other things here? suspended? muted? silenced?
-
- // now we can return the masto representation of those accounts
- mastoAccounts := []*apimodel.Account{}
- for _, acc := range filteredAccounts {
- mastoAccount, err := p.tc.AccountToMastoPublic(acc)
- if err != nil {
- return nil, fmt.Errorf("error converting account to api model: %s", err)
- }
- mastoAccounts = append(mastoAccounts, mastoAccount)
- }
-
- return mastoAccounts, nil
-}
-
-func (p *processor) StatusGet(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) {
- l := p.log.WithField("func", "StatusGet")
-
- l.Tracef("going to search for target status %s", targetStatusID)
- targetStatus := >smodel.Status{}
- if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
- return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err)
- }
-
- l.Tracef("going to search for target account %s", targetStatus.AccountID)
- targetAccount := >smodel.Account{}
- if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
- return nil, fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)
- }
-
- l.Trace("going to get relevant accounts")
- relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
- if err != nil {
- return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
- }
-
- l.Trace("going to see if status is visible")
- visible, err := p.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
- if err != nil {
- return nil, fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)
- }
-
- if !visible {
- return nil, errors.New("status is not visible")
- }
-
- var boostOfStatus *gtsmodel.Status
- if targetStatus.BoostOfID != "" {
- boostOfStatus = >smodel.Status{}
- if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
- return nil, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)
- }
- }
-
- mastoStatus, err := p.tc.StatusToMasto(targetStatus, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)
- if err != nil {
- return nil, fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)
- }
-
- return mastoStatus, nil
-
-}
-
-func (p *processor) StatusUnfave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) {
- l := p.log.WithField("func", "StatusUnfave")
- l.Tracef("going to search for target status %s", targetStatusID)
- targetStatus := >smodel.Status{}
- if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
- return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err)
- }
-
- l.Tracef("going to search for target account %s", targetStatus.AccountID)
- targetAccount := >smodel.Account{}
- if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
- return nil, fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)
- }
-
- l.Trace("going to get relevant accounts")
- relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
- if err != nil {
- return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
- }
-
- l.Trace("going to see if status is visible")
- visible, err := p.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
- if err != nil {
- return nil, fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)
- }
-
- if !visible {
- return nil, errors.New("status is not visible")
- }
-
- // is the status faveable?
- if targetStatus.VisibilityAdvanced != nil {
- if !targetStatus.VisibilityAdvanced.Likeable {
- return nil, errors.New("status is not faveable")
- }
- }
-
- // it's visible! it's faveable! so let's unfave the FUCK out of it
- _, err = p.db.UnfaveStatus(targetStatus, authed.Account.ID)
- if err != nil {
- return nil, fmt.Errorf("error unfaveing status: %s", err)
- }
-
- var boostOfStatus *gtsmodel.Status
- if targetStatus.BoostOfID != "" {
- boostOfStatus = >smodel.Status{}
- if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
- return nil, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)
- }
- }
-
- mastoStatus, err := p.tc.StatusToMasto(targetStatus, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)
- if err != nil {
- return nil, fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)
- }
-
- return mastoStatus, nil
-}
diff --git a/internal/message/timelineprocess.go b/internal/message/timelineprocess.go
@@ -1,67 +0,0 @@
-package message
-
-import (
- "fmt"
-
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
-)
-
-func (p *processor) HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) ([]apimodel.Status, ErrorWithCode) {
- statuses, err := p.db.GetHomeTimelineForAccount(authed.Account.ID, maxID, sinceID, minID, limit, local)
- if err != nil {
- return nil, NewErrorInternalError(err)
- }
-
- apiStatuses := []apimodel.Status{}
- for _, s := range statuses {
- targetAccount := >smodel.Account{}
- if err := p.db.GetByID(s.AccountID, targetAccount); err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error getting status author: %s", err))
- }
-
- relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(s)
- if err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error getting relevant statuses for status with id %s and uri %s: %s", s.ID, s.URI, err))
- }
-
- visible, err := p.db.StatusVisible(s, targetAccount, authed.Account, relevantAccounts)
- if err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error checking status visibility: %s", err))
- }
- if !visible {
- continue
- }
-
- var boostedStatus *gtsmodel.Status
- if s.BoostOfID != "" {
- bs := >smodel.Status{}
- if err := p.db.GetByID(s.BoostOfID, bs); err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error getting boosted status: %s", err))
- }
- boostedRelevantAccounts, err := p.db.PullRelevantAccountsFromStatus(bs)
- if err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error getting relevant accounts from boosted status: %s", err))
- }
-
- boostedVisible, err := p.db.StatusVisible(bs, relevantAccounts.BoostedAccount, authed.Account, boostedRelevantAccounts)
- if err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error checking boosted status visibility: %s", err))
- }
-
- if boostedVisible {
- boostedStatus = bs
- }
- }
-
- apiStatus, err := p.tc.StatusToMasto(s, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostedStatus)
- if err != nil {
- return nil, NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error converting status to masto: %s", err))
- }
-
- apiStatuses = append(apiStatuses, *apiStatus)
- }
-
- return apiStatuses, nil
-}
diff --git a/internal/oauth/mock_Server.go b/internal/oauth/mock_Server.go
@@ -1,89 +0,0 @@
-// Code generated by mockery v2.7.4. DO NOT EDIT.
-
-package oauth
-
-import (
- http "net/http"
-
- mock "github.com/stretchr/testify/mock"
- oauth2 "github.com/superseriousbusiness/oauth2/v4"
-)
-
-// MockServer is an autogenerated mock type for the Server type
-type MockServer struct {
- mock.Mock
-}
-
-// GenerateUserAccessToken provides a mock function with given fields: ti, clientSecret, userID
-func (_m *MockServer) GenerateUserAccessToken(ti oauth2.TokenInfo, clientSecret string, userID string) (oauth2.TokenInfo, error) {
- ret := _m.Called(ti, clientSecret, userID)
-
- var r0 oauth2.TokenInfo
- if rf, ok := ret.Get(0).(func(oauth2.TokenInfo, string, string) oauth2.TokenInfo); ok {
- r0 = rf(ti, clientSecret, userID)
- } else {
- if ret.Get(0) != nil {
- r0 = ret.Get(0).(oauth2.TokenInfo)
- }
- }
-
- var r1 error
- if rf, ok := ret.Get(1).(func(oauth2.TokenInfo, string, string) error); ok {
- r1 = rf(ti, clientSecret, userID)
- } else {
- r1 = ret.Error(1)
- }
-
- return r0, r1
-}
-
-// HandleAuthorizeRequest provides a mock function with given fields: w, r
-func (_m *MockServer) HandleAuthorizeRequest(w http.ResponseWriter, r *http.Request) error {
- ret := _m.Called(w, r)
-
- var r0 error
- if rf, ok := ret.Get(0).(func(http.ResponseWriter, *http.Request) error); ok {
- r0 = rf(w, r)
- } else {
- r0 = ret.Error(0)
- }
-
- return r0
-}
-
-// HandleTokenRequest provides a mock function with given fields: w, r
-func (_m *MockServer) HandleTokenRequest(w http.ResponseWriter, r *http.Request) error {
- ret := _m.Called(w, r)
-
- var r0 error
- if rf, ok := ret.Get(0).(func(http.ResponseWriter, *http.Request) error); ok {
- r0 = rf(w, r)
- } else {
- r0 = ret.Error(0)
- }
-
- return r0
-}
-
-// ValidationBearerToken provides a mock function with given fields: r
-func (_m *MockServer) ValidationBearerToken(r *http.Request) (oauth2.TokenInfo, error) {
- ret := _m.Called(r)
-
- var r0 oauth2.TokenInfo
- if rf, ok := ret.Get(0).(func(*http.Request) oauth2.TokenInfo); ok {
- r0 = rf(r)
- } else {
- if ret.Get(0) != nil {
- r0 = ret.Get(0).(oauth2.TokenInfo)
- }
- }
-
- var r1 error
- if rf, ok := ret.Get(1).(func(*http.Request) error); ok {
- r1 = rf(r)
- } else {
- r1 = ret.Error(1)
- }
-
- return r0, r1
-}
diff --git a/internal/processing/account.go b/internal/processing/account.go
@@ -0,0 +1,553 @@
+/*
+ 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 processing
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/google/uuid"
+ 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"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+// accountCreate does the dirty work of making an account and user in the database.
+// It then returns a token to the caller, for use with the new account, as per the
+// spec here: https://docs.joinmastodon.org/methods/accounts/
+func (p *processor) AccountCreate(authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, error) {
+ l := p.log.WithField("func", "accountCreate")
+
+ if err := p.db.IsEmailAvailable(form.Email); err != nil {
+ return nil, err
+ }
+
+ if err := p.db.IsUsernameAvailable(form.Username); err != nil {
+ return nil, err
+ }
+
+ // don't store a reason if we don't require one
+ reason := form.Reason
+ if !p.config.AccountsConfig.ReasonRequired {
+ reason = ""
+ }
+
+ l.Trace("creating new username and account")
+ user, err := p.db.NewSignup(form.Username, reason, p.config.AccountsConfig.RequireApproval, form.Email, form.Password, form.IP, form.Locale, authed.Application.ID)
+ if err != nil {
+ return nil, fmt.Errorf("error creating new signup in the database: %s", err)
+ }
+
+ l.Tracef("generating a token for user %s with account %s and application %s", user.ID, user.AccountID, authed.Application.ID)
+ accessToken, err := p.oauthServer.GenerateUserAccessToken(authed.Token, authed.Application.ClientSecret, user.ID)
+ if err != nil {
+ return nil, fmt.Errorf("error creating new access token for user %s: %s", user.ID, err)
+ }
+
+ return &apimodel.Token{
+ AccessToken: accessToken.GetAccess(),
+ TokenType: "Bearer",
+ Scope: accessToken.GetScope(),
+ CreatedAt: accessToken.GetAccessCreateAt().Unix(),
+ }, nil
+}
+
+func (p *processor) AccountGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Account, error) {
+ targetAccount := >smodel.Account{}
+ if err := p.db.GetByID(targetAccountID, targetAccount); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ return nil, errors.New("account not found")
+ }
+ return nil, fmt.Errorf("db error: %s", err)
+ }
+
+ // lazily dereference things on the account if it hasn't been done yet
+ var requestingUsername string
+ if authed.Account != nil {
+ requestingUsername = authed.Account.Username
+ }
+ if err := p.dereferenceAccountFields(targetAccount, requestingUsername, false); err != nil {
+ p.log.WithField("func", "AccountGet").Debugf("dereferencing account: %s", err)
+ }
+
+ var mastoAccount *apimodel.Account
+ var err error
+ if authed.Account != nil && targetAccount.ID == authed.Account.ID {
+ mastoAccount, err = p.tc.AccountToMastoSensitive(targetAccount)
+ } else {
+ mastoAccount, err = p.tc.AccountToMastoPublic(targetAccount)
+ }
+ if err != nil {
+ return nil, fmt.Errorf("error converting account: %s", err)
+ }
+ return mastoAccount, nil
+}
+
+func (p *processor) AccountUpdate(authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) {
+ l := p.log.WithField("func", "AccountUpdate")
+
+ if form.Discoverable != nil {
+ if err := p.db.UpdateOneByID(authed.Account.ID, "discoverable", *form.Discoverable, >smodel.Account{}); err != nil {
+ return nil, fmt.Errorf("error updating discoverable: %s", err)
+ }
+ }
+
+ if form.Bot != nil {
+ if err := p.db.UpdateOneByID(authed.Account.ID, "bot", *form.Bot, >smodel.Account{}); err != nil {
+ return nil, fmt.Errorf("error updating bot: %s", err)
+ }
+ }
+
+ if form.DisplayName != nil {
+ if err := util.ValidateDisplayName(*form.DisplayName); err != nil {
+ return nil, err
+ }
+ if err := p.db.UpdateOneByID(authed.Account.ID, "display_name", *form.DisplayName, >smodel.Account{}); err != nil {
+ return nil, err
+ }
+ }
+
+ if form.Note != nil {
+ if err := util.ValidateNote(*form.Note); err != nil {
+ return nil, err
+ }
+ if err := p.db.UpdateOneByID(authed.Account.ID, "note", *form.Note, >smodel.Account{}); err != nil {
+ return nil, err
+ }
+ }
+
+ if form.Avatar != nil && form.Avatar.Size != 0 {
+ avatarInfo, err := p.updateAccountAvatar(form.Avatar, authed.Account.ID)
+ if err != nil {
+ return nil, err
+ }
+ l.Tracef("new avatar info for account %s is %+v", authed.Account.ID, avatarInfo)
+ }
+
+ if form.Header != nil && form.Header.Size != 0 {
+ headerInfo, err := p.updateAccountHeader(form.Header, authed.Account.ID)
+ if err != nil {
+ return nil, err
+ }
+ l.Tracef("new header info for account %s is %+v", authed.Account.ID, headerInfo)
+ }
+
+ if form.Locked != nil {
+ if err := p.db.UpdateOneByID(authed.Account.ID, "locked", *form.Locked, >smodel.Account{}); err != nil {
+ return nil, err
+ }
+ }
+
+ if form.Source != nil {
+ if form.Source.Language != nil {
+ if err := util.ValidateLanguage(*form.Source.Language); err != nil {
+ return nil, err
+ }
+ if err := p.db.UpdateOneByID(authed.Account.ID, "language", *form.Source.Language, >smodel.Account{}); err != nil {
+ return nil, err
+ }
+ }
+
+ if form.Source.Sensitive != nil {
+ if err := p.db.UpdateOneByID(authed.Account.ID, "locked", *form.Locked, >smodel.Account{}); err != nil {
+ return nil, err
+ }
+ }
+
+ if form.Source.Privacy != nil {
+ if err := util.ValidatePrivacy(*form.Source.Privacy); err != nil {
+ return nil, err
+ }
+ if err := p.db.UpdateOneByID(authed.Account.ID, "privacy", *form.Source.Privacy, >smodel.Account{}); err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ // fetch the account with all updated values set
+ updatedAccount := >smodel.Account{}
+ if err := p.db.GetByID(authed.Account.ID, updatedAccount); err != nil {
+ return nil, fmt.Errorf("could not fetch updated account %s: %s", authed.Account.ID, err)
+ }
+
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsProfile,
+ APActivityType: gtsmodel.ActivityStreamsUpdate,
+ GTSModel: updatedAccount,
+ OriginAccount: updatedAccount,
+ }
+
+ acctSensitive, err := p.tc.AccountToMastoSensitive(updatedAccount)
+ if err != nil {
+ return nil, fmt.Errorf("could not convert account into mastosensitive account: %s", err)
+ }
+ return acctSensitive, nil
+}
+
+func (p *processor) AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, ErrorWithCode) {
+ targetAccount := >smodel.Account{}
+ if err := p.db.GetByID(targetAccountID, targetAccount); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ return nil, NewErrorNotFound(fmt.Errorf("no entry found for account id %s", targetAccountID))
+ }
+ return nil, NewErrorInternalError(err)
+ }
+
+ statuses := []gtsmodel.Status{}
+ apiStatuses := []apimodel.Status{}
+ if err := p.db.GetStatusesByTimeDescending(targetAccountID, &statuses, limit, excludeReplies, maxID, pinned, mediaOnly); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ return apiStatuses, nil
+ }
+ return nil, NewErrorInternalError(err)
+ }
+
+ for _, s := range statuses {
+ relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(&s)
+ if err != nil {
+ return nil, NewErrorInternalError(fmt.Errorf("error getting relevant statuses: %s", err))
+ }
+
+ visible, err := p.db.StatusVisible(&s, targetAccount, authed.Account, relevantAccounts)
+ if err != nil {
+ return nil, NewErrorInternalError(fmt.Errorf("error checking status visibility: %s", err))
+ }
+ if !visible {
+ continue
+ }
+
+ var boostedStatus *gtsmodel.Status
+ if s.BoostOfID != "" {
+ bs := >smodel.Status{}
+ if err := p.db.GetByID(s.BoostOfID, bs); err != nil {
+ return nil, NewErrorInternalError(fmt.Errorf("error getting boosted status: %s", err))
+ }
+ boostedRelevantAccounts, err := p.db.PullRelevantAccountsFromStatus(bs)
+ if err != nil {
+ return nil, NewErrorInternalError(fmt.Errorf("error getting relevant accounts from boosted status: %s", err))
+ }
+
+ boostedVisible, err := p.db.StatusVisible(bs, relevantAccounts.BoostedAccount, authed.Account, boostedRelevantAccounts)
+ if err != nil {
+ return nil, NewErrorInternalError(fmt.Errorf("error checking boosted status visibility: %s", err))
+ }
+
+ if boostedVisible {
+ boostedStatus = bs
+ }
+ }
+
+ apiStatus, err := p.tc.StatusToMasto(&s, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostedStatus)
+ if err != nil {
+ return nil, NewErrorInternalError(fmt.Errorf("error converting status to masto: %s", err))
+ }
+
+ apiStatuses = append(apiStatuses, *apiStatus)
+ }
+
+ return apiStatuses, nil
+}
+
+func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode) {
+ blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+
+ if blocked {
+ return nil, NewErrorNotFound(fmt.Errorf("block exists between accounts"))
+ }
+
+ followers := []gtsmodel.Follow{}
+ accounts := []apimodel.Account{}
+ if err := p.db.GetFollowersByAccountID(targetAccountID, &followers); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ return accounts, nil
+ }
+ return nil, NewErrorInternalError(err)
+ }
+
+ for _, f := range followers {
+ blocked, err := p.db.Blocked(authed.Account.ID, f.AccountID)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+ if blocked {
+ continue
+ }
+
+ a := >smodel.Account{}
+ if err := p.db.GetByID(f.AccountID, a); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ continue
+ }
+ return nil, NewErrorInternalError(err)
+ }
+
+ // derefence account fields in case we haven't done it already
+ if err := p.dereferenceAccountFields(a, authed.Account.Username, false); err != nil {
+ // don't bail if we can't fetch them, we'll try another time
+ p.log.WithField("func", "AccountFollowersGet").Debugf("error dereferencing account fields: %s", err)
+ }
+
+ account, err := p.tc.AccountToMastoPublic(a)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+ accounts = append(accounts, *account)
+ }
+ return accounts, nil
+}
+
+func (p *processor) AccountFollowingGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode) {
+ blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+
+ if blocked {
+ return nil, NewErrorNotFound(fmt.Errorf("block exists between accounts"))
+ }
+
+ following := []gtsmodel.Follow{}
+ accounts := []apimodel.Account{}
+ if err := p.db.GetFollowingByAccountID(targetAccountID, &following); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ return accounts, nil
+ }
+ return nil, NewErrorInternalError(err)
+ }
+
+ for _, f := range following {
+ blocked, err := p.db.Blocked(authed.Account.ID, f.AccountID)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+ if blocked {
+ continue
+ }
+
+ a := >smodel.Account{}
+ if err := p.db.GetByID(f.TargetAccountID, a); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ continue
+ }
+ return nil, NewErrorInternalError(err)
+ }
+
+ // derefence account fields in case we haven't done it already
+ if err := p.dereferenceAccountFields(a, authed.Account.Username, false); err != nil {
+ // don't bail if we can't fetch them, we'll try another time
+ p.log.WithField("func", "AccountFollowingGet").Debugf("error dereferencing account fields: %s", err)
+ }
+
+ account, err := p.tc.AccountToMastoPublic(a)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+ accounts = append(accounts, *account)
+ }
+ return accounts, nil
+}
+
+func (p *processor) AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode) {
+ if authed == nil || authed.Account == nil {
+ return nil, NewErrorForbidden(errors.New("not authed"))
+ }
+
+ gtsR, err := p.db.GetRelationship(authed.Account.ID, targetAccountID)
+ if err != nil {
+ return nil, NewErrorInternalError(fmt.Errorf("error getting relationship: %s", err))
+ }
+
+ r, err := p.tc.RelationshipToMasto(gtsR)
+ if err != nil {
+ return nil, NewErrorInternalError(fmt.Errorf("error converting relationship: %s", err))
+ }
+
+ return r, nil
+}
+
+func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, ErrorWithCode) {
+ // if there's a block between the accounts we shouldn't create the request ofc
+ blocked, err := p.db.Blocked(authed.Account.ID, form.TargetAccountID)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+ if blocked {
+ return nil, NewErrorNotFound(fmt.Errorf("accountfollowcreate: block exists between accounts"))
+ }
+
+ // make sure the target account actually exists in our db
+ targetAcct := >smodel.Account{}
+ if err := p.db.GetByID(form.TargetAccountID, targetAcct); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ return nil, NewErrorNotFound(fmt.Errorf("accountfollowcreate: account %s not found in the db: %s", form.TargetAccountID, err))
+ }
+ }
+
+ // check if a follow exists already
+ follows, err := p.db.Follows(authed.Account, targetAcct)
+ if err != nil {
+ return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow in db: %s", err))
+ }
+ if follows {
+ // already follows so just return the relationship
+ return p.AccountRelationshipGet(authed, form.TargetAccountID)
+ }
+
+ // check if a follow exists already
+ followRequested, err := p.db.FollowRequested(authed.Account, targetAcct)
+ if err != nil {
+ return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow request in db: %s", err))
+ }
+ if followRequested {
+ // already follow requested so just return the relationship
+ return p.AccountRelationshipGet(authed, form.TargetAccountID)
+ }
+
+ // make the follow request
+
+ newFollowID := uuid.NewString()
+
+ fr := >smodel.FollowRequest{
+ ID: newFollowID,
+ AccountID: authed.Account.ID,
+ TargetAccountID: form.TargetAccountID,
+ ShowReblogs: true,
+ URI: util.GenerateURIForFollow(authed.Account.Username, p.config.Protocol, p.config.Host, newFollowID),
+ Notify: false,
+ }
+ if form.Reblogs != nil {
+ fr.ShowReblogs = *form.Reblogs
+ }
+ if form.Notify != nil {
+ fr.Notify = *form.Notify
+ }
+
+ // whack it in the database
+ if err := p.db.Put(fr); err != nil {
+ return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error creating follow request in db: %s", err))
+ }
+
+ // if it's a local account that's not locked we can just straight up accept the follow request
+ if !targetAcct.Locked && targetAcct.Domain == "" {
+ if _, err := p.db.AcceptFollowRequest(authed.Account.ID, form.TargetAccountID); err != nil {
+ return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error accepting folow request for local unlocked account: %s", err))
+ }
+ // return the new relationship
+ return p.AccountRelationshipGet(authed, form.TargetAccountID)
+ }
+
+ // otherwise we leave the follow request as it is and we handle the rest of the process asynchronously
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsFollow,
+ APActivityType: gtsmodel.ActivityStreamsCreate,
+ GTSModel: fr,
+ OriginAccount: authed.Account,
+ TargetAccount: targetAcct,
+ }
+
+ // return whatever relationship results from this
+ return p.AccountRelationshipGet(authed, form.TargetAccountID)
+}
+
+func (p *processor) AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode) {
+ // if there's a block between the accounts we shouldn't do anything
+ blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+ if blocked {
+ return nil, NewErrorNotFound(fmt.Errorf("AccountFollowRemove: block exists between accounts"))
+ }
+
+ // make sure the target account actually exists in our db
+ targetAcct := >smodel.Account{}
+ if err := p.db.GetByID(targetAccountID, targetAcct); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ return nil, NewErrorNotFound(fmt.Errorf("AccountFollowRemove: account %s not found in the db: %s", targetAccountID, err))
+ }
+ }
+
+ // check if a follow request exists, and remove it if it does (storing the URI for later)
+ var frChanged bool
+ var frURI string
+ fr := >smodel.FollowRequest{}
+ if err := p.db.GetWhere([]db.Where{
+ {Key: "account_id", Value: authed.Account.ID},
+ {Key: "target_account_id", Value: targetAccountID},
+ }, fr); err == nil {
+ frURI = fr.URI
+ if err := p.db.DeleteByID(fr.ID, fr); err != nil {
+ return nil, NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow request from db: %s", err))
+ }
+ frChanged = true
+ }
+
+ // now do the same thing for any existing follow
+ var fChanged bool
+ var fURI string
+ f := >smodel.Follow{}
+ if err := p.db.GetWhere([]db.Where{
+ {Key: "account_id", Value: authed.Account.ID},
+ {Key: "target_account_id", Value: targetAccountID},
+ }, f); err == nil {
+ fURI = f.URI
+ if err := p.db.DeleteByID(f.ID, f); err != nil {
+ return nil, NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow from db: %s", err))
+ }
+ fChanged = true
+ }
+
+ // follow request status changed so send the UNDO activity to the channel for async processing
+ if frChanged {
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsFollow,
+ APActivityType: gtsmodel.ActivityStreamsUndo,
+ GTSModel: >smodel.Follow{
+ AccountID: authed.Account.ID,
+ TargetAccountID: targetAccountID,
+ URI: frURI,
+ },
+ OriginAccount: authed.Account,
+ TargetAccount: targetAcct,
+ }
+ }
+
+ // follow status changed so send the UNDO activity to the channel for async processing
+ if fChanged {
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsFollow,
+ APActivityType: gtsmodel.ActivityStreamsUndo,
+ GTSModel: >smodel.Follow{
+ AccountID: authed.Account.ID,
+ TargetAccountID: targetAccountID,
+ URI: fURI,
+ },
+ OriginAccount: authed.Account,
+ TargetAccount: targetAcct,
+ }
+ }
+
+ // return whatever relationship results from all this
+ return p.AccountRelationshipGet(authed, targetAccountID)
+}
diff --git a/internal/processing/admin.go b/internal/processing/admin.go
@@ -0,0 +1,66 @@
+/*
+ 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 processing
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+func (p *processor) AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) {
+ if !authed.User.Admin {
+ return nil, fmt.Errorf("user %s not an admin", authed.User.ID)
+ }
+
+ // open the emoji and extract the bytes from it
+ f, err := form.Image.Open()
+ if err != nil {
+ return nil, fmt.Errorf("error opening emoji: %s", err)
+ }
+ buf := new(bytes.Buffer)
+ size, err := io.Copy(buf, f)
+ if err != nil {
+ return nil, fmt.Errorf("error reading emoji: %s", err)
+ }
+ if size == 0 {
+ return nil, errors.New("could not read provided emoji: size 0 bytes")
+ }
+
+ // allow the mediaHandler to work its magic of processing the emoji bytes, and putting them in whatever storage backend we're using
+ emoji, err := p.mediaHandler.ProcessLocalEmoji(buf.Bytes(), form.Shortcode)
+ if err != nil {
+ return nil, fmt.Errorf("error reading emoji: %s", err)
+ }
+
+ mastoEmoji, err := p.tc.EmojiToMasto(emoji)
+ if err != nil {
+ return nil, fmt.Errorf("error converting emoji to mastotype: %s", err)
+ }
+
+ if err := p.db.Put(emoji); err != nil {
+ return nil, fmt.Errorf("database error while processing emoji: %s", err)
+ }
+
+ return &mastoEmoji, nil
+}
diff --git a/internal/processing/app.go b/internal/processing/app.go
@@ -0,0 +1,77 @@
+/*
+ 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 processing
+
+import (
+ "github.com/google/uuid"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+func (p *processor) AppCreate(authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error) {
+ // set default 'read' for scopes if it's not set, this follows the default of the mastodon api https://docs.joinmastodon.org/methods/apps/
+ var scopes string
+ if form.Scopes == "" {
+ scopes = "read"
+ } else {
+ scopes = form.Scopes
+ }
+
+ // generate new IDs for this application and its associated client
+ clientID := uuid.NewString()
+ clientSecret := uuid.NewString()
+ vapidKey := uuid.NewString()
+
+ // generate the application to put in the database
+ app := >smodel.Application{
+ Name: form.ClientName,
+ Website: form.Website,
+ RedirectURI: form.RedirectURIs,
+ ClientID: clientID,
+ ClientSecret: clientSecret,
+ Scopes: scopes,
+ VapidKey: vapidKey,
+ }
+
+ // chuck it in the db
+ if err := p.db.Put(app); err != nil {
+ return nil, err
+ }
+
+ // now we need to model an oauth client from the application that the oauth library can use
+ oc := &oauth.Client{
+ ID: clientID,
+ Secret: clientSecret,
+ Domain: form.RedirectURIs,
+ UserID: "", // This client isn't yet associated with a specific user, it's just an app client right now
+ }
+
+ // chuck it in the db
+ if err := p.db.Put(oc); err != nil {
+ return nil, err
+ }
+
+ mastoApp, err := p.tc.AppToMastoSensitive(app)
+ if err != nil {
+ return nil, err
+ }
+
+ return mastoApp, nil
+}
diff --git a/internal/processing/error.go b/internal/processing/error.go
@@ -0,0 +1,124 @@
+/*
+ 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 processing
+
+import (
+ "errors"
+ "net/http"
+ "strings"
+)
+
+// ErrorWithCode wraps an internal error with an http code, and a 'safe' version of
+// the error that can be served to clients without revealing internal business logic.
+//
+// A typical use of this error would be to first log the Original error, then return
+// the Safe error and the StatusCode to an API caller.
+type ErrorWithCode interface {
+ // Error returns the original internal error for debugging within the GoToSocial logs.
+ // This should *NEVER* be returned to a client as it may contain sensitive information.
+ Error() string
+ // Safe returns the API-safe version of the error for serialization towards a client.
+ // There's not much point logging this internally because it won't contain much helpful information.
+ Safe() string
+ // Code returns the status code for serving to a client.
+ Code() int
+}
+
+type errorWithCode struct {
+ original error
+ safe error
+ code int
+}
+
+func (e errorWithCode) Error() string {
+ return e.original.Error()
+}
+
+func (e errorWithCode) Safe() string {
+ return e.safe.Error()
+}
+
+func (e errorWithCode) Code() int {
+ return e.code
+}
+
+// NewErrorBadRequest returns an ErrorWithCode 400 with the given original error and optional help text.
+func NewErrorBadRequest(original error, helpText ...string) ErrorWithCode {
+ safe := "bad request"
+ if helpText != nil {
+ safe = safe + ": " + strings.Join(helpText, ": ")
+ }
+ return errorWithCode{
+ original: original,
+ safe: errors.New(safe),
+ code: http.StatusBadRequest,
+ }
+}
+
+// NewErrorNotAuthorized returns an ErrorWithCode 401 with the given original error and optional help text.
+func NewErrorNotAuthorized(original error, helpText ...string) ErrorWithCode {
+ safe := "not authorized"
+ if helpText != nil {
+ safe = safe + ": " + strings.Join(helpText, ": ")
+ }
+ return errorWithCode{
+ original: original,
+ safe: errors.New(safe),
+ code: http.StatusUnauthorized,
+ }
+}
+
+// NewErrorForbidden returns an ErrorWithCode 403 with the given original error and optional help text.
+func NewErrorForbidden(original error, helpText ...string) ErrorWithCode {
+ safe := "forbidden"
+ if helpText != nil {
+ safe = safe + ": " + strings.Join(helpText, ": ")
+ }
+ return errorWithCode{
+ original: original,
+ safe: errors.New(safe),
+ code: http.StatusForbidden,
+ }
+}
+
+// NewErrorNotFound returns an ErrorWithCode 404 with the given original error and optional help text.
+func NewErrorNotFound(original error, helpText ...string) ErrorWithCode {
+ safe := "404 not found"
+ if helpText != nil {
+ safe = safe + ": " + strings.Join(helpText, ": ")
+ }
+ return errorWithCode{
+ original: original,
+ safe: errors.New(safe),
+ code: http.StatusNotFound,
+ }
+}
+
+// NewErrorInternalError returns an ErrorWithCode 500 with the given original error and optional help text.
+func NewErrorInternalError(original error, helpText ...string) ErrorWithCode {
+ safe := "internal server error"
+ if helpText != nil {
+ safe = safe + ": " + strings.Join(helpText, ": ")
+ }
+ return errorWithCode{
+ original: original,
+ safe: errors.New(safe),
+ code: http.StatusInternalServerError,
+ }
+}
diff --git a/internal/processing/federation.go b/internal/processing/federation.go
@@ -0,0 +1,282 @@
+/*
+ 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 processing
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "net/url"
+
+ "github.com/go-fed/activity/streams"
+ 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
+// username to perform the validation. It will *also* dereference the originator of the request and return it as a gtsmodel account
+// for further processing. NOTE that this function will have the side effect of putting the dereferenced account into the database,
+// and passing it into the processor through a channel for further asynchronous processing.
+func (p *processor) authenticateAndDereferenceFediRequest(username string, r *http.Request) (*gtsmodel.Account, error) {
+
+ // first authenticate
+ requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(username, r)
+ if err != nil {
+ return nil, fmt.Errorf("couldn't authenticate request for username %s: %s", username, err)
+ }
+
+ // OK now we can do the dereferencing part
+ // we might already have an entry for this account so check that first
+ requestingAccount := >smodel.Account{}
+
+ err = p.db.GetWhere([]db.Where{{Key: "uri", Value: requestingAccountURI.String()}}, requestingAccount)
+ if err == nil {
+ // we do have it yay, return it
+ return requestingAccount, nil
+ }
+
+ if _, ok := err.(db.ErrNoEntries); !ok {
+ // something has actually gone wrong so bail
+ return nil, fmt.Errorf("database error getting account with uri %s: %s", requestingAccountURI.String(), err)
+ }
+
+ // we just don't have an entry for this account yet
+ // what we do now should depend on our chosen federation method
+ // for now though, we'll just dereference it
+ // TODO: slow-fed
+ requestingPerson, err := p.federator.DereferenceRemoteAccount(username, requestingAccountURI)
+ if err != nil {
+ return nil, fmt.Errorf("couldn't dereference %s: %s", requestingAccountURI.String(), err)
+ }
+
+ // convert it to our internal account representation
+ requestingAccount, err = p.tc.ASRepresentationToAccount(requestingPerson, false)
+ if err != nil {
+ return nil, fmt.Errorf("couldn't convert dereferenced uri %s to gtsmodel account: %s", requestingAccountURI.String(), err)
+ }
+
+ // shove it in the database for later
+ if err := p.db.Put(requestingAccount); err != nil {
+ return nil, fmt.Errorf("database error inserting account with uri %s: %s", requestingAccountURI.String(), err)
+ }
+
+ // put it in our channel to queue it for async processing
+ p.FromFederator() <- gtsmodel.FromFederator{
+ APObjectType: gtsmodel.ActivityStreamsProfile,
+ APActivityType: gtsmodel.ActivityStreamsCreate,
+ GTSModel: requestingAccount,
+ }
+
+ return requestingAccount, nil
+}
+
+func (p *processor) GetFediUser(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) {
+ // get the account the request is referring to
+ requestedAccount := >smodel.Account{}
+ if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
+ }
+
+ // authenticate the request
+ requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
+ if err != nil {
+ return nil, NewErrorNotAuthorized(err)
+ }
+
+ blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+
+ if blocked {
+ return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
+ }
+
+ requestedPerson, err := p.tc.AccountToAS(requestedAccount)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+
+ data, err := streams.Serialize(requestedPerson)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+
+ return data, nil
+}
+
+func (p *processor) GetFediFollowers(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) {
+ // get the account the request is referring to
+ requestedAccount := >smodel.Account{}
+ if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
+ }
+
+ // authenticate the request
+ requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
+ if err != nil {
+ return nil, NewErrorNotAuthorized(err)
+ }
+
+ blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+
+ if blocked {
+ return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
+ }
+
+ requestedAccountURI, err := url.Parse(requestedAccount.URI)
+ if err != nil {
+ return nil, NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err))
+ }
+
+ requestedFollowers, err := p.federator.FederatingDB().Followers(context.Background(), requestedAccountURI)
+ if err != nil {
+ return nil, NewErrorInternalError(fmt.Errorf("error fetching followers for uri %s: %s", requestedAccountURI.String(), err))
+ }
+
+ data, err := streams.Serialize(requestedFollowers)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+
+ return data, nil
+}
+
+func (p *processor) GetFediFollowing(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) {
+ // get the account the request is referring to
+ requestedAccount := >smodel.Account{}
+ if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
+ }
+
+ // authenticate the request
+ requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
+ if err != nil {
+ return nil, NewErrorNotAuthorized(err)
+ }
+
+ blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+
+ if blocked {
+ return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
+ }
+
+ requestedAccountURI, err := url.Parse(requestedAccount.URI)
+ if err != nil {
+ return nil, NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err))
+ }
+
+ requestedFollowing, err := p.federator.FederatingDB().Following(context.Background(), requestedAccountURI)
+ if err != nil {
+ return nil, NewErrorInternalError(fmt.Errorf("error fetching following for uri %s: %s", requestedAccountURI.String(), err))
+ }
+
+ data, err := streams.Serialize(requestedFollowing)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+
+ return data, nil
+}
+
+func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, ErrorWithCode) {
+ // get the account the request is referring to
+ requestedAccount := >smodel.Account{}
+ if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
+ }
+
+ // authenticate the request
+ requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
+ if err != nil {
+ return nil, NewErrorNotAuthorized(err)
+ }
+
+ blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+
+ if blocked {
+ return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
+ }
+
+ s := >smodel.Status{}
+ if err := p.db.GetWhere([]db.Where{
+ {Key: "id", Value: requestedStatusID},
+ {Key: "account_id", Value: requestedAccount.ID},
+ }, s); err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("database error getting status with id %s and account id %s: %s", requestedStatusID, requestedAccount.ID, err))
+ }
+
+ asStatus, err := p.tc.StatusToAS(s)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+
+ data, err := streams.Serialize(asStatus)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+
+ return data, nil
+}
+
+func (p *processor) GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, ErrorWithCode) {
+ // get the account the request is referring to
+ requestedAccount := >smodel.Account{}
+ if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
+ }
+
+ // return the webfinger representation
+ return &apimodel.WebfingerAccountResponse{
+ Subject: fmt.Sprintf("acct:%s@%s", requestedAccount.Username, p.config.Host),
+ Aliases: []string{
+ requestedAccount.URI,
+ requestedAccount.URL,
+ },
+ Links: []apimodel.WebfingerLink{
+ {
+ Rel: "http://webfinger.net/rel/profile-page",
+ Type: "text/html",
+ Href: requestedAccount.URL,
+ },
+ {
+ Rel: "self",
+ Type: "application/activity+json",
+ Href: requestedAccount.URI,
+ },
+ },
+ }, 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/processing/followrequest.go b/internal/processing/followrequest.go
@@ -0,0 +1,90 @@
+/*
+ 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 processing
+
+import (
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+func (p *processor) FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, ErrorWithCode) {
+ frs := []gtsmodel.FollowRequest{}
+ if err := p.db.GetFollowRequestsForAccountID(auth.Account.ID, &frs); err != nil {
+ if _, ok := err.(db.ErrNoEntries); !ok {
+ return nil, NewErrorInternalError(err)
+ }
+ }
+
+ accts := []apimodel.Account{}
+ for _, fr := range frs {
+ acct := >smodel.Account{}
+ if err := p.db.GetByID(fr.AccountID, acct); err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+ mastoAcct, err := p.tc.AccountToMastoPublic(acct)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+ accts = append(accts, *mastoAcct)
+ }
+ return accts, nil
+}
+
+func (p *processor) FollowRequestAccept(auth *oauth.Auth, accountID string) (*apimodel.Relationship, ErrorWithCode) {
+ follow, err := p.db.AcceptFollowRequest(accountID, auth.Account.ID)
+ if err != nil {
+ return nil, NewErrorNotFound(err)
+ }
+
+ originAccount := >smodel.Account{}
+ if err := p.db.GetByID(follow.AccountID, originAccount); err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+
+ targetAccount := >smodel.Account{}
+ if err := p.db.GetByID(follow.TargetAccountID, targetAccount); err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsFollow,
+ APActivityType: gtsmodel.ActivityStreamsAccept,
+ GTSModel: follow,
+ OriginAccount: originAccount,
+ TargetAccount: targetAccount,
+ }
+
+ gtsR, err := p.db.GetRelationship(auth.Account.ID, accountID)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+
+ r, err := p.tc.RelationshipToMasto(gtsR)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+
+ return r, nil
+}
+
+func (p *processor) FollowRequestDeny(auth *oauth.Auth) ErrorWithCode {
+ return nil
+}
diff --git a/internal/processing/fromclientapi.go b/internal/processing/fromclientapi.go
@@ -0,0 +1,313 @@
+/*
+ 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 processing
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/url"
+
+ "github.com/go-fed/activity/streams"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error {
+ switch clientMsg.APActivityType {
+ case gtsmodel.ActivityStreamsCreate:
+ // CREATE
+ switch clientMsg.APObjectType {
+ case gtsmodel.ActivityStreamsNote:
+ // CREATE NOTE
+ status, ok := clientMsg.GTSModel.(*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 != nil && status.VisibilityAdvanced.Federated {
+ return p.federateStatus(status)
+ }
+ return nil
+ case gtsmodel.ActivityStreamsFollow:
+ // CREATE FOLLOW REQUEST
+ followRequest, ok := clientMsg.GTSModel.(*gtsmodel.FollowRequest)
+ if !ok {
+ return errors.New("followrequest was not parseable as *gtsmodel.FollowRequest")
+ }
+
+ if err := p.notifyFollowRequest(followRequest, clientMsg.TargetAccount); err != nil {
+ return err
+ }
+
+ return p.federateFollow(followRequest, clientMsg.OriginAccount, clientMsg.TargetAccount)
+ case gtsmodel.ActivityStreamsLike:
+ // CREATE LIKE/FAVE
+ fave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave)
+ if !ok {
+ return errors.New("fave was not parseable as *gtsmodel.StatusFave")
+ }
+
+ if err := p.notifyFave(fave, clientMsg.TargetAccount); err != nil {
+ return err
+ }
+
+ return p.federateFave(fave, clientMsg.OriginAccount, clientMsg.TargetAccount)
+
+ case gtsmodel.ActivityStreamsAnnounce:
+ // CREATE BOOST/ANNOUNCE
+ boostWrapperStatus, ok := clientMsg.GTSModel.(*gtsmodel.Status)
+ if !ok {
+ return errors.New("boost was not parseable as *gtsmodel.Status")
+ }
+
+ if err := p.notifyAnnounce(boostWrapperStatus); err != nil {
+ return err
+ }
+
+ return p.federateAnnounce(boostWrapperStatus, clientMsg.OriginAccount, clientMsg.TargetAccount)
+ }
+ case gtsmodel.ActivityStreamsUpdate:
+ // UPDATE
+ switch clientMsg.APObjectType {
+ case gtsmodel.ActivityStreamsProfile, gtsmodel.ActivityStreamsPerson:
+ // UPDATE ACCOUNT/PROFILE
+ account, ok := clientMsg.GTSModel.(*gtsmodel.Account)
+ if !ok {
+ return errors.New("account was not parseable as *gtsmodel.Account")
+ }
+
+ return p.federateAccountUpdate(account, clientMsg.OriginAccount)
+ }
+ case gtsmodel.ActivityStreamsAccept:
+ // ACCEPT
+ switch clientMsg.APObjectType {
+ case gtsmodel.ActivityStreamsFollow:
+ // ACCEPT FOLLOW
+ follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow)
+ if !ok {
+ return errors.New("accept was not parseable as *gtsmodel.Follow")
+ }
+
+ if err := p.notifyFollow(follow, clientMsg.TargetAccount); err != nil {
+ return err
+ }
+
+ return p.federateAcceptFollowRequest(follow, clientMsg.OriginAccount, clientMsg.TargetAccount)
+ }
+ case gtsmodel.ActivityStreamsUndo:
+ // UNDO
+ switch clientMsg.APObjectType {
+ case gtsmodel.ActivityStreamsFollow:
+ // UNDO FOLLOW
+ follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow)
+ if !ok {
+ return errors.New("undo was not parseable as *gtsmodel.Follow")
+ }
+ return p.federateUnfollow(follow, clientMsg.OriginAccount, clientMsg.TargetAccount)
+ }
+ }
+ return nil
+}
+
+func (p *processor) federateStatus(status *gtsmodel.Status) error {
+ asStatus, err := p.tc.StatusToAS(status)
+ if err != nil {
+ return fmt.Errorf("federateStatus: error converting status to as format: %s", err)
+ }
+
+ outboxIRI, err := url.Parse(status.GTSAuthorAccount.OutboxURI)
+ if err != nil {
+ return fmt.Errorf("federateStatus: error parsing outboxURI %s: %s", status.GTSAuthorAccount.OutboxURI, err)
+ }
+
+ _, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, asStatus)
+ return err
+}
+
+func (p *processor) federateFollow(followRequest *gtsmodel.FollowRequest, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
+ // if both accounts are local there's nothing to do here
+ if originAccount.Domain == "" && targetAccount.Domain == "" {
+ return nil
+ }
+
+ follow := p.tc.FollowRequestToFollow(followRequest)
+
+ asFollow, err := p.tc.FollowToAS(follow, originAccount, targetAccount)
+ if err != nil {
+ return fmt.Errorf("federateFollow: error converting follow to as format: %s", err)
+ }
+
+ outboxIRI, err := url.Parse(originAccount.OutboxURI)
+ if err != nil {
+ return fmt.Errorf("federateFollow: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
+ }
+
+ _, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, asFollow)
+ return err
+}
+
+func (p *processor) federateUnfollow(follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
+ // if both accounts are local there's nothing to do here
+ if originAccount.Domain == "" && targetAccount.Domain == "" {
+ return nil
+ }
+
+ // recreate the follow
+ asFollow, err := p.tc.FollowToAS(follow, originAccount, targetAccount)
+ if err != nil {
+ return fmt.Errorf("federateUnfollow: error converting follow to as format: %s", err)
+ }
+
+ targetAccountURI, err := url.Parse(targetAccount.URI)
+ if err != nil {
+ return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err)
+ }
+
+ // create an Undo and set the appropriate actor on it
+ undo := streams.NewActivityStreamsUndo()
+ undo.SetActivityStreamsActor(asFollow.GetActivityStreamsActor())
+
+ // Set the recreated follow as the 'object' property.
+ undoObject := streams.NewActivityStreamsObjectProperty()
+ undoObject.AppendActivityStreamsFollow(asFollow)
+ undo.SetActivityStreamsObject(undoObject)
+
+ // Set the To of the undo as the target of the recreated follow
+ undoTo := streams.NewActivityStreamsToProperty()
+ undoTo.AppendIRI(targetAccountURI)
+ undo.SetActivityStreamsTo(undoTo)
+
+ outboxIRI, err := url.Parse(originAccount.OutboxURI)
+ if err != nil {
+ return fmt.Errorf("federateUnfollow: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
+ }
+
+ // send off the Undo
+ _, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, undo)
+ return err
+}
+
+func (p *processor) federateAcceptFollowRequest(follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
+ // if both accounts are local there's nothing to do here
+ if originAccount.Domain == "" && targetAccount.Domain == "" {
+ return nil
+ }
+
+ // recreate the AS follow
+ asFollow, err := p.tc.FollowToAS(follow, originAccount, targetAccount)
+ if err != nil {
+ return fmt.Errorf("federateUnfollow: error converting follow to as format: %s", err)
+ }
+
+ acceptingAccountURI, err := url.Parse(targetAccount.URI)
+ if err != nil {
+ return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err)
+ }
+
+ requestingAccountURI, err := url.Parse(originAccount.URI)
+ if err != nil {
+ return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err)
+ }
+
+ // create an Accept
+ accept := streams.NewActivityStreamsAccept()
+
+ // set the accepting actor on it
+ acceptActorProp := streams.NewActivityStreamsActorProperty()
+ acceptActorProp.AppendIRI(acceptingAccountURI)
+ accept.SetActivityStreamsActor(acceptActorProp)
+
+ // Set the recreated follow as the 'object' property.
+ acceptObject := streams.NewActivityStreamsObjectProperty()
+ acceptObject.AppendActivityStreamsFollow(asFollow)
+ accept.SetActivityStreamsObject(acceptObject)
+
+ // Set the To of the accept as the originator of the follow
+ acceptTo := streams.NewActivityStreamsToProperty()
+ acceptTo.AppendIRI(requestingAccountURI)
+ accept.SetActivityStreamsTo(acceptTo)
+
+ outboxIRI, err := url.Parse(targetAccount.OutboxURI)
+ if err != nil {
+ return fmt.Errorf("federateAcceptFollowRequest: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
+ }
+
+ // send off the accept using the accepter's outbox
+ _, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, accept)
+ return err
+}
+
+func (p *processor) federateFave(fave *gtsmodel.StatusFave, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
+ // if both accounts are local there's nothing to do here
+ if originAccount.Domain == "" && targetAccount.Domain == "" {
+ return nil
+ }
+
+ // create the AS fave
+ asFave, err := p.tc.FaveToAS(fave)
+ if err != nil {
+ return fmt.Errorf("federateFave: error converting fave to as format: %s", err)
+ }
+
+ outboxIRI, err := url.Parse(originAccount.OutboxURI)
+ if err != nil {
+ return fmt.Errorf("federateFave: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
+ }
+ _, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, asFave)
+ return err
+}
+
+func (p *processor) federateAnnounce(boostWrapperStatus *gtsmodel.Status, boostingAccount *gtsmodel.Account, boostedAccount *gtsmodel.Account) error {
+ announce, err := p.tc.BoostToAS(boostWrapperStatus, boostingAccount, boostedAccount)
+ if err != nil {
+ return fmt.Errorf("federateAnnounce: error converting status to announce: %s", err)
+ }
+
+ outboxIRI, err := url.Parse(boostingAccount.OutboxURI)
+ if err != nil {
+ return fmt.Errorf("federateAnnounce: error parsing outboxURI %s: %s", boostingAccount.OutboxURI, err)
+ }
+
+ _, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, announce)
+ return err
+}
+
+func (p *processor) federateAccountUpdate(updatedAccount *gtsmodel.Account, originAccount *gtsmodel.Account) error {
+ person, err := p.tc.AccountToAS(updatedAccount)
+ if err != nil {
+ return fmt.Errorf("federateAccountUpdate: error converting account to person: %s", err)
+ }
+
+ update, err := p.tc.WrapPersonInUpdate(person, originAccount)
+ if err != nil {
+ return fmt.Errorf("federateAccountUpdate: error wrapping person in update: %s", err)
+ }
+
+ outboxIRI, err := url.Parse(originAccount.OutboxURI)
+ if err != nil {
+ return fmt.Errorf("federateAnnounce: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
+ }
+
+ _, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, update)
+ return err
+}
diff --git a/internal/processing/fromcommon.go b/internal/processing/fromcommon.go
@@ -0,0 +1,164 @@
+/*
+ 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 processing
+
+import (
+ "fmt"
+
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) notifyStatus(status *gtsmodel.Status) error {
+ // if there are no mentions in this status then just bail
+ if len(status.Mentions) == 0 {
+ return nil
+ }
+
+ if status.GTSMentions == nil {
+ // there are mentions but they're not fully populated on the status yet so do this
+ menchies := []*gtsmodel.Mention{}
+ for _, m := range status.Mentions {
+ gtsm := >smodel.Mention{}
+ if err := p.db.GetByID(m, gtsm); err != nil {
+ return fmt.Errorf("notifyStatus: error getting mention with id %s from the db: %s", m, err)
+ }
+ menchies = append(menchies, gtsm)
+ }
+ status.GTSMentions = menchies
+ }
+
+ // now we have mentions as full gtsmodel.Mention structs on the status we can continue
+ for _, m := range status.GTSMentions {
+ // make sure this is a local account, otherwise we don't need to create a notification for it
+ if m.GTSAccount == nil {
+ a := >smodel.Account{}
+ if err := p.db.GetByID(m.TargetAccountID, a); err != nil {
+ // we don't have the account or there's been an error
+ return fmt.Errorf("notifyStatus: error getting account with id %s from the db: %s", m.TargetAccountID, err)
+ }
+ m.GTSAccount = a
+ }
+ if m.GTSAccount.Domain != "" {
+ // not a local account so skip it
+ continue
+ }
+
+ // make sure a notif doesn't already exist for this mention
+ err := p.db.GetWhere([]db.Where{
+ {Key: "notification_type", Value: gtsmodel.NotificationMention},
+ {Key: "target_account_id", Value: m.TargetAccountID},
+ {Key: "origin_account_id", Value: status.AccountID},
+ {Key: "status_id", Value: status.ID},
+ }, >smodel.Notification{})
+ if err == nil {
+ // notification exists already so just continue
+ continue
+ }
+ if _, ok := err.(db.ErrNoEntries); !ok {
+ // there's a real error in the db
+ return fmt.Errorf("notifyStatus: error checking existence of notification for mention with id %s : %s", m.ID, err)
+ }
+
+ // if we've reached this point we know the mention is for a local account, and the notification doesn't exist, so create it
+ notif := >smodel.Notification{
+ NotificationType: gtsmodel.NotificationMention,
+ TargetAccountID: m.TargetAccountID,
+ OriginAccountID: status.AccountID,
+ StatusID: status.ID,
+ }
+
+ if err := p.db.Put(notif); err != nil {
+ return fmt.Errorf("notifyStatus: error putting notification in database: %s", err)
+ }
+ }
+
+ return nil
+}
+
+func (p *processor) notifyFollowRequest(followRequest *gtsmodel.FollowRequest, receivingAccount *gtsmodel.Account) error {
+ // return if this isn't a local account
+ if receivingAccount.Domain != "" {
+ return nil
+ }
+
+ notif := >smodel.Notification{
+ NotificationType: gtsmodel.NotificationFollowRequest,
+ TargetAccountID: followRequest.TargetAccountID,
+ OriginAccountID: followRequest.AccountID,
+ }
+
+ if err := p.db.Put(notif); err != nil {
+ return fmt.Errorf("notifyFollowRequest: error putting notification in database: %s", err)
+ }
+
+ return nil
+}
+
+func (p *processor) notifyFollow(follow *gtsmodel.Follow, receivingAccount *gtsmodel.Account) error {
+ // return if this isn't a local account
+ if receivingAccount.Domain != "" {
+ return nil
+ }
+
+ // first remove the follow request notification
+ if err := p.db.DeleteWhere([]db.Where{
+ {Key: "notification_type", Value: gtsmodel.NotificationFollowRequest},
+ {Key: "target_account_id", Value: follow.TargetAccountID},
+ {Key: "origin_account_id", Value: follow.AccountID},
+ }, >smodel.Notification{}); err != nil {
+ return fmt.Errorf("notifyFollow: error removing old follow request notification from database: %s", err)
+ }
+
+ // now create the new follow notification
+ notif := >smodel.Notification{
+ NotificationType: gtsmodel.NotificationFollow,
+ TargetAccountID: follow.TargetAccountID,
+ OriginAccountID: follow.AccountID,
+ }
+ if err := p.db.Put(notif); err != nil {
+ return fmt.Errorf("notifyFollow: error putting notification in database: %s", err)
+ }
+
+ return nil
+}
+
+func (p *processor) notifyFave(fave *gtsmodel.StatusFave, receivingAccount *gtsmodel.Account) error {
+ // return if this isn't a local account
+ if receivingAccount.Domain != "" {
+ return nil
+ }
+
+ notif := >smodel.Notification{
+ NotificationType: gtsmodel.NotificationFave,
+ TargetAccountID: fave.TargetAccountID,
+ OriginAccountID: fave.AccountID,
+ StatusID: fave.StatusID,
+ }
+
+ if err := p.db.Put(notif); err != nil {
+ return fmt.Errorf("notifyFave: error putting notification in database: %s", err)
+ }
+
+ return nil
+}
+
+func (p *processor) notifyAnnounce(status *gtsmodel.Status) error {
+ return nil
+}
diff --git a/internal/processing/fromfederator.go b/internal/processing/fromfederator.go
@@ -0,0 +1,433 @@
+/*
+ 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 processing
+
+import (
+ "errors"
+ "fmt"
+ "net/url"
+
+ "github.com/google/uuid"
+ "github.com/sirupsen/logrus"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) error {
+ l := p.log.WithFields(logrus.Fields{
+ "func": "processFromFederator",
+ "federatorMsg": fmt.Sprintf("%+v", federatorMsg),
+ })
+
+ l.Debug("entering function PROCESS FROM FEDERATOR")
+
+ switch federatorMsg.APActivityType {
+ case gtsmodel.ActivityStreamsCreate:
+ // CREATE
+ switch federatorMsg.APObjectType {
+ case gtsmodel.ActivityStreamsNote:
+ // CREATE A STATUS
+ incomingStatus, ok := federatorMsg.GTSModel.(*gtsmodel.Status)
+ if !ok {
+ return errors.New("note was not parseable as *gtsmodel.Status")
+ }
+
+ l.Debug("will now derefence incoming status")
+ if err := p.dereferenceStatusFields(incomingStatus, federatorMsg.ReceivingAccount.Username); err != nil {
+ return fmt.Errorf("error dereferencing status from federator: %s", err)
+ }
+ if err := p.db.UpdateByID(incomingStatus.ID, incomingStatus); err != nil {
+ return fmt.Errorf("error updating dereferenced status in the db: %s", err)
+ }
+
+ if err := p.notifyStatus(incomingStatus); err != nil {
+ return err
+ }
+ case gtsmodel.ActivityStreamsProfile:
+ // CREATE AN ACCOUNT
+ incomingAccount, ok := federatorMsg.GTSModel.(*gtsmodel.Account)
+ if !ok {
+ return errors.New("profile was not parseable as *gtsmodel.Account")
+ }
+
+ l.Debug("will now derefence incoming account")
+ if err := p.dereferenceAccountFields(incomingAccount, "", false); err != nil {
+ return fmt.Errorf("error dereferencing account from federator: %s", err)
+ }
+ if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil {
+ return fmt.Errorf("error updating dereferenced account in the db: %s", err)
+ }
+ case gtsmodel.ActivityStreamsLike:
+ // CREATE A FAVE
+ incomingFave, ok := federatorMsg.GTSModel.(*gtsmodel.StatusFave)
+ if !ok {
+ return errors.New("like was not parseable as *gtsmodel.StatusFave")
+ }
+
+ if err := p.notifyFave(incomingFave, federatorMsg.ReceivingAccount); err != nil {
+ return err
+ }
+ case gtsmodel.ActivityStreamsFollow:
+ // CREATE A FOLLOW REQUEST
+ incomingFollowRequest, ok := federatorMsg.GTSModel.(*gtsmodel.FollowRequest)
+ if !ok {
+ return errors.New("incomingFollowRequest was not parseable as *gtsmodel.FollowRequest")
+ }
+
+ if err := p.notifyFollowRequest(incomingFollowRequest, federatorMsg.ReceivingAccount); err != nil {
+ return err
+ }
+ case gtsmodel.ActivityStreamsAnnounce:
+ // CREATE AN ANNOUNCE
+ incomingAnnounce, ok := federatorMsg.GTSModel.(*gtsmodel.Status)
+ if !ok {
+ return errors.New("announce was not parseable as *gtsmodel.Status")
+ }
+
+ if err := p.dereferenceAnnounce(incomingAnnounce, federatorMsg.ReceivingAccount.Username); err != nil {
+ return fmt.Errorf("error dereferencing announce from federator: %s", err)
+ }
+
+ if err := p.db.Put(incomingAnnounce); err != nil {
+ if _, ok := err.(db.ErrAlreadyExists); !ok {
+ return fmt.Errorf("error adding dereferenced announce to the db: %s", err)
+ }
+ }
+
+ if err := p.notifyAnnounce(incomingAnnounce); err != nil {
+ return err
+ }
+ }
+ case gtsmodel.ActivityStreamsUpdate:
+ // UPDATE
+ switch federatorMsg.APObjectType {
+ case gtsmodel.ActivityStreamsProfile:
+ // UPDATE AN ACCOUNT
+ incomingAccount, ok := federatorMsg.GTSModel.(*gtsmodel.Account)
+ if !ok {
+ return errors.New("profile was not parseable as *gtsmodel.Account")
+ }
+
+ l.Debug("will now derefence incoming account")
+ if err := p.dereferenceAccountFields(incomingAccount, federatorMsg.ReceivingAccount.Username, true); err != nil {
+ return fmt.Errorf("error dereferencing account from federator: %s", err)
+ }
+ if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil {
+ return fmt.Errorf("error updating dereferenced account in the db: %s", err)
+ }
+ }
+ case gtsmodel.ActivityStreamsDelete:
+ // DELETE
+ switch federatorMsg.APObjectType {
+ case gtsmodel.ActivityStreamsNote:
+ // DELETE A STATUS
+ // TODO: handle side effects of status deletion here:
+ // 1. delete all media associated with status
+ // 2. delete boosts of status
+ // 3. etc etc etc
+ case gtsmodel.ActivityStreamsProfile:
+ // DELETE A PROFILE/ACCOUNT
+ // TODO: handle side effects of account deletion here: delete all objects, statuses, media etc associated with account
+ }
+ case gtsmodel.ActivityStreamsAccept:
+ // ACCEPT
+ switch federatorMsg.APObjectType {
+ case gtsmodel.ActivityStreamsFollow:
+ // ACCEPT A FOLLOW
+ follow, ok := federatorMsg.GTSModel.(*gtsmodel.Follow)
+ if !ok {
+ return errors.New("follow was not parseable as *gtsmodel.Follow")
+ }
+
+ if err := p.notifyFollow(follow, federatorMsg.ReceivingAccount); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+// dereferenceStatusFields fetches all the information we temporarily pinned to an incoming
+// federated status, back in the federating db's Create function.
+//
+// When a status comes in from the federation API, there are certain fields that
+// haven't been dereferenced yet, because we needed to provide a snappy synchronous
+// response to the caller. By the time it reaches this function though, it's being
+// processed asynchronously, so we have all the time in the world to fetch the various
+// bits and bobs that are attached to the status, and properly flesh it out, before we
+// send the status to any timelines and notify people.
+//
+// Things to dereference and fetch here:
+//
+// 1. Media attachments.
+// 2. Hashtags.
+// 3. Emojis.
+// 4. Mentions.
+// 5. Posting account.
+// 6. Replied-to-status.
+//
+// SIDE EFFECTS:
+// This function will deference all of the above, insert them in the database as necessary,
+// and attach them to the status. The status itself will not be added to the database yet,
+// that's up the caller to do.
+func (p *processor) dereferenceStatusFields(status *gtsmodel.Status, requestingUsername string) error {
+ l := p.log.WithFields(logrus.Fields{
+ "func": "dereferenceStatusFields",
+ "status": fmt.Sprintf("%+v", status),
+ })
+ l.Debug("entering function")
+
+ t, err := p.federator.GetTransportForUser(requestingUsername)
+ if err != nil {
+ return fmt.Errorf("error creating transport: %s", err)
+ }
+
+ // the status should have an ID by now, but just in case it doesn't let's generate one here
+ // because we'll need it further down
+ if status.ID == "" {
+ status.ID = uuid.NewString()
+ }
+
+ // 1. Media attachments.
+ //
+ // At this point we should know:
+ // * the media type of the file we're looking for (a.File.ContentType)
+ // * the blurhash (a.Blurhash)
+ // * the file type (a.Type)
+ // * the remote URL (a.RemoteURL)
+ // This should be enough to pass along to the media processor.
+ attachmentIDs := []string{}
+ for _, a := range status.GTSMediaAttachments {
+ l.Debugf("dereferencing attachment: %+v", a)
+
+ // it might have been processed elsewhere so check first if it's already in the database or not
+ maybeAttachment := >smodel.MediaAttachment{}
+ err := p.db.GetWhere([]db.Where{{Key: "remote_url", Value: a.RemoteURL}}, maybeAttachment)
+ if err == nil {
+ // we already have it in the db, dereferenced, no need to do it again
+ l.Debugf("attachment already exists with id %s", maybeAttachment.ID)
+ attachmentIDs = append(attachmentIDs, maybeAttachment.ID)
+ continue
+ }
+ if _, ok := err.(db.ErrNoEntries); !ok {
+ // we have a real error
+ return fmt.Errorf("error checking db for existence of attachment with remote url %s: %s", a.RemoteURL, err)
+ }
+ // it just doesn't exist yet so carry on
+ l.Debug("attachment doesn't exist yet, calling ProcessRemoteAttachment", a)
+ deferencedAttachment, err := p.mediaHandler.ProcessRemoteAttachment(t, a, status.AccountID)
+ if err != nil {
+ p.log.Errorf("error dereferencing status attachment: %s", err)
+ continue
+ }
+ l.Debugf("dereferenced attachment: %+v", deferencedAttachment)
+ deferencedAttachment.StatusID = status.ID
+ deferencedAttachment.Description = a.Description
+ if err := p.db.Put(deferencedAttachment); err != nil {
+ return fmt.Errorf("error inserting dereferenced attachment with remote url %s: %s", a.RemoteURL, err)
+ }
+ attachmentIDs = append(attachmentIDs, deferencedAttachment.ID)
+ }
+ status.Attachments = attachmentIDs
+
+ // 2. Hashtags
+
+ // 3. Emojis
+
+ // 4. Mentions
+ // At this point, mentions should have the namestring and mentionedAccountURI set on them.
+ //
+ // We should dereference any accounts mentioned here which we don't have in our db yet, by their URI.
+ mentions := []string{}
+ for _, m := range status.GTSMentions {
+ uri, err := url.Parse(m.MentionedAccountURI)
+ if err != nil {
+ l.Debugf("error parsing mentioned account uri %s: %s", m.MentionedAccountURI, err)
+ continue
+ }
+
+ m.StatusID = status.ID
+ m.OriginAccountID = status.GTSAuthorAccount.ID
+ m.OriginAccountURI = status.GTSAuthorAccount.URI
+
+ targetAccount := >smodel.Account{}
+ if err := p.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, targetAccount); err != nil {
+ // proper error
+ if _, ok := err.(db.ErrNoEntries); !ok {
+ return fmt.Errorf("db error checking for account with uri %s", uri.String())
+ }
+
+ // we just don't have it yet, so we should go get it....
+ accountable, err := p.federator.DereferenceRemoteAccount(requestingUsername, uri)
+ if err != nil {
+ // we can't dereference it so just skip it
+ l.Debugf("error dereferencing remote account with uri %s: %s", uri.String(), err)
+ continue
+ }
+
+ targetAccount, err = p.tc.ASRepresentationToAccount(accountable, false)
+ if err != nil {
+ l.Debugf("error converting remote account with uri %s into gts model: %s", uri.String(), err)
+ continue
+ }
+
+ if err := p.db.Put(targetAccount); err != nil {
+ return fmt.Errorf("db error inserting account with uri %s", uri.String())
+ }
+ }
+
+ // by this point, we know the targetAccount exists in our database with an ID :)
+ m.TargetAccountID = targetAccount.ID
+ if err := p.db.Put(m); err != nil {
+ return fmt.Errorf("error creating mention: %s", err)
+ }
+ mentions = append(mentions, m.ID)
+ }
+ status.Mentions = mentions
+
+ return nil
+}
+
+func (p *processor) dereferenceAccountFields(account *gtsmodel.Account, requestingUsername string, refresh bool) error {
+ l := p.log.WithFields(logrus.Fields{
+ "func": "dereferenceAccountFields",
+ "requestingUsername": requestingUsername,
+ })
+
+ t, err := p.federator.GetTransportForUser(requestingUsername)
+ if err != nil {
+ return fmt.Errorf("error getting transport for user: %s", err)
+ }
+
+ // fetch the header and avatar
+ if err := p.fetchHeaderAndAviForAccount(account, t, refresh); err != nil {
+ // if this doesn't work, just skip it -- we can do it later
+ l.Debugf("error fetching header/avi for account: %s", err)
+ }
+
+ if err := p.db.UpdateByID(account.ID, account); err != nil {
+ return fmt.Errorf("error updating account in database: %s", err)
+ }
+
+ return nil
+}
+
+func (p *processor) dereferenceAnnounce(announce *gtsmodel.Status, requestingUsername string) error {
+ if announce.GTSBoostedStatus == nil || announce.GTSBoostedStatus.URI == "" {
+ // we can't do anything unfortunately
+ return errors.New("dereferenceAnnounce: no URI to dereference")
+ }
+
+ // check if we already have the boosted status in the database
+ boostedStatus := >smodel.Status{}
+ err := p.db.GetWhere([]db.Where{{Key: "uri", Value: announce.GTSBoostedStatus.URI}}, boostedStatus)
+ if err == nil {
+ // nice, we already have it so we don't actually need to dereference it from remote
+ announce.Content = boostedStatus.Content
+ announce.ContentWarning = boostedStatus.ContentWarning
+ announce.ActivityStreamsType = boostedStatus.ActivityStreamsType
+ announce.Sensitive = boostedStatus.Sensitive
+ announce.Language = boostedStatus.Language
+ announce.Text = boostedStatus.Text
+ announce.BoostOfID = boostedStatus.ID
+ announce.Visibility = boostedStatus.Visibility
+ announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced
+ announce.GTSBoostedStatus = boostedStatus
+ return nil
+ }
+
+ // we don't have it so we need to dereference it
+ remoteStatusID, err := url.Parse(announce.GTSBoostedStatus.URI)
+ if err != nil {
+ return fmt.Errorf("dereferenceAnnounce: error parsing url %s: %s", announce.GTSBoostedStatus.URI, err)
+ }
+
+ statusable, err := p.federator.DereferenceRemoteStatus(requestingUsername, remoteStatusID)
+ if err != nil {
+ return fmt.Errorf("dereferenceAnnounce: error dereferencing remote status with id %s: %s", announce.GTSBoostedStatus.URI, err)
+ }
+
+ // make sure we have the author account in the db
+ attributedToProp := statusable.GetActivityStreamsAttributedTo()
+ for iter := attributedToProp.Begin(); iter != attributedToProp.End(); iter = iter.Next() {
+ accountURI := iter.GetIRI()
+ if accountURI == nil {
+ continue
+ }
+
+ if err := p.db.GetWhere([]db.Where{{Key: "uri", Value: accountURI.String()}}, >smodel.Account{}); err == nil {
+ // we already have it, fine
+ continue
+ }
+
+ // we don't have the boosted status author account yet so dereference it
+ accountable, err := p.federator.DereferenceRemoteAccount(requestingUsername, accountURI)
+ if err != nil {
+ return fmt.Errorf("dereferenceAnnounce: error dereferencing remote account with id %s: %s", accountURI.String(), err)
+ }
+ account, err := p.tc.ASRepresentationToAccount(accountable, false)
+ if err != nil {
+ return fmt.Errorf("dereferenceAnnounce: error converting dereferenced account with id %s into account : %s", accountURI.String(), err)
+ }
+
+ // insert the dereferenced account so it gets an ID etc
+ if err := p.db.Put(account); err != nil {
+ return fmt.Errorf("dereferenceAnnounce: error putting dereferenced account with id %s into database : %s", accountURI.String(), err)
+ }
+
+ if err := p.dereferenceAccountFields(account, requestingUsername, false); err != nil {
+ return fmt.Errorf("dereferenceAnnounce: error dereferencing fields on account with id %s : %s", accountURI.String(), err)
+ }
+ }
+
+ // now convert the statusable into something we can understand
+ boostedStatus, err = p.tc.ASStatusToStatus(statusable)
+ if err != nil {
+ return fmt.Errorf("dereferenceAnnounce: error converting dereferenced statusable with id %s into status : %s", announce.GTSBoostedStatus.URI, err)
+ }
+
+ // put it in the db already so it gets an ID generated for it
+ if err := p.db.Put(boostedStatus); err != nil {
+ return fmt.Errorf("dereferenceAnnounce: error putting dereferenced status with id %s into the db: %s", announce.GTSBoostedStatus.URI, err)
+ }
+
+ // now dereference additional fields straight away (we're already async here so we have time)
+ if err := p.dereferenceStatusFields(boostedStatus, requestingUsername); err != nil {
+ return fmt.Errorf("dereferenceAnnounce: error dereferencing status fields for status with id %s: %s", announce.GTSBoostedStatus.URI, err)
+ }
+
+ // update with the newly dereferenced fields
+ if err := p.db.UpdateByID(boostedStatus.ID, boostedStatus); err != nil {
+ return fmt.Errorf("dereferenceAnnounce: error updating dereferenced status in the db: %s", err)
+ }
+
+ // we have everything we need!
+ announce.Content = boostedStatus.Content
+ announce.ContentWarning = boostedStatus.ContentWarning
+ announce.ActivityStreamsType = boostedStatus.ActivityStreamsType
+ announce.Sensitive = boostedStatus.Sensitive
+ announce.Language = boostedStatus.Language
+ announce.Text = boostedStatus.Text
+ announce.BoostOfID = boostedStatus.ID
+ announce.Visibility = boostedStatus.Visibility
+ announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced
+ announce.GTSBoostedStatus = boostedStatus
+ return nil
+}
diff --git a/internal/processing/instance.go b/internal/processing/instance.go
@@ -0,0 +1,41 @@
+/*
+ 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 processing
+
+import (
+ "fmt"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) InstanceGet(domain string) (*apimodel.Instance, ErrorWithCode) {
+ i := >smodel.Instance{}
+ if err := p.db.GetWhere([]db.Where{{Key: "domain", Value: domain}}, i); err != nil {
+ return nil, NewErrorInternalError(fmt.Errorf("db error fetching instance %s: %s", p.config.Host, err))
+ }
+
+ ai, err := p.tc.InstanceToMasto(i)
+ if err != nil {
+ return nil, NewErrorInternalError(fmt.Errorf("error converting instance to api representation: %s", err))
+ }
+
+ return ai, nil
+}
diff --git a/internal/processing/media.go b/internal/processing/media.go
@@ -0,0 +1,285 @@
+/*
+ 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 processing
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "strconv"
+ "strings"
+
+ 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/media"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+func (p *processor) MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) {
+ // First check this user/account is permitted to create media
+ // There's no point continuing otherwise.
+ //
+ // TODO: move this check to the oauth.Authed function and do it for all accounts
+ if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
+ return nil, errors.New("not authorized to post new media")
+ }
+
+ // open the attachment and extract the bytes from it
+ f, err := form.File.Open()
+ if err != nil {
+ return nil, fmt.Errorf("error opening attachment: %s", err)
+ }
+ buf := new(bytes.Buffer)
+ size, err := io.Copy(buf, f)
+ if err != nil {
+ return nil, fmt.Errorf("error reading attachment: %s", err)
+
+ }
+ if size == 0 {
+ return nil, errors.New("could not read provided attachment: size 0 bytes")
+ }
+
+ // allow the mediaHandler to work its magic of processing the attachment bytes, and putting them in whatever storage backend we're using
+ attachment, err := p.mediaHandler.ProcessAttachment(buf.Bytes(), authed.Account.ID, "")
+ if err != nil {
+ return nil, fmt.Errorf("error reading attachment: %s", err)
+ }
+
+ // now we need to add extra fields that the attachment processor doesn't know (from the form)
+ // TODO: handle this inside mediaHandler.ProcessAttachment (just pass more params to it)
+
+ // first description
+ attachment.Description = form.Description
+
+ // now parse the focus parameter
+ focusx, focusy, err := parseFocus(form.Focus)
+ if err != nil {
+ return nil, err
+ }
+ attachment.FileMeta.Focus.X = focusx
+ attachment.FileMeta.Focus.Y = focusy
+
+ // prepare the frontend representation now -- if there are any errors here at least we can bail without
+ // having already put something in the database and then having to clean it up again (eugh)
+ mastoAttachment, err := p.tc.AttachmentToMasto(attachment)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing media attachment to frontend type: %s", err)
+ }
+
+ // now we can confidently put the attachment in the database
+ if err := p.db.Put(attachment); err != nil {
+ return nil, fmt.Errorf("error storing media attachment in db: %s", err)
+ }
+
+ return &mastoAttachment, nil
+}
+
+func (p *processor) MediaGet(authed *oauth.Auth, mediaAttachmentID string) (*apimodel.Attachment, ErrorWithCode) {
+ attachment := >smodel.MediaAttachment{}
+ if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ // attachment doesn't exist
+ return nil, NewErrorNotFound(errors.New("attachment doesn't exist in the db"))
+ }
+ return nil, NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err))
+ }
+
+ if attachment.AccountID != authed.Account.ID {
+ return nil, NewErrorNotFound(errors.New("attachment not owned by requesting account"))
+ }
+
+ a, err := p.tc.AttachmentToMasto(attachment)
+ if err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err))
+ }
+
+ return &a, nil
+}
+
+func (p *processor) MediaUpdate(authed *oauth.Auth, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, ErrorWithCode) {
+ attachment := >smodel.MediaAttachment{}
+ if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ // attachment doesn't exist
+ return nil, NewErrorNotFound(errors.New("attachment doesn't exist in the db"))
+ }
+ return nil, NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err))
+ }
+
+ if attachment.AccountID != authed.Account.ID {
+ return nil, NewErrorNotFound(errors.New("attachment not owned by requesting account"))
+ }
+
+ if form.Description != nil {
+ attachment.Description = *form.Description
+ if err := p.db.UpdateByID(mediaAttachmentID, attachment); err != nil {
+ return nil, NewErrorInternalError(fmt.Errorf("database error updating description: %s", err))
+ }
+ }
+
+ if form.Focus != nil {
+ focusx, focusy, err := parseFocus(*form.Focus)
+ if err != nil {
+ return nil, NewErrorBadRequest(err)
+ }
+ attachment.FileMeta.Focus.X = focusx
+ attachment.FileMeta.Focus.Y = focusy
+ if err := p.db.UpdateByID(mediaAttachmentID, attachment); err != nil {
+ return nil, NewErrorInternalError(fmt.Errorf("database error updating focus: %s", err))
+ }
+ }
+
+ a, err := p.tc.AttachmentToMasto(attachment)
+ if err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err))
+ }
+
+ return &a, nil
+}
+
+func (p *processor) FileGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) {
+ // parse the form fields
+ mediaSize, err := media.ParseMediaSize(form.MediaSize)
+ if err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("media size %s not valid", form.MediaSize))
+ }
+
+ mediaType, err := media.ParseMediaType(form.MediaType)
+ if err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("media type %s not valid", form.MediaType))
+ }
+
+ spl := strings.Split(form.FileName, ".")
+ if len(spl) != 2 || spl[0] == "" || spl[1] == "" {
+ return nil, NewErrorNotFound(fmt.Errorf("file name %s not parseable", form.FileName))
+ }
+ wantedMediaID := spl[0]
+
+ // get the account that owns the media and make sure it's not suspended
+ acct := >smodel.Account{}
+ if err := p.db.GetByID(form.AccountID, acct); err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("account with id %s could not be selected from the db: %s", form.AccountID, err))
+ }
+ if !acct.SuspendedAt.IsZero() {
+ return nil, NewErrorNotFound(fmt.Errorf("account with id %s is suspended", form.AccountID))
+ }
+
+ // make sure the requesting account and the media account don't block each other
+ if authed.Account != nil {
+ blocked, err := p.db.Blocked(authed.Account.ID, form.AccountID)
+ if err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("block status could not be established between accounts %s and %s: %s", form.AccountID, authed.Account.ID, err))
+ }
+ if blocked {
+ return nil, NewErrorNotFound(fmt.Errorf("block exists between accounts %s and %s", form.AccountID, authed.Account.ID))
+ }
+ }
+
+ // the way we store emojis is a little different from the way we store other attachments,
+ // so we need to take different steps depending on the media type being requested
+ content := &apimodel.Content{}
+ var storagePath string
+ switch mediaType {
+ case media.Emoji:
+ e := >smodel.Emoji{}
+ if err := p.db.GetByID(wantedMediaID, e); err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("emoji %s could not be taken from the db: %s", wantedMediaID, err))
+ }
+ if e.Disabled {
+ return nil, NewErrorNotFound(fmt.Errorf("emoji %s has been disabled", wantedMediaID))
+ }
+ switch mediaSize {
+ case media.Original:
+ content.ContentType = e.ImageContentType
+ storagePath = e.ImagePath
+ case media.Static:
+ content.ContentType = e.ImageStaticContentType
+ storagePath = e.ImageStaticPath
+ default:
+ return nil, NewErrorNotFound(fmt.Errorf("media size %s not recognized for emoji", mediaSize))
+ }
+ case media.Attachment, media.Header, media.Avatar:
+ a := >smodel.MediaAttachment{}
+ if err := p.db.GetByID(wantedMediaID, a); err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("attachment %s could not be taken from the db: %s", wantedMediaID, err))
+ }
+ if a.AccountID != form.AccountID {
+ return nil, NewErrorNotFound(fmt.Errorf("attachment %s is not owned by %s", wantedMediaID, form.AccountID))
+ }
+ switch mediaSize {
+ case media.Original:
+ content.ContentType = a.File.ContentType
+ storagePath = a.File.Path
+ case media.Small:
+ content.ContentType = a.Thumbnail.ContentType
+ storagePath = a.Thumbnail.Path
+ default:
+ return nil, NewErrorNotFound(fmt.Errorf("media size %s not recognized for attachment", mediaSize))
+ }
+ }
+
+ bytes, err := p.storage.RetrieveFileFrom(storagePath)
+ if err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("error retrieving from storage: %s", err))
+ }
+
+ content.ContentLength = int64(len(bytes))
+ content.Content = bytes
+ return content, nil
+}
+
+func parseFocus(focus string) (focusx, focusy float32, err error) {
+ if focus == "" {
+ return
+ }
+ spl := strings.Split(focus, ",")
+ if len(spl) != 2 {
+ err = fmt.Errorf("improperly formatted focus %s", focus)
+ return
+ }
+ xStr := spl[0]
+ yStr := spl[1]
+ if xStr == "" || yStr == "" {
+ err = fmt.Errorf("improperly formatted focus %s", focus)
+ return
+ }
+ fx, err := strconv.ParseFloat(xStr, 32)
+ if err != nil {
+ err = fmt.Errorf("improperly formatted focus %s: %s", focus, err)
+ return
+ }
+ if fx > 1 || fx < -1 {
+ err = fmt.Errorf("improperly formatted focus %s", focus)
+ return
+ }
+ focusx = float32(fx)
+ fy, err := strconv.ParseFloat(yStr, 32)
+ if err != nil {
+ err = fmt.Errorf("improperly formatted focus %s: %s", focus, err)
+ return
+ }
+ if fy > 1 || fy < -1 {
+ err = fmt.Errorf("improperly formatted focus %s", focus)
+ return
+ }
+ focusy = float32(fy)
+ return
+}
diff --git a/internal/processing/notification.go b/internal/processing/notification.go
@@ -0,0 +1,45 @@
+/*
+ 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 processing
+
+import (
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+func (p *processor) NotificationsGet(authed *oauth.Auth, limit int, maxID string) ([]*apimodel.Notification, ErrorWithCode) {
+ l := p.log.WithField("func", "NotificationsGet")
+
+ notifs, err := p.db.GetNotificationsForAccount(authed.Account.ID, limit, maxID)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+
+ mastoNotifs := []*apimodel.Notification{}
+ for _, n := range notifs {
+ mastoNotif, err := p.tc.NotificationToMasto(n)
+ if err != nil {
+ l.Debugf("got an error converting a notification to masto, will skip it: %s", err)
+ continue
+ }
+ mastoNotifs = append(mastoNotifs, mastoNotif)
+ }
+
+ return mastoNotifs, nil
+}
diff --git a/internal/processing/processor.go b/internal/processing/processor.go
@@ -0,0 +1,255 @@
+/*
+ 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 processing
+
+import (
+ "context"
+ "net/http"
+
+ "github.com/sirupsen/logrus"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/blob"
+ "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/media"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+)
+
+// Processor should be passed to api modules (see internal/apimodule/...). It is used for
+// passing messages back and forth from the client API and the federating interface, via channels.
+// It also contains logic for filtering which messages should end up where.
+// It is designed to be used asynchronously: the client API and the federating API should just be able to
+// fire messages into the processor and not wait for a reply before proceeding with other work. This allows
+// for clean distribution of messages without slowing down the client API and harming the user experience.
+type Processor interface {
+ // ToClientAPI returns a channel for putting in messages that need to go to the gts client API.
+ // ToClientAPI() chan gtsmodel.ToClientAPI
+ // FromClientAPI returns a channel for putting messages in that come from the client api going to the processor
+ FromClientAPI() chan gtsmodel.FromClientAPI
+ // ToFederator returns a channel for putting in messages that need to go to the federator (activitypub).
+ // ToFederator() chan gtsmodel.ToFederator
+ // FromFederator returns a channel for putting messages in that come from the federator (activitypub) going into the processor
+ FromFederator() chan gtsmodel.FromFederator
+ // Start starts the Processor, reading from its channels and passing messages back and forth.
+ Start() error
+ // Stop stops the processor cleanly, finishing handling any remaining messages before closing down.
+ Stop() error
+
+ /*
+ CLIENT API-FACING PROCESSING FUNCTIONS
+ These functions are intended to be called when the API client needs an immediate (ie., synchronous) reply
+ to an HTTP request. As such, they will only do the bare-minimum of work necessary to give a properly
+ formed reply. For more intensive (and time-consuming) calls, where you don't require an immediate
+ response, pass work to the processor using a channel instead.
+ */
+
+ // AccountCreate processes the given form for creating a new account, returning an oauth token for that account if successful.
+ AccountCreate(authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, error)
+ // AccountGet processes the given request for account information.
+ AccountGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Account, error)
+ // AccountUpdate processes the update of an account with the given form
+ AccountUpdate(authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error)
+ // AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
+ // the account given in authed.
+ AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, ErrorWithCode)
+ // AccountFollowersGet fetches a list of the target account's followers.
+ AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode)
+ // AccountFollowingGet fetches a list of the accounts that target account is following.
+ AccountFollowingGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode)
+ // AccountRelationshipGet returns a relationship model describing the relationship of the targetAccount to the Authed account.
+ AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode)
+ // AccountFollowCreate handles a follow request to an account, either remote or local.
+ AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, ErrorWithCode)
+ // AccountFollowRemove handles the removal of a follow/follow request to an account, either remote or local.
+ AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode)
+
+ // AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form.
+ AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error)
+
+ // AppCreate processes the creation of a new API application
+ AppCreate(authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error)
+
+ // 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) (*apimodel.Relationship, ErrorWithCode)
+
+ // InstanceGet retrieves instance information for serving at api/v1/instance
+ InstanceGet(domain string) (*apimodel.Instance, ErrorWithCode)
+
+ // MediaCreate handles the creation of a media attachment, using the given form.
+ MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error)
+ // MediaGet handles the GET of a media attachment with the given ID
+ MediaGet(authed *oauth.Auth, attachmentID string) (*apimodel.Attachment, ErrorWithCode)
+ // MediaUpdate handles the PUT of a media attachment with the given ID and form
+ MediaUpdate(authed *oauth.Auth, attachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, ErrorWithCode)
+
+ // NotificationsGet
+ NotificationsGet(authed *oauth.Auth, limit int, maxID string) ([]*apimodel.Notification, ErrorWithCode)
+
+ // SearchGet performs a search with the given params, resolving/dereferencing remotely as desired
+ SearchGet(authed *oauth.Auth, searchQuery *apimodel.SearchQuery) (*apimodel.SearchResult, ErrorWithCode)
+
+ // StatusCreate processes the given form to create a new status, returning the api model representation of that status if it's OK.
+ StatusCreate(authed *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, error)
+ // StatusDelete processes the delete of a given status, returning the deleted status if the delete goes through.
+ StatusDelete(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error)
+ // StatusFave processes the faving of a given status, returning the updated status if the fave goes through.
+ StatusFave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error)
+ // StatusBoost processes the boost/reblog of a given status, returning the newly-created boost if all is well.
+ StatusBoost(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, ErrorWithCode)
+ // StatusFavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings.
+ StatusFavedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, error)
+ // StatusGet gets the given status, taking account of privacy settings and blocks etc.
+ StatusGet(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error)
+ // StatusUnfave processes the unfaving of a given status, returning the updated status if the fave goes through.
+ StatusUnfave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error)
+
+ // HomeTimelineGet returns statuses from the home timeline, with the given filters/parameters.
+ HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) ([]apimodel.Status, ErrorWithCode)
+
+ /*
+ FEDERATION API-FACING PROCESSING FUNCTIONS
+ These functions are intended to be called when the federating client needs an immediate (ie., synchronous) reply
+ to an HTTP request. As such, they will only do the bare-minimum of work necessary to give a properly
+ formed reply. For more intensive (and time-consuming) calls, where you don't require an immediate
+ response, pass work to the processor using a channel instead.
+ */
+
+ // GetFediUser handles the getting of a fedi/activitypub representation of a user/account, performing appropriate authentication
+ // before returning a JSON serializable interface to the caller.
+ GetFediUser(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode)
+
+ // GetFediFollowers handles the getting of a fedi/activitypub representation of a user/account's followers, performing appropriate
+ // authentication before returning a JSON serializable interface to the caller.
+ GetFediFollowers(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode)
+
+ // GetFediFollowing handles the getting of a fedi/activitypub representation of a user/account's following, performing appropriate
+ // authentication before returning a JSON serializable interface to the caller.
+ GetFediFollowing(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode)
+
+ // GetFediStatus handles the getting of a fedi/activitypub representation of a particular status, performing appropriate
+ // authentication before returning a JSON serializable interface to the caller.
+ GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, ErrorWithCode)
+
+ // 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
+type processor struct {
+ // federator pub.FederatingActor
+ // toClientAPI chan gtsmodel.ToClientAPI
+ fromClientAPI chan gtsmodel.FromClientAPI
+ // toFederator chan gtsmodel.ToFederator
+ fromFederator chan gtsmodel.FromFederator
+ federator federation.Federator
+ stop chan interface{}
+ log *logrus.Logger
+ config *config.Config
+ tc typeutils.TypeConverter
+ oauthServer oauth.Server
+ mediaHandler media.Handler
+ storage blob.Storage
+ db db.DB
+}
+
+// NewProcessor returns a new Processor that uses the given federator and logger
+func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator federation.Federator, oauthServer oauth.Server, mediaHandler media.Handler, storage blob.Storage, db db.DB, log *logrus.Logger) Processor {
+ return &processor{
+ // toClientAPI: make(chan gtsmodel.ToClientAPI, 100),
+ fromClientAPI: make(chan gtsmodel.FromClientAPI, 100),
+ // toFederator: make(chan gtsmodel.ToFederator, 100),
+ fromFederator: make(chan gtsmodel.FromFederator, 100),
+ federator: federator,
+ stop: make(chan interface{}),
+ log: log,
+ config: config,
+ tc: tc,
+ oauthServer: oauthServer,
+ mediaHandler: mediaHandler,
+ storage: storage,
+ db: db,
+ }
+}
+
+// func (p *processor) ToClientAPI() chan gtsmodel.ToClientAPI {
+// return p.toClientAPI
+// }
+
+func (p *processor) FromClientAPI() chan gtsmodel.FromClientAPI {
+ return p.fromClientAPI
+}
+
+// func (p *processor) ToFederator() chan gtsmodel.ToFederator {
+// return p.toFederator
+// }
+
+func (p *processor) FromFederator() chan gtsmodel.FromFederator {
+ return p.fromFederator
+}
+
+// Start starts the Processor, reading from its channels and passing messages back and forth.
+func (p *processor) Start() error {
+ go func() {
+ DistLoop:
+ for {
+ select {
+ 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.fromFederator:
+ p.log.Infof("received message FROM federator: %+v", federatorMsg)
+ if err := p.processFromFederator(federatorMsg); err != nil {
+ p.log.Error(err)
+ }
+ case <-p.stop:
+ break DistLoop
+ }
+ }
+ }()
+ return nil
+}
+
+// Stop stops the processor cleanly, finishing handling any remaining messages before closing down.
+// TODO: empty message buffer properly before stopping otherwise we'll lose federating messages.
+func (p *processor) Stop() error {
+ close(p.stop)
+ return nil
+}
diff --git a/internal/processing/search.go b/internal/processing/search.go
@@ -0,0 +1,295 @@
+/*
+ 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 processing
+
+import (
+ "errors"
+ "fmt"
+ "net/url"
+ "strings"
+
+ "github.com/sirupsen/logrus"
+ 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"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+func (p *processor) SearchGet(authed *oauth.Auth, searchQuery *apimodel.SearchQuery) (*apimodel.SearchResult, ErrorWithCode) {
+ l := p.log.WithFields(logrus.Fields{
+ "func": "SearchGet",
+ "query": searchQuery.Query,
+ })
+
+ results := &apimodel.SearchResult{
+ Accounts: []apimodel.Account{},
+ Statuses: []apimodel.Status{},
+ Hashtags: []apimodel.Tag{},
+ }
+ foundAccounts := []*gtsmodel.Account{}
+ foundStatuses := []*gtsmodel.Status{}
+ // foundHashtags := []*gtsmodel.Tag{}
+
+ // convert the query to lowercase and trim leading/trailing spaces
+ query := strings.ToLower(strings.TrimSpace(searchQuery.Query))
+
+ var foundOne bool
+ // check if the query is something like @whatever_username@example.org -- this means it's a remote account
+ if !foundOne && util.IsMention(searchQuery.Query) {
+ l.Debug("search term is a mention, looking it up...")
+ foundAccount, err := p.searchAccountByMention(authed, searchQuery.Query, searchQuery.Resolve)
+ if err == nil && foundAccount != nil {
+ foundAccounts = append(foundAccounts, foundAccount)
+ foundOne = true
+ l.Debug("got an account by searching by mention")
+ }
+ }
+
+ // check if the query is a URI and just do a lookup for that, straight up
+ if uri, err := url.Parse(query); err == nil && !foundOne {
+ // 1. check if it's a status
+ if foundStatus, err := p.searchStatusByURI(authed, uri, searchQuery.Resolve); err == nil && foundStatus != nil {
+ foundStatuses = append(foundStatuses, foundStatus)
+ foundOne = true
+ l.Debug("got a status by searching by URI")
+ }
+
+ // 2. check if it's an account
+ if foundAccount, err := p.searchAccountByURI(authed, uri, searchQuery.Resolve); err == nil && foundAccount != nil {
+ foundAccounts = append(foundAccounts, foundAccount)
+ foundOne = true
+ l.Debug("got an account by searching by URI")
+ }
+ }
+
+ if !foundOne {
+ // we haven't found anything yet so search for text now
+ l.Debug("nothing found by mention or by URI, will fall back to searching by text now")
+ }
+
+ /*
+ FROM HERE ON we have our search results, it's just a matter of filtering them according to what this user is allowed to see,
+ and then converting them into our frontend format.
+ */
+ for _, foundAccount := range foundAccounts {
+ // make sure there's no block in either direction between the account and the requester
+ if blocked, err := p.db.Blocked(authed.Account.ID, foundAccount.ID); err == nil && !blocked {
+ // all good, convert it and add it to the results
+ if acctMasto, err := p.tc.AccountToMastoPublic(foundAccount); err == nil && acctMasto != nil {
+ results.Accounts = append(results.Accounts, *acctMasto)
+ }
+ }
+ }
+
+ for _, foundStatus := range foundStatuses {
+ statusOwner := >smodel.Account{}
+ if err := p.db.GetByID(foundStatus.AccountID, statusOwner); err != nil {
+ continue
+ }
+
+ relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(foundStatus)
+ if err != nil {
+ continue
+ }
+ if visible, err := p.db.StatusVisible(foundStatus, statusOwner, authed.Account, relevantAccounts); !visible || err != nil {
+ continue
+ }
+
+ statusMasto, err := p.tc.StatusToMasto(foundStatus, statusOwner, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, nil)
+ if err != nil {
+ continue
+ }
+
+ results.Statuses = append(results.Statuses, *statusMasto)
+ }
+
+ return results, nil
+}
+
+func (p *processor) searchStatusByURI(authed *oauth.Auth, uri *url.URL, resolve bool) (*gtsmodel.Status, error) {
+
+ maybeStatus := >smodel.Status{}
+ if err := p.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String(), CaseInsensitive: true}}, maybeStatus); err == nil {
+ // we have it and it's a status
+ return maybeStatus, nil
+ } else if err := p.db.GetWhere([]db.Where{{Key: "url", Value: uri.String(), CaseInsensitive: true}}, maybeStatus); err == nil {
+ // we have it and it's a status
+ return maybeStatus, nil
+ }
+
+ // we don't have it locally so dereference it if we're allowed to
+ if resolve {
+ statusable, err := p.federator.DereferenceRemoteStatus(authed.Account.Username, uri)
+ if err == nil {
+ // it IS a status!
+
+ // extract the status owner's IRI from the statusable
+ var statusOwnerURI *url.URL
+ statusAttributedTo := statusable.GetActivityStreamsAttributedTo()
+ for i := statusAttributedTo.Begin(); i != statusAttributedTo.End(); i = i.Next() {
+ if i.IsIRI() {
+ statusOwnerURI = i.GetIRI()
+ break
+ }
+ }
+ if statusOwnerURI == nil {
+ return nil, errors.New("couldn't extract ownerAccountURI from statusable")
+ }
+
+ // make sure the status owner exists in the db by searching for it
+ _, err := p.searchAccountByURI(authed, statusOwnerURI, resolve)
+ if err != nil {
+ return nil, err
+ }
+
+ // we have the status owner, we have the dereferenced status, so now we should finish dereferencing the status properly
+
+ // first turn it into a gtsmodel.Status
+ status, err := p.tc.ASStatusToStatus(statusable)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+
+ // put it in the DB so it gets a UUID
+ if err := p.db.Put(status); err != nil {
+ return nil, fmt.Errorf("error putting status in the db: %s", err)
+ }
+
+ // properly dereference everything in the status (media attachments etc)
+ if err := p.dereferenceStatusFields(status, authed.Account.Username); err != nil {
+ return nil, fmt.Errorf("error dereferencing status fields: %s", err)
+ }
+
+ // update with the nicely dereferenced status
+ if err := p.db.UpdateByID(status.ID, status); err != nil {
+ return nil, fmt.Errorf("error updating status in the db: %s", err)
+ }
+
+ return status, nil
+ }
+ }
+ return nil, nil
+}
+
+func (p *processor) searchAccountByURI(authed *oauth.Auth, uri *url.URL, resolve bool) (*gtsmodel.Account, error) {
+ maybeAccount := >smodel.Account{}
+ if err := p.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String(), CaseInsensitive: true}}, maybeAccount); err == nil {
+ // we have it and it's an account
+ return maybeAccount, nil
+ } else if err = p.db.GetWhere([]db.Where{{Key: "url", Value: uri.String(), CaseInsensitive: true}}, maybeAccount); err == nil {
+ // we have it and it's an account
+ return maybeAccount, nil
+ }
+ if resolve {
+ // we don't have it locally so try and dereference it
+ accountable, err := p.federator.DereferenceRemoteAccount(authed.Account.Username, uri)
+ if err != nil {
+ return nil, fmt.Errorf("searchAccountByURI: error dereferencing account with uri %s: %s", uri.String(), err)
+ }
+
+ // it IS an account!
+ account, err := p.tc.ASRepresentationToAccount(accountable, false)
+ if err != nil {
+ return nil, fmt.Errorf("searchAccountByURI: error dereferencing account with uri %s: %s", uri.String(), err)
+ }
+
+ if err := p.db.Put(account); err != nil {
+ return nil, fmt.Errorf("searchAccountByURI: error inserting account with uri %s: %s", uri.String(), err)
+ }
+
+ if err := p.dereferenceAccountFields(account, authed.Account.Username, false); err != nil {
+ return nil, fmt.Errorf("searchAccountByURI: error further dereferencing account with uri %s: %s", uri.String(), err)
+ }
+
+ return account, nil
+ }
+ return nil, nil
+}
+
+func (p *processor) searchAccountByMention(authed *oauth.Auth, mention string, resolve bool) (*gtsmodel.Account, error) {
+ // query is for a remote account
+ username, domain, err := util.ExtractMentionParts(mention)
+ if err != nil {
+ return nil, fmt.Errorf("searchAccountByMention: error extracting mention parts: %s", err)
+ }
+
+ // if it's a local account we can skip a whole bunch of stuff
+ maybeAcct := >smodel.Account{}
+ if domain == p.config.Host {
+ if err = p.db.GetLocalAccountByUsername(username, maybeAcct); err != nil {
+ return nil, fmt.Errorf("searchAccountByMention: error getting local account by username: %s", err)
+ }
+ return maybeAcct, nil
+ }
+
+ // it's not a local account so first we'll check if it's in the database already...
+ where := []db.Where{
+ {Key: "username", Value: username, CaseInsensitive: true},
+ {Key: "domain", Value: domain, CaseInsensitive: true},
+ }
+ err = p.db.GetWhere(where, maybeAcct)
+ if err == nil {
+ // we've got it stored locally already!
+ return maybeAcct, nil
+ }
+
+ if _, ok := err.(db.ErrNoEntries); !ok {
+ // if it's not errNoEntries there's been a real database error so bail at this point
+ return nil, fmt.Errorf("searchAccountByMention: database error: %s", err)
+ }
+
+ // we got a db.ErrNoEntries, so we just don't have the account locally stored -- check if we can dereference it
+ if resolve {
+ // we're allowed to resolve it so let's try
+
+ // first we need to webfinger the remote account to convert the username and domain into the activitypub URI for the account
+ acctURI, err := p.federator.FingerRemoteAccount(authed.Account.Username, username, domain)
+ if err != nil {
+ // something went wrong doing the webfinger lookup so we can't process the request
+ return nil, fmt.Errorf("searchAccountByMention: error fingering remote account with username %s and domain %s: %s", username, domain, err)
+ }
+
+ // dereference the account based on the URI we retrieved from the webfinger lookup
+ accountable, err := p.federator.DereferenceRemoteAccount(authed.Account.Username, acctURI)
+ if err != nil {
+ // something went wrong doing the dereferencing so we can't process the request
+ return nil, fmt.Errorf("searchAccountByMention: error dereferencing remote account with uri %s: %s", acctURI.String(), err)
+ }
+
+ // convert the dereferenced account to the gts model of that account
+ foundAccount, err := p.tc.ASRepresentationToAccount(accountable, false)
+ if err != nil {
+ // something went wrong doing the conversion to a gtsmodel.Account so we can't process the request
+ return nil, fmt.Errorf("searchAccountByMention: error converting account with uri %s: %s", acctURI.String(), err)
+ }
+
+ // put this new account in our database
+ if err := p.db.Put(foundAccount); err != nil {
+ return nil, fmt.Errorf("searchAccountByMention: error inserting account with uri %s: %s", acctURI.String(), err)
+ }
+
+ // properly dereference all the fields on the account immediately
+ if err := p.dereferenceAccountFields(foundAccount, authed.Account.Username, true); err != nil {
+ return nil, fmt.Errorf("searchAccountByMention: error dereferencing fields on account with uri %s: %s", acctURI.String(), err)
+ }
+ }
+
+ return nil, nil
+}
diff --git a/internal/processing/status.go b/internal/processing/status.go
@@ -0,0 +1,481 @@
+/*
+ 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 processing
+
+import (
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/google/uuid"
+ 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"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+func (p *processor) StatusCreate(auth *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, error) {
+ uris := util.GenerateURIsForAccount(auth.Account.Username, p.config.Protocol, p.config.Host)
+ thisStatusID := uuid.NewString()
+ thisStatusURI := fmt.Sprintf("%s/%s", uris.StatusesURI, thisStatusID)
+ thisStatusURL := fmt.Sprintf("%s/%s", uris.StatusesURL, thisStatusID)
+ newStatus := >smodel.Status{
+ ID: thisStatusID,
+ URI: thisStatusURI,
+ URL: thisStatusURL,
+ Content: util.HTMLFormat(form.Status),
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ Local: true,
+ AccountID: auth.Account.ID,
+ ContentWarning: form.SpoilerText,
+ ActivityStreamsType: gtsmodel.ActivityStreamsNote,
+ Sensitive: form.Sensitive,
+ Language: form.Language,
+ CreatedWithApplicationID: auth.Application.ID,
+ Text: form.Status,
+ }
+
+ // check if replyToID is ok
+ if err := p.processReplyToID(form, auth.Account.ID, newStatus); err != nil {
+ return nil, err
+ }
+
+ // check if mediaIDs are ok
+ if err := p.processMediaIDs(form, auth.Account.ID, newStatus); err != nil {
+ return nil, err
+ }
+
+ // check if visibility settings are ok
+ if err := p.processVisibility(form, auth.Account.Privacy, newStatus); err != nil {
+ return nil, err
+ }
+
+ // handle language settings
+ if err := p.processLanguage(form, auth.Account.Language, newStatus); err != nil {
+ return nil, err
+ }
+
+ // handle mentions
+ if err := p.processMentions(form, auth.Account.ID, newStatus); err != nil {
+ return nil, err
+ }
+
+ if err := p.processTags(form, auth.Account.ID, newStatus); err != nil {
+ return nil, err
+ }
+
+ if err := p.processEmojis(form, auth.Account.ID, newStatus); err != nil {
+ return nil, err
+ }
+
+ // put the new status in the database, generating an ID for it in the process
+ if err := p.db.Put(newStatus); err != nil {
+ return nil, err
+ }
+
+ // change the status ID of the media attachments to the new status
+ for _, a := range newStatus.GTSMediaAttachments {
+ a.StatusID = newStatus.ID
+ a.UpdatedAt = time.Now()
+ if err := p.db.UpdateByID(a.ID, a); err != nil {
+ return nil, err
+ }
+ }
+
+ // put the new status in the appropriate channel for async processing
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: newStatus.ActivityStreamsType,
+ APActivityType: gtsmodel.ActivityStreamsCreate,
+ GTSModel: 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)
+}
+
+func (p *processor) StatusDelete(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) {
+ l := p.log.WithField("func", "StatusDelete")
+ l.Tracef("going to search for target status %s", targetStatusID)
+ targetStatus := >smodel.Status{}
+ if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
+ return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err)
+ }
+
+ if targetStatus.AccountID != authed.Account.ID {
+ return nil, errors.New("status doesn't belong to requesting account")
+ }
+
+ l.Trace("going to get relevant accounts")
+ relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
+ if err != nil {
+ return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
+ }
+
+ var boostOfStatus *gtsmodel.Status
+ if targetStatus.BoostOfID != "" {
+ boostOfStatus = >smodel.Status{}
+ if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
+ return nil, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)
+ }
+ }
+
+ mastoStatus, err := p.tc.StatusToMasto(targetStatus, authed.Account, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)
+ if err != nil {
+ return nil, fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)
+ }
+
+ if err := p.db.DeleteByID(targetStatus.ID, targetStatus); err != nil {
+ return nil, fmt.Errorf("error deleting status from the database: %s", err)
+ }
+
+ return mastoStatus, nil
+}
+
+func (p *processor) StatusFave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) {
+ l := p.log.WithField("func", "StatusFave")
+ l.Tracef("going to search for target status %s", targetStatusID)
+ targetStatus := >smodel.Status{}
+ if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
+ return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err)
+ }
+
+ l.Tracef("going to search for target account %s", targetStatus.AccountID)
+ targetAccount := >smodel.Account{}
+ if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
+ return nil, fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)
+ }
+
+ l.Trace("going to get relevant accounts")
+ relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
+ if err != nil {
+ return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
+ }
+
+ var boostOfStatus *gtsmodel.Status
+ if targetStatus.BoostOfID != "" {
+ boostOfStatus = >smodel.Status{}
+ if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
+ return nil, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)
+ }
+ }
+
+ l.Trace("going to see if status is visible")
+ visible, err := p.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
+ if err != nil {
+ return nil, fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)
+ }
+
+ if !visible {
+ return nil, errors.New("status is not visible")
+ }
+
+ // is the status faveable?
+ if targetStatus.VisibilityAdvanced != nil {
+ if !targetStatus.VisibilityAdvanced.Likeable {
+ return nil, errors.New("status is not faveable")
+ }
+ }
+
+ // first check if the status is already faved, if so we don't need to do anything
+ newFave := true
+ gtsFave := >smodel.Status{}
+ if err := p.db.GetWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: authed.Account.ID}}, gtsFave); err == nil {
+ // we already have a fave for this status
+ newFave = false
+ }
+
+ if newFave {
+ thisFaveID := uuid.NewString()
+
+ // we need to create a new fave in the database
+ gtsFave := >smodel.StatusFave{
+ ID: thisFaveID,
+ AccountID: authed.Account.ID,
+ TargetAccountID: targetAccount.ID,
+ StatusID: targetStatus.ID,
+ URI: util.GenerateURIForLike(authed.Account.Username, p.config.Protocol, p.config.Host, thisFaveID),
+ GTSStatus: targetStatus,
+ GTSTargetAccount: targetAccount,
+ GTSFavingAccount: authed.Account,
+ }
+
+ if err := p.db.Put(gtsFave); err != nil {
+ return nil, err
+ }
+
+ // send the new fave through the processor channel for federation etc
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsLike,
+ APActivityType: gtsmodel.ActivityStreamsCreate,
+ GTSModel: gtsFave,
+ OriginAccount: authed.Account,
+ TargetAccount: targetAccount,
+ }
+ }
+
+ // return the mastodon representation of the target status
+ mastoStatus, err := p.tc.StatusToMasto(targetStatus, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)
+ if err != nil {
+ return nil, fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)
+ }
+
+ return mastoStatus, nil
+}
+
+func (p *processor) StatusBoost(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, ErrorWithCode) {
+ l := p.log.WithField("func", "StatusBoost")
+
+ l.Tracef("going to search for target status %s", targetStatusID)
+ targetStatus := >smodel.Status{}
+ if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
+ }
+
+ l.Tracef("going to search for target account %s", targetStatus.AccountID)
+ targetAccount := >smodel.Account{}
+ if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
+ }
+
+ l.Trace("going to get relevant accounts")
+ relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
+ if err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err))
+ }
+
+ l.Trace("going to see if status is visible")
+ visible, err := p.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
+ if err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
+ }
+
+ if !visible {
+ return nil, NewErrorNotFound(errors.New("status is not visible"))
+ }
+
+ if targetStatus.VisibilityAdvanced != nil {
+ if !targetStatus.VisibilityAdvanced.Boostable {
+ return nil, NewErrorForbidden(errors.New("status is not boostable"))
+ }
+ }
+
+ // it's visible! it's boostable! so let's boost the FUCK out of it
+ boostWrapperStatus, err := p.tc.StatusToBoost(targetStatus, authed.Account)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+
+ boostWrapperStatus.CreatedWithApplicationID = authed.Application.ID
+ boostWrapperStatus.GTSBoostedAccount = targetAccount
+
+ // put the boost in the database
+ if err := p.db.Put(boostWrapperStatus); err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+
+ // send it to the processor for async processing
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsAnnounce,
+ APActivityType: gtsmodel.ActivityStreamsCreate,
+ GTSModel: boostWrapperStatus,
+ OriginAccount: authed.Account,
+ TargetAccount: targetAccount,
+ }
+
+ // return the frontend representation of the new status to the submitter
+ mastoStatus, err := p.tc.StatusToMasto(boostWrapperStatus, authed.Account, authed.Account, targetAccount, nil, targetStatus)
+ if err != nil {
+ return nil, NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
+ }
+
+ return mastoStatus, nil
+}
+
+func (p *processor) StatusFavedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, error) {
+ l := p.log.WithField("func", "StatusFavedBy")
+
+ l.Tracef("going to search for target status %s", targetStatusID)
+ targetStatus := >smodel.Status{}
+ if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
+ return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err)
+ }
+
+ l.Tracef("going to search for target account %s", targetStatus.AccountID)
+ targetAccount := >smodel.Account{}
+ if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
+ return nil, fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)
+ }
+
+ l.Trace("going to get relevant accounts")
+ relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
+ if err != nil {
+ return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
+ }
+
+ l.Trace("going to see if status is visible")
+ visible, err := p.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
+ if err != nil {
+ return nil, fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)
+ }
+
+ if !visible {
+ return nil, errors.New("status is not visible")
+ }
+
+ // get ALL accounts that faved a status -- doesn't take account of blocks and mutes and stuff
+ favingAccounts, err := p.db.WhoFavedStatus(targetStatus)
+ if err != nil {
+ return nil, fmt.Errorf("error seeing who faved status: %s", err)
+ }
+
+ // filter the list so the user doesn't see accounts they blocked or which blocked them
+ filteredAccounts := []*gtsmodel.Account{}
+ for _, acc := range favingAccounts {
+ blocked, err := p.db.Blocked(authed.Account.ID, acc.ID)
+ if err != nil {
+ return nil, fmt.Errorf("error checking blocks: %s", err)
+ }
+ if !blocked {
+ filteredAccounts = append(filteredAccounts, acc)
+ }
+ }
+
+ // TODO: filter other things here? suspended? muted? silenced?
+
+ // now we can return the masto representation of those accounts
+ mastoAccounts := []*apimodel.Account{}
+ for _, acc := range filteredAccounts {
+ mastoAccount, err := p.tc.AccountToMastoPublic(acc)
+ if err != nil {
+ return nil, fmt.Errorf("error converting account to api model: %s", err)
+ }
+ mastoAccounts = append(mastoAccounts, mastoAccount)
+ }
+
+ return mastoAccounts, nil
+}
+
+func (p *processor) StatusGet(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) {
+ l := p.log.WithField("func", "StatusGet")
+
+ l.Tracef("going to search for target status %s", targetStatusID)
+ targetStatus := >smodel.Status{}
+ if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
+ return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err)
+ }
+
+ l.Tracef("going to search for target account %s", targetStatus.AccountID)
+ targetAccount := >smodel.Account{}
+ if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
+ return nil, fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)
+ }
+
+ l.Trace("going to get relevant accounts")
+ relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
+ if err != nil {
+ return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
+ }
+
+ l.Trace("going to see if status is visible")
+ visible, err := p.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
+ if err != nil {
+ return nil, fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)
+ }
+
+ if !visible {
+ return nil, errors.New("status is not visible")
+ }
+
+ var boostOfStatus *gtsmodel.Status
+ if targetStatus.BoostOfID != "" {
+ boostOfStatus = >smodel.Status{}
+ if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
+ return nil, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)
+ }
+ }
+
+ mastoStatus, err := p.tc.StatusToMasto(targetStatus, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)
+ if err != nil {
+ return nil, fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)
+ }
+
+ return mastoStatus, nil
+
+}
+
+func (p *processor) StatusUnfave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) {
+ l := p.log.WithField("func", "StatusUnfave")
+ l.Tracef("going to search for target status %s", targetStatusID)
+ targetStatus := >smodel.Status{}
+ if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
+ return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err)
+ }
+
+ l.Tracef("going to search for target account %s", targetStatus.AccountID)
+ targetAccount := >smodel.Account{}
+ if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
+ return nil, fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)
+ }
+
+ l.Trace("going to get relevant accounts")
+ relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
+ if err != nil {
+ return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
+ }
+
+ l.Trace("going to see if status is visible")
+ visible, err := p.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
+ if err != nil {
+ return nil, fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)
+ }
+
+ if !visible {
+ return nil, errors.New("status is not visible")
+ }
+
+ // is the status faveable?
+ if targetStatus.VisibilityAdvanced != nil {
+ if !targetStatus.VisibilityAdvanced.Likeable {
+ return nil, errors.New("status is not faveable")
+ }
+ }
+
+ // it's visible! it's faveable! so let's unfave the FUCK out of it
+ _, err = p.db.UnfaveStatus(targetStatus, authed.Account.ID)
+ if err != nil {
+ return nil, fmt.Errorf("error unfaveing status: %s", err)
+ }
+
+ var boostOfStatus *gtsmodel.Status
+ if targetStatus.BoostOfID != "" {
+ boostOfStatus = >smodel.Status{}
+ if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
+ return nil, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)
+ }
+ }
+
+ mastoStatus, err := p.tc.StatusToMasto(targetStatus, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)
+ if err != nil {
+ return nil, fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)
+ }
+
+ return mastoStatus, nil
+}
diff --git a/internal/processing/timeline.go b/internal/processing/timeline.go
@@ -0,0 +1,99 @@
+/*
+ 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 processing
+
+import (
+ "fmt"
+
+ 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) HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) ([]apimodel.Status, ErrorWithCode) {
+ l := p.log.WithField("func", "HomeTimelineGet")
+
+ statuses, err := p.db.GetHomeTimelineForAccount(authed.Account.ID, maxID, sinceID, minID, limit, local)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+
+ apiStatuses := []apimodel.Status{}
+ for _, s := range statuses {
+ targetAccount := >smodel.Account{}
+ if err := p.db.GetByID(s.AccountID, targetAccount); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ l.Debugf("skipping status %s because account %s can't be found in the db", s.ID, s.AccountID)
+ continue
+ }
+ return nil, NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error getting status author: %s", err))
+ }
+
+ relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(s)
+ if err != nil {
+ l.Debugf("skipping status %s because we couldn't pull relevant accounts from the db", s.ID)
+ continue
+ }
+
+ visible, err := p.db.StatusVisible(s, targetAccount, authed.Account, relevantAccounts)
+ if err != nil {
+ return nil, NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error checking status visibility: %s", err))
+ }
+ if !visible {
+ continue
+ }
+
+ var boostedStatus *gtsmodel.Status
+ if s.BoostOfID != "" {
+ bs := >smodel.Status{}
+ if err := p.db.GetByID(s.BoostOfID, bs); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ l.Debugf("skipping status %s because status %s can't be found in the db", s.ID, s.BoostOfID)
+ continue
+ }
+ return nil, NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error getting boosted status: %s", err))
+ }
+ boostedRelevantAccounts, err := p.db.PullRelevantAccountsFromStatus(bs)
+ if err != nil {
+ l.Debugf("skipping status %s because we couldn't pull relevant accounts from the db", s.ID)
+ continue
+ }
+
+ boostedVisible, err := p.db.StatusVisible(bs, relevantAccounts.BoostedAccount, authed.Account, boostedRelevantAccounts)
+ if err != nil {
+ return nil, NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error checking boosted status visibility: %s", err))
+ }
+
+ if boostedVisible {
+ boostedStatus = bs
+ }
+ }
+
+ apiStatus, err := p.tc.StatusToMasto(s, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostedStatus)
+ if err != nil {
+ l.Debugf("skipping status %s because it couldn't be converted to its mastodon representation: %s", s.ID, err)
+ continue
+ }
+
+ apiStatuses = append(apiStatuses, *apiStatus)
+ }
+
+ return apiStatuses, nil
+}
diff --git a/internal/processing/util.go b/internal/processing/util.go
@@ -0,0 +1,357 @@
+/*
+ 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 processing
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "mime/multipart"
+
+ 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/media"
+ "github.com/superseriousbusiness/gotosocial/internal/transport"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+func (p *processor) processVisibility(form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error {
+ // by default all flags are set to true
+ gtsAdvancedVis := >smodel.VisibilityAdvanced{
+ Federated: true,
+ Boostable: true,
+ Replyable: true,
+ Likeable: true,
+ }
+
+ var gtsBasicVis gtsmodel.Visibility
+ // Advanced takes priority if it's set.
+ // If it's not set, take whatever masto visibility is set.
+ // If *that's* not set either, then just take the account default.
+ // If that's also not set, take the default for the whole instance.
+ if form.VisibilityAdvanced != nil {
+ gtsBasicVis = gtsmodel.Visibility(*form.VisibilityAdvanced)
+ } else if form.Visibility != "" {
+ gtsBasicVis = p.tc.MastoVisToVis(form.Visibility)
+ } else if accountDefaultVis != "" {
+ gtsBasicVis = accountDefaultVis
+ } else {
+ gtsBasicVis = gtsmodel.VisibilityDefault
+ }
+
+ switch gtsBasicVis {
+ case gtsmodel.VisibilityPublic:
+ // for public, there's no need to change any of the advanced flags from true regardless of what the user filled out
+ break
+ case gtsmodel.VisibilityUnlocked:
+ // for unlocked the user can set any combination of flags they like so look at them all to see if they're set and then apply them
+ if form.Federated != nil {
+ gtsAdvancedVis.Federated = *form.Federated
+ }
+
+ if form.Boostable != nil {
+ gtsAdvancedVis.Boostable = *form.Boostable
+ }
+
+ if form.Replyable != nil {
+ gtsAdvancedVis.Replyable = *form.Replyable
+ }
+
+ if form.Likeable != nil {
+ gtsAdvancedVis.Likeable = *form.Likeable
+ }
+
+ case gtsmodel.VisibilityFollowersOnly, gtsmodel.VisibilityMutualsOnly:
+ // for followers or mutuals only, boostable will *always* be false, but the other fields can be set so check and apply them
+ gtsAdvancedVis.Boostable = false
+
+ if form.Federated != nil {
+ gtsAdvancedVis.Federated = *form.Federated
+ }
+
+ if form.Replyable != nil {
+ gtsAdvancedVis.Replyable = *form.Replyable
+ }
+
+ if form.Likeable != nil {
+ gtsAdvancedVis.Likeable = *form.Likeable
+ }
+
+ case gtsmodel.VisibilityDirect:
+ // direct is pretty easy: there's only one possible setting so return it
+ gtsAdvancedVis.Federated = true
+ gtsAdvancedVis.Boostable = false
+ gtsAdvancedVis.Federated = true
+ gtsAdvancedVis.Likeable = true
+ }
+
+ status.Visibility = gtsBasicVis
+ status.VisibilityAdvanced = gtsAdvancedVis
+ return nil
+}
+
+func (p *processor) processReplyToID(form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error {
+ if form.InReplyToID == "" {
+ return nil
+ }
+
+ // If this status is a reply to another status, we need to do a bit of work to establish whether or not this status can be posted:
+ //
+ // 1. Does the replied status exist in the database?
+ // 2. Is the replied status marked as replyable?
+ // 3. Does a block exist between either the current account or the account that posted the status it's replying to?
+ //
+ // If this is all OK, then we fetch the repliedStatus and the repliedAccount for later processing.
+ repliedStatus := >smodel.Status{}
+ repliedAccount := >smodel.Account{}
+ // check replied status exists + is replyable
+ if err := p.db.GetByID(form.InReplyToID, repliedStatus); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ return fmt.Errorf("status with id %s not replyable because it doesn't exist", form.InReplyToID)
+ }
+ return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
+ }
+
+ if repliedStatus.VisibilityAdvanced != nil {
+ if !repliedStatus.VisibilityAdvanced.Replyable {
+ return fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID)
+ }
+ }
+
+ // check replied account is known to us
+ if err := p.db.GetByID(repliedStatus.AccountID, repliedAccount); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ return fmt.Errorf("status with id %s not replyable because account id %s is not known", form.InReplyToID, repliedStatus.AccountID)
+ }
+ return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
+ }
+ // check if a block exists
+ if blocked, err := p.db.Blocked(thisAccountID, repliedAccount.ID); err != nil {
+ if _, ok := err.(db.ErrNoEntries); !ok {
+ return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
+ }
+ } else if blocked {
+ return fmt.Errorf("status with id %s not replyable", form.InReplyToID)
+ }
+ status.InReplyToID = repliedStatus.ID
+ status.InReplyToAccountID = repliedAccount.ID
+
+ return nil
+}
+
+func (p *processor) processMediaIDs(form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error {
+ if form.MediaIDs == nil {
+ return nil
+ }
+
+ gtsMediaAttachments := []*gtsmodel.MediaAttachment{}
+ attachments := []string{}
+ for _, mediaID := range form.MediaIDs {
+ // check these attachments exist
+ a := >smodel.MediaAttachment{}
+ if err := p.db.GetByID(mediaID, a); err != nil {
+ return fmt.Errorf("invalid media type or media not found for media id %s", mediaID)
+ }
+ // check they belong to the requesting account id
+ if a.AccountID != thisAccountID {
+ return fmt.Errorf("media with id %s does not belong to account %s", mediaID, thisAccountID)
+ }
+ // check they're not already used in a status
+ if a.StatusID != "" || a.ScheduledStatusID != "" {
+ return fmt.Errorf("media with id %s is already attached to a status", mediaID)
+ }
+ gtsMediaAttachments = append(gtsMediaAttachments, a)
+ attachments = append(attachments, a.ID)
+ }
+ status.GTSMediaAttachments = gtsMediaAttachments
+ status.Attachments = attachments
+ return nil
+}
+
+func (p *processor) processLanguage(form *apimodel.AdvancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error {
+ if form.Language != "" {
+ status.Language = form.Language
+ } else {
+ status.Language = accountDefaultLanguage
+ }
+ if status.Language == "" {
+ return errors.New("no language given either in status create form or account default")
+ }
+ return nil
+}
+
+func (p *processor) processMentions(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
+ menchies := []string{}
+ 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)
+ }
+ for _, menchie := range gtsMenchies {
+ if err := p.db.Put(menchie); err != nil {
+ return fmt.Errorf("error putting mentions in db: %s", err)
+ }
+ menchies = append(menchies, menchie.ID)
+ }
+ // add full populated gts menchies to the status for passing them around conveniently
+ status.GTSMentions = gtsMenchies
+ // add just the ids of the mentioned accounts to the status for putting in the db
+ status.Mentions = menchies
+ return nil
+}
+
+func (p *processor) processTags(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
+ tags := []string{}
+ 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)
+ }
+ for _, tag := range gtsTags {
+ if err := p.db.Upsert(tag, "name"); err != nil {
+ return fmt.Errorf("error putting tags in db: %s", err)
+ }
+ tags = append(tags, tag.ID)
+ }
+ // add full populated gts tags to the status for passing them around conveniently
+ status.GTSTags = gtsTags
+ // add just the ids of the used tags to the status for putting in the db
+ status.Tags = tags
+ return nil
+}
+
+func (p *processor) processEmojis(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
+ emojis := []string{}
+ 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)
+ }
+ for _, e := range gtsEmojis {
+ emojis = append(emojis, e.ID)
+ }
+ // add full populated gts emojis to the status for passing them around conveniently
+ status.GTSEmojis = gtsEmojis
+ // add just the ids of the used emojis to the status for putting in the db
+ status.Emojis = emojis
+ return nil
+}
+
+/*
+ HELPER FUNCTIONS
+*/
+
+// TODO: try to combine the below two functions because this is a lot of code repetition.
+
+// updateAccountAvatar does the dirty work of checking the avatar part of an account update form,
+// parsing and checking the image, and doing the necessary updates in the database for this to become
+// the account's new avatar image.
+func (p *processor) updateAccountAvatar(avatar *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) {
+ var err error
+ if int(avatar.Size) > p.config.MediaConfig.MaxImageSize {
+ err = fmt.Errorf("avatar with size %d exceeded max image size of %d bytes", avatar.Size, p.config.MediaConfig.MaxImageSize)
+ return nil, err
+ }
+ f, err := avatar.Open()
+ if err != nil {
+ return nil, fmt.Errorf("could not read provided avatar: %s", err)
+ }
+
+ // extract the bytes
+ buf := new(bytes.Buffer)
+ size, err := io.Copy(buf, f)
+ if err != nil {
+ return nil, fmt.Errorf("could not read provided avatar: %s", err)
+ }
+ if size == 0 {
+ return nil, errors.New("could not read provided avatar: size 0 bytes")
+ }
+
+ // do the setting
+ avatarInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Avatar, "")
+ if err != nil {
+ return nil, fmt.Errorf("error processing avatar: %s", err)
+ }
+
+ return avatarInfo, f.Close()
+}
+
+// updateAccountHeader does the dirty work of checking the header part of an account update form,
+// parsing and checking the image, and doing the necessary updates in the database for this to become
+// the account's new header image.
+func (p *processor) updateAccountHeader(header *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) {
+ var err error
+ if int(header.Size) > p.config.MediaConfig.MaxImageSize {
+ err = fmt.Errorf("header with size %d exceeded max image size of %d bytes", header.Size, p.config.MediaConfig.MaxImageSize)
+ return nil, err
+ }
+ f, err := header.Open()
+ if err != nil {
+ return nil, fmt.Errorf("could not read provided header: %s", err)
+ }
+
+ // extract the bytes
+ buf := new(bytes.Buffer)
+ size, err := io.Copy(buf, f)
+ if err != nil {
+ return nil, fmt.Errorf("could not read provided header: %s", err)
+ }
+ if size == 0 {
+ return nil, errors.New("could not read provided header: size 0 bytes")
+ }
+
+ // do the setting
+ headerInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Header, "")
+ if err != nil {
+ return nil, fmt.Errorf("error processing header: %s", err)
+ }
+
+ return headerInfo, f.Close()
+}
+
+// fetchHeaderAndAviForAccount fetches the header and avatar for a remote account, using a transport
+// on behalf of requestingUsername.
+//
+// targetAccount's AvatarMediaAttachmentID and HeaderMediaAttachmentID will be updated as necessary.
+//
+// SIDE EFFECTS: remote header and avatar will be stored in local storage, and the database will be updated
+// to reflect the creation of these new attachments.
+func (p *processor) fetchHeaderAndAviForAccount(targetAccount *gtsmodel.Account, t transport.Transport, refresh bool) error {
+ if targetAccount.AvatarRemoteURL != "" && (targetAccount.AvatarMediaAttachmentID == "" || refresh) {
+ a, err := p.mediaHandler.ProcessRemoteHeaderOrAvatar(t, >smodel.MediaAttachment{
+ RemoteURL: targetAccount.AvatarRemoteURL,
+ Avatar: true,
+ }, targetAccount.ID)
+ if err != nil {
+ return fmt.Errorf("error processing avatar for user: %s", err)
+ }
+ targetAccount.AvatarMediaAttachmentID = a.ID
+ }
+
+ if targetAccount.HeaderRemoteURL != "" && (targetAccount.HeaderMediaAttachmentID == "" || refresh) {
+ a, err := p.mediaHandler.ProcessRemoteHeaderOrAvatar(t, >smodel.MediaAttachment{
+ RemoteURL: targetAccount.HeaderRemoteURL,
+ Header: true,
+ }, targetAccount.ID)
+ if err != nil {
+ return fmt.Errorf("error processing header for user: %s", err)
+ }
+ targetAccount.HeaderMediaAttachmentID = a.ID
+ }
+ return nil
+}
diff --git a/internal/router/mock_Router.go b/internal/router/mock_Router.go
@@ -1,44 +0,0 @@
-// Code generated by mockery v2.7.4. DO NOT EDIT.
-
-package router
-
-import (
- context "context"
-
- gin "github.com/gin-gonic/gin"
- mock "github.com/stretchr/testify/mock"
-)
-
-// MockRouter is an autogenerated mock type for the Router type
-type MockRouter struct {
- mock.Mock
-}
-
-// AttachHandler provides a mock function with given fields: method, path, f
-func (_m *MockRouter) AttachHandler(method string, path string, f gin.HandlerFunc) {
- _m.Called(method, path, f)
-}
-
-// AttachMiddleware provides a mock function with given fields: handler
-func (_m *MockRouter) AttachMiddleware(handler gin.HandlerFunc) {
- _m.Called(handler)
-}
-
-// Start provides a mock function with given fields:
-func (_m *MockRouter) Start() {
- _m.Called()
-}
-
-// Stop provides a mock function with given fields: ctx
-func (_m *MockRouter) Stop(ctx context.Context) error {
- ret := _m.Called(ctx)
-
- var r0 error
- if rf, ok := ret.Get(0).(func(context.Context) error); ok {
- r0 = rf(ctx)
- } else {
- r0 = ret.Error(0)
- }
-
- return r0
-}
diff --git a/internal/storage/inmem.go b/internal/storage/inmem.go
@@ -1,55 +0,0 @@
-package storage
-
-import (
- "fmt"
-
- "github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/config"
-)
-
-// NewInMem returns an in-memory implementation of the Storage interface.
-// This is good for testing and whatnot but ***SHOULD ABSOLUTELY NOT EVER
-// BE USED IN A PRODUCTION SETTING***, because A) everything will be wiped out
-// if you restart the server and B) if you store lots of images your RAM use
-// will absolutely go through the roof.
-func NewInMem(c *config.Config, log *logrus.Logger) (Storage, error) {
- return &inMemStorage{
- stored: make(map[string][]byte),
- log: log,
- }, nil
-}
-
-type inMemStorage struct {
- stored map[string][]byte
- log *logrus.Logger
-}
-
-func (s *inMemStorage) StoreFileAt(path string, data []byte) error {
- l := s.log.WithField("func", "StoreFileAt")
- l.Debugf("storing at path %s", path)
- s.stored[path] = data
- return nil
-}
-
-func (s *inMemStorage) RetrieveFileFrom(path string) ([]byte, error) {
- l := s.log.WithField("func", "RetrieveFileFrom")
- l.Debugf("retrieving from path %s", path)
- d, ok := s.stored[path]
- if !ok || len(d) == 0 {
- return nil, fmt.Errorf("no data found at path %s", path)
- }
- return d, nil
-}
-
-func (s *inMemStorage) ListKeys() ([]string, error) {
- keys := []string{}
- for k := range s.stored {
- keys = append(keys, k)
- }
- return keys, nil
-}
-
-func (s *inMemStorage) RemoveFileAt(path string) error {
- delete(s.stored, path)
- return nil
-}
diff --git a/internal/storage/local.go b/internal/storage/local.go
@@ -1,70 +0,0 @@
-package storage
-
-import (
- "fmt"
- "os"
- "path/filepath"
- "strings"
-
- "github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/config"
-)
-
-// NewLocal returns an implementation of the Storage interface that uses
-// the local filesystem for storing and retrieving files, attachments, etc.
-func NewLocal(c *config.Config, log *logrus.Logger) (Storage, error) {
- return &localStorage{
- config: c,
- log: log,
- }, nil
-}
-
-type localStorage struct {
- config *config.Config
- log *logrus.Logger
-}
-
-func (s *localStorage) StoreFileAt(path string, data []byte) error {
- l := s.log.WithField("func", "StoreFileAt")
- l.Debugf("storing at path %s", path)
- components := strings.Split(path, "/")
- dir := strings.Join(components[0:len(components)-1], "/")
- if err := os.MkdirAll(dir, 0777); err != nil {
- return fmt.Errorf("error writing file at %s: %s", path, err)
- }
- if err := os.WriteFile(path, data, 0777); err != nil {
- return fmt.Errorf("error writing file at %s: %s", path, err)
- }
- return nil
-}
-
-func (s *localStorage) RetrieveFileFrom(path string) ([]byte, error) {
- l := s.log.WithField("func", "RetrieveFileFrom")
- l.Debugf("retrieving from path %s", path)
- b, err := os.ReadFile(path)
- if err != nil {
- return nil, fmt.Errorf("error reading file at %s: %s", path, err)
- }
- return b, nil
-}
-
-func (s *localStorage) ListKeys() ([]string, error) {
- keys := []string{}
- err := filepath.Walk(s.config.StorageConfig.BasePath, func(path string, info os.FileInfo, err error) error {
- if err != nil {
- return err
- }
- if !info.IsDir() {
- keys = append(keys, path)
- }
- return nil
- })
- if err != nil {
- return nil, err
- }
- return keys, nil
-}
-
-func (s *localStorage) RemoveFileAt(path string) error {
- return os.Remove(path)
-}
diff --git a/internal/storage/mock_Storage.go b/internal/storage/mock_Storage.go
@@ -1,84 +0,0 @@
-// Code generated by mockery v2.7.4. DO NOT EDIT.
-
-package storage
-
-import mock "github.com/stretchr/testify/mock"
-
-// MockStorage is an autogenerated mock type for the Storage type
-type MockStorage struct {
- mock.Mock
-}
-
-// ListKeys provides a mock function with given fields:
-func (_m *MockStorage) ListKeys() ([]string, error) {
- ret := _m.Called()
-
- var r0 []string
- if rf, ok := ret.Get(0).(func() []string); ok {
- r0 = rf()
- } else {
- if ret.Get(0) != nil {
- r0 = ret.Get(0).([]string)
- }
- }
-
- var r1 error
- if rf, ok := ret.Get(1).(func() error); ok {
- r1 = rf()
- } else {
- r1 = ret.Error(1)
- }
-
- return r0, r1
-}
-
-// RemoveFileAt provides a mock function with given fields: path
-func (_m *MockStorage) RemoveFileAt(path string) error {
- ret := _m.Called(path)
-
- var r0 error
- if rf, ok := ret.Get(0).(func(string) error); ok {
- r0 = rf(path)
- } else {
- r0 = ret.Error(0)
- }
-
- return r0
-}
-
-// RetrieveFileFrom provides a mock function with given fields: path
-func (_m *MockStorage) RetrieveFileFrom(path string) ([]byte, error) {
- ret := _m.Called(path)
-
- var r0 []byte
- if rf, ok := ret.Get(0).(func(string) []byte); ok {
- r0 = rf(path)
- } else {
- if ret.Get(0) != nil {
- r0 = ret.Get(0).([]byte)
- }
- }
-
- var r1 error
- if rf, ok := ret.Get(1).(func(string) error); ok {
- r1 = rf(path)
- } else {
- r1 = ret.Error(1)
- }
-
- return r0, r1
-}
-
-// StoreFileAt provides a mock function with given fields: path, data
-func (_m *MockStorage) StoreFileAt(path string, data []byte) error {
- ret := _m.Called(path, data)
-
- var r0 error
- if rf, ok := ret.Get(0).(func(string, []byte) error); ok {
- r0 = rf(path, data)
- } else {
- r0 = ret.Error(0)
- }
-
- return r0
-}
diff --git a/internal/storage/storage.go b/internal/storage/storage.go
@@ -1,30 +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 storage contains an interface and implementations for storing and retrieving files and attachments.
-package storage
-
-// Storage is an interface for storing and retrieving blobs
-// such as images, videos, and any other attachments/documents
-// that shouldn't be stored in a database.
-type Storage interface {
- StoreFileAt(path string, data []byte) error
- RetrieveFileFrom(path string) ([]byte, error)
- ListKeys() ([]string, error)
- RemoveFileAt(path string) error
-}
diff --git a/internal/util/uri.go b/internal/util/uri.go
@@ -113,7 +113,7 @@ func GenerateURIForFollow(username string, protocol string, host string, thisFol
return fmt.Sprintf("%s://%s/%s/%s/%s/%s", protocol, host, UsersPath, username, FollowPath, thisFollowID)
}
-// GenerateURIForFollow returns the AP URI for a new like/fave -- something like:
+// GenerateURIForLike returns the AP URI for a new like/fave -- something like:
// https://example.org/users/whatever_user/liked/41c7f33f-1060-48d9-84df-38dcb13cf0d8
func GenerateURIForLike(username string, protocol string, host string, thisFavedID string) string {
return fmt.Sprintf("%s://%s/%s/%s/%s/%s", protocol, host, UsersPath, username, LikedPath, thisFavedID)
@@ -195,7 +195,7 @@ func IsLikedPath(id *url.URL) bool {
return likedPathRegex.MatchString(strings.ToLower(id.Path))
}
-// IsLikedPath returns true if the given URL path corresponds to eg /users/example_username/liked/SOME_UUID_OF_A_STATUS
+// IsLikePath returns true if the given URL path corresponds to eg /users/example_username/liked/SOME_UUID_OF_A_STATUS
func IsLikePath(id *url.URL) bool {
return likePathRegex.MatchString(strings.ToLower(id.Path))
}
diff --git a/testrig/actions.go b/testrig/actions.go
@@ -29,7 +29,6 @@ import (
"syscall"
"github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/action"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/client/account"
"github.com/superseriousbusiness/gotosocial/internal/api/client/admin"
@@ -39,13 +38,14 @@ import (
mediaModule "github.com/superseriousbusiness/gotosocial/internal/api/client/media"
"github.com/superseriousbusiness/gotosocial/internal/api/client/status"
"github.com/superseriousbusiness/gotosocial/internal/api/security"
+ "github.com/superseriousbusiness/gotosocial/internal/cliactions"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/gotosocial"
)
// Run creates and starts a gotosocial testrig server
-var Run action.GTSAction = func(ctx context.Context, _ *config.Config, log *logrus.Logger) error {
+var Run cliactions.GTSAction = func(ctx context.Context, _ *config.Config, log *logrus.Logger) error {
c := NewTestConfig()
dbService := NewTestDB()
federatingDB := NewTestFederatingDB(dbService)
@@ -99,7 +99,7 @@ var Run action.GTSAction = func(ctx context.Context, _ *config.Config, log *logr
}
}
- gts, err := gotosocial.New(dbService, router, federator, c)
+ gts, err := gotosocial.NewServer(dbService, router, federator, c)
if err != nil {
return fmt.Errorf("error creating gotosocial service: %s", err)
}
diff --git a/testrig/mediahandler.go b/testrig/mediahandler.go
@@ -19,13 +19,13 @@
package testrig
import (
+ "github.com/superseriousbusiness/gotosocial/internal/blob"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
)
// NewTestMediaHandler returns a media handler with the default test config, the default test logger,
// and the given db and storage.
-func NewTestMediaHandler(db db.DB, storage storage.Storage) media.Handler {
+func NewTestMediaHandler(db db.DB, storage blob.Storage) media.Handler {
return media.New(NewTestConfig(), db, storage, NewTestLog())
}
diff --git a/testrig/processor.go b/testrig/processor.go
@@ -19,13 +19,13 @@
package testrig
import (
+ "github.com/superseriousbusiness/gotosocial/internal/blob"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/federation"
- "github.com/superseriousbusiness/gotosocial/internal/message"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
+ "github.com/superseriousbusiness/gotosocial/internal/processing"
)
// NewTestProcessor returns a Processor suitable for testing purposes
-func NewTestProcessor(db db.DB, storage storage.Storage, federator federation.Federator) message.Processor {
- return message.NewProcessor(NewTestConfig(), NewTestTypeConverter(db), federator, NewTestOauthServer(db), NewTestMediaHandler(db, storage), storage, db, NewTestLog())
+func NewTestProcessor(db db.DB, storage blob.Storage, federator federation.Federator) processing.Processor {
+ return processing.NewProcessor(NewTestConfig(), NewTestTypeConverter(db), federator, NewTestOauthServer(db), NewTestMediaHandler(db, storage), storage, db, NewTestLog())
}
diff --git a/testrig/storage.go b/testrig/storage.go
@@ -22,12 +22,12 @@ import (
"fmt"
"os"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
+ "github.com/superseriousbusiness/gotosocial/internal/blob"
)
// NewTestStorage returns a new in memory storage with the default test config
-func NewTestStorage() storage.Storage {
- s, err := storage.NewInMem(NewTestConfig(), NewTestLog())
+func NewTestStorage() blob.Storage {
+ s, err := blob.NewInMem(NewTestConfig(), NewTestLog())
if err != nil {
panic(err)
}
@@ -35,7 +35,7 @@ func NewTestStorage() storage.Storage {
}
// StandardStorageSetup populates the storage with standard test entries from the given directory.
-func StandardStorageSetup(s storage.Storage, relativePath string) {
+func StandardStorageSetup(s blob.Storage, relativePath string) {
storedA := newTestStoredAttachments()
a := NewTestAttachments()
for k, paths := range storedA {
@@ -92,7 +92,7 @@ func StandardStorageSetup(s storage.Storage, relativePath string) {
}
// StandardStorageTeardown deletes everything in storage so that it's clean for the next test
-func StandardStorageTeardown(s storage.Storage) {
+func StandardStorageTeardown(s blob.Storage) {
keys, err := s.ListKeys()
if err != nil {
panic(err)