From 4920229a3b6e1d7dde536bc9ff766542b05d935c Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Fri, 20 Aug 2021 12:26:56 +0200 Subject: [PATCH] Database updates (#144) * start moving some database stuff around * continue moving db stuff around * more fiddling * more updates * and some more * and yet more * i broke SOMETHING but what, it's a mystery * tidy up * vendor ttlcache * use ttlcache * fix up some tests * rename some stuff * little reminder * some more updates --- README.md | 1 + docs/api/swagger.yaml | 4 +- go.mod | 1 + go.sum | 4 + internal/ap/extract.go | 2 +- internal/api/client/auth/callback.go | 4 +- internal/api/security/signaturecheck.go | 23 +- internal/cache/cache.go | 20 + internal/cache/error.go | 27 + internal/{db/pg/put.go => cache/fetch.go} | 19 +- internal/cache/store.go | 24 + internal/cliactions/admin/account/account.go | 20 +- internal/cliactions/server/server.go | 2 + internal/db/account.go | 66 ++ internal/db/admin.go | 53 ++ internal/db/basic.go | 87 ++ internal/db/db.go | 265 +----- internal/db/domain.go | 36 + internal/db/error.go | 24 +- internal/db/instance.go | 36 + internal/db/media.go | 27 + internal/db/mention.go | 30 + internal/db/notification.go | 31 + internal/db/pg/account.go | 256 ++++++ internal/db/pg/account_test.go | 70 ++ internal/db/pg/admin.go | 235 +++++ internal/db/pg/basic.go | 205 +++++ internal/db/pg/blocks.go | 67 -- internal/db/pg/domain.go | 83 ++ internal/db/pg/get.go | 75 -- internal/db/pg/instance.go | 39 +- internal/db/pg/{delete.go => media.go} | 52 +- internal/db/pg/mention.go | 108 +++ internal/db/pg/notification.go | 135 +++ internal/db/pg/pg.go | 827 ++---------------- internal/db/pg/pg_test.go | 47 + internal/db/pg/relationship.go | 276 ++++++ internal/db/pg/status.go | 318 +++++++ internal/db/pg/status_test.go | 134 +++ internal/db/pg/statuscontext.go | 104 --- internal/db/pg/timeline.go | 40 +- internal/db/pg/update.go | 73 -- internal/db/pg/util.go | 25 + internal/db/relationship.go | 71 ++ internal/db/status.go | 75 ++ internal/db/timeline.go | 44 + internal/federation/dereference.go | 18 + internal/federation/dereferencing/account.go | 5 +- internal/federation/dereferencing/announce.go | 10 +- internal/federation/dereferencing/blocked.go | 2 +- internal/federation/dereferencing/status.go | 93 +- internal/federation/federatingdb/accept.go | 18 + internal/federation/federatingdb/announce.go | 18 + internal/federation/federatingdb/create.go | 4 +- internal/federation/federatingdb/delete.go | 29 +- internal/federation/federatingdb/exists.go | 18 + internal/federation/federatingdb/followers.go | 7 +- internal/federation/federatingdb/following.go | 27 +- internal/federation/federatingdb/get.go | 4 +- internal/federation/federatingdb/inbox.go | 18 + internal/federation/federatingdb/liked.go | 18 + internal/federation/federatingdb/outbox.go | 20 +- internal/federation/federatingdb/owns.go | 32 +- internal/federation/federatingdb/undo.go | 18 + internal/federation/federatingdb/update.go | 18 + internal/federation/federatingdb/util.go | 8 +- internal/federation/federatingprotocol.go | 42 +- internal/federation/finger.go | 2 +- internal/federation/handshake.go | 18 + internal/federation/transport.go | 18 + internal/federation/util.go | 23 - internal/gtsmodel/account.go | 6 +- internal/gtsmodel/domainblock.go | 3 +- internal/gtsmodel/emaildomainblock.go | 3 +- internal/gtsmodel/emoji.go | 3 +- internal/gtsmodel/follow.go | 6 +- internal/gtsmodel/followrequest.go | 6 +- internal/gtsmodel/instance.go | 6 +- internal/gtsmodel/mediaattachment.go | 3 +- internal/gtsmodel/mention.go | 19 +- internal/gtsmodel/notification.go | 20 +- internal/gtsmodel/status.go | 64 +- internal/gtsmodel/statusbookmark.go | 6 +- internal/gtsmodel/statusfave.go | 16 +- internal/gtsmodel/statusmute.go | 9 +- internal/gtsmodel/tag.go | 2 +- internal/gtsmodel/user.go | 6 +- internal/media/handler.go | 6 +- internal/oauth/clientstore.go | 4 +- internal/oauth/clientstore_test.go | 2 +- internal/oauth/server.go | 2 +- internal/oauth/tokenstore.go | 4 +- internal/processing/account/createblock.go | 26 +- internal/processing/account/createfollow.go | 29 +- internal/processing/account/delete.go | 12 +- internal/processing/account/get.go | 4 +- internal/processing/account/getfollowers.go | 32 +- internal/processing/account/getfollowing.go | 32 +- internal/processing/account/getstatuses.go | 13 +- internal/processing/account/removeblock.go | 12 +- internal/processing/account/removefollow.go | 4 +- .../processing/admin/createdomainblock.go | 8 +- .../processing/admin/deletedomainblock.go | 2 +- internal/processing/admin/getdomainblock.go | 2 +- internal/processing/admin/getdomainblocks.go | 2 +- internal/processing/blocks.go | 4 +- internal/processing/federation.go | 37 +- internal/processing/followrequest.go | 6 +- internal/processing/fromclientapi.go | 32 +- internal/processing/fromcommon.go | 111 ++- internal/processing/fromfederator.go | 8 +- internal/processing/instance.go | 8 +- internal/processing/media/delete.go | 4 +- internal/processing/media/getfile.go | 2 +- internal/processing/media/getmedia.go | 2 +- internal/processing/media/update.go | 2 +- internal/processing/notification.go | 2 +- internal/processing/search.go | 27 +- internal/processing/status/boost.go | 33 +- internal/processing/status/boostedby.go | 39 +- internal/processing/status/context.go | 47 +- internal/processing/status/create.go | 18 +- internal/processing/status/delete.go | 34 +- internal/processing/status/fave.go | 53 +- internal/processing/status/favedby.go | 31 +- internal/processing/status/get.go | 31 +- internal/processing/status/unboost.go | 39 +- internal/processing/status/unfave.go | 31 +- internal/processing/status/util.go | 28 +- internal/processing/status/util_test.go | 28 +- internal/processing/streaming.go | 18 + internal/processing/timeline.go | 12 +- internal/router/session.go | 2 +- internal/text/common.go | 4 +- internal/timeline/index.go | 8 +- internal/timeline/index_test.go | 7 +- internal/timeline/manager_test.go | 10 +- internal/timeline/prepare.go | 6 +- internal/transport/controller.go | 6 +- internal/typeutils/astointernal.go | 117 +-- internal/typeutils/converter.go | 13 +- internal/typeutils/internal.go | 10 +- internal/typeutils/internaltoas.go | 124 +-- internal/typeutils/internaltofrontend.go | 213 ++--- internal/typeutils/util.go | 8 +- internal/util/statustools.go | 19 +- internal/util/unique.go | 32 + internal/visibility/filter.go | 18 + internal/visibility/relevantaccounts.go | 229 +++++ internal/visibility/statushometimelineable.go | 43 +- .../visibility/statuspublictimelineable.go | 18 + internal/visibility/statusvisible.go | 67 +- internal/visibility/util.go | 191 ---- testrig/db.go | 4 +- testrig/testmodels.go | 47 +- .../github.com/ReneKroon/ttlcache/.travis.yml | 18 + vendor/github.com/ReneKroon/ttlcache/LICENSE | 21 + .../github.com/ReneKroon/ttlcache/Readme.md | 71 ++ vendor/github.com/ReneKroon/ttlcache/cache.go | 307 +++++++ vendor/github.com/ReneKroon/ttlcache/go.mod | 9 + vendor/github.com/ReneKroon/ttlcache/go.sum | 11 + vendor/github.com/ReneKroon/ttlcache/item.go | 46 + .../ReneKroon/ttlcache/priority_queue.go | 71 ++ vendor/modules.txt | 3 + 164 files changed, 4850 insertions(+), 2617 deletions(-) create mode 100644 internal/cache/error.go rename internal/{db/pg/put.go => cache/fetch.go} (69%) create mode 100644 internal/cache/store.go create mode 100644 internal/db/account.go create mode 100644 internal/db/admin.go create mode 100644 internal/db/basic.go create mode 100644 internal/db/domain.go create mode 100644 internal/db/instance.go create mode 100644 internal/db/media.go create mode 100644 internal/db/mention.go create mode 100644 internal/db/notification.go create mode 100644 internal/db/pg/account.go create mode 100644 internal/db/pg/account_test.go create mode 100644 internal/db/pg/admin.go create mode 100644 internal/db/pg/basic.go delete mode 100644 internal/db/pg/blocks.go create mode 100644 internal/db/pg/domain.go delete mode 100644 internal/db/pg/get.go rename internal/db/pg/{delete.go => media.go} (53%) create mode 100644 internal/db/pg/mention.go create mode 100644 internal/db/pg/notification.go create mode 100644 internal/db/pg/pg_test.go create mode 100644 internal/db/pg/relationship.go create mode 100644 internal/db/pg/status.go create mode 100644 internal/db/pg/status_test.go delete mode 100644 internal/db/pg/statuscontext.go delete mode 100644 internal/db/pg/update.go create mode 100644 internal/db/pg/util.go create mode 100644 internal/db/relationship.go create mode 100644 internal/db/status.go create mode 100644 internal/db/timeline.go delete mode 100644 internal/federation/util.go create mode 100644 internal/util/unique.go create mode 100644 internal/visibility/relevantaccounts.go delete mode 100644 internal/visibility/util.go create mode 100644 vendor/github.com/ReneKroon/ttlcache/.travis.yml create mode 100644 vendor/github.com/ReneKroon/ttlcache/LICENSE create mode 100644 vendor/github.com/ReneKroon/ttlcache/Readme.md create mode 100644 vendor/github.com/ReneKroon/ttlcache/cache.go create mode 100644 vendor/github.com/ReneKroon/ttlcache/go.mod create mode 100644 vendor/github.com/ReneKroon/ttlcache/go.sum create mode 100644 vendor/github.com/ReneKroon/ttlcache/item.go create mode 100644 vendor/github.com/ReneKroon/ttlcache/priority_queue.go diff --git a/README.md b/README.md index 5a64dfa4f..110112750 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ The following libraries and frameworks are used by GoToSocial, with gratitude * [mvdan/xurls](https://github.com/mvdan/xurls); URL parsing regular expressions. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html). * [nfnt/resize](https://github.com/nfnt/resize); convenient image resizing. [ISC License](https://spdx.org/licenses/ISC.html). * [oklog/ulid](https://github.com/oklog/ulid); sequential, database-friendly ID generation. [Apache-2.0 License](https://spdx.org/licenses/Apache-2.0.html). +* [ReneKroon/ttlcache](https://github.com/ReneKroon/ttlcache); in-memory caching. [MIT License](https://spdx.org/licenses/MIT.html). * [russross/blackfriday](https://github.com/russross/blackfriday); markdown parsing for statuses. [Simplified BSD License](https://spdx.org/licenses/BSD-2-Clause.html). * [sirupsen/logrus](https://github.com/sirupsen/logrus); logging. [MIT License](https://spdx.org/licenses/MIT.html). * [stretchr/testify](https://github.com/stretchr/testify); test framework. [MIT License](https://spdx.org/licenses/MIT.html). diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml index faf9f181e..40a2caa5e 100644 --- a/docs/api/swagger.yaml +++ b/docs/api/swagger.yaml @@ -1679,7 +1679,7 @@ info: name: AGPL3 url: https://www.gnu.org/licenses/agpl-3.0.en.html title: GoToSocial - version: 0.1.0-SNAPSHOT-dereference_remote_replies + version: 0.1.0-SNAPSHOT paths: /api/v1/accounts: post: @@ -3404,6 +3404,8 @@ paths: description: "" schema: $ref: '#/definitions/swaggerStatusRepliesCollection' + "400": + description: bad request "401": description: unauthorized "403": diff --git a/go.mod b/go.mod index 10597a06b..e6790ccfc 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/superseriousbusiness/gotosocial go 1.16 require ( + github.com/ReneKroon/ttlcache v1.7.0 github.com/buckket/go-blurhash v1.1.0 github.com/coreos/go-oidc/v3 v3.0.0 github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect diff --git a/go.sum b/go.sum index 4d6968ada..a05bbcd4e 100644 --- a/go.sum +++ b/go.sum @@ -33,6 +33,8 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/ReneKroon/ttlcache v1.7.0 h1:8BkjFfrzVFXyrqnMtezAaJ6AHPSsVV10m6w28N/Fgkk= +github.com/ReneKroon/ttlcache v1.7.0/go.mod h1:8BGGzdumrIjWxdRx8zpK6L3oGMWvIXdvB2GD1cfvd+I= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4= @@ -425,6 +427,8 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opentelemetry.io/otel v0.13.0/go.mod h1:dlSNewoRYikTkotEnxdmuBHgzT+k/idJSfDv/FxEnOY= +go.uber.org/goleak v0.10.0 h1:G3eWbSNIskeRqtsN/1uI5B+eP73y3JUuBsv9AZjehb4= +go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI= golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/internal/ap/extract.go b/internal/ap/extract.go index baffd4bf2..1ee0e008e 100644 --- a/internal/ap/extract.go +++ b/internal/ap/extract.go @@ -581,7 +581,7 @@ func ExtractMention(i Mentionable) (*gtsmodel.Mention, error) { if hrefProp == nil || !hrefProp.IsIRI() { return nil, errors.New("no href prop") } - mention.MentionedAccountURI = hrefProp.GetIRI().String() + mention.TargetAccountURI = hrefProp.GetIRI().String() return mention, nil } diff --git a/internal/api/client/auth/callback.go b/internal/api/client/auth/callback.go index 8bf2a50b5..a26838aa3 100644 --- a/internal/api/client/auth/callback.go +++ b/internal/api/client/auth/callback.go @@ -116,7 +116,7 @@ func (m *Module) parseUserFromClaims(claims *oidc.Claims, ip net.IP, appID strin return user, nil } - if _, ok := err.(db.ErrNoEntries); !ok { + if err != db.ErrNoEntries { // we have an actual error in the database return nil, fmt.Errorf("error checking database for email %s: %s", claims.Email, err) } @@ -128,7 +128,7 @@ func (m *Module) parseUserFromClaims(claims *oidc.Claims, ip net.IP, appID strin return nil, fmt.Errorf("user with email address %s is unconfirmed", claims.Email) } - if _, ok := err.(db.ErrNoEntries); !ok { + if err != db.ErrNoEntries { // we have an actual error in the database return nil, fmt.Errorf("error checking database for email %s: %s", claims.Email, err) } diff --git a/internal/api/security/signaturecheck.go b/internal/api/security/signaturecheck.go index b852c92ab..88b0b4dff 100644 --- a/internal/api/security/signaturecheck.go +++ b/internal/api/security/signaturecheck.go @@ -6,8 +6,6 @@ import ( "github.com/gin-gonic/gin" "github.com/go-fed/httpsig" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -33,13 +31,13 @@ func (m *Module) SignatureCheck(c *gin.Context) { // we managed to parse the url! // if the domain is blocked we want to bail as early as possible - blockedDomain, err := m.blockedDomain(requestingPublicKeyID.Host) + blocked, err := m.db.IsURIBlocked(requestingPublicKeyID) if err != nil { l.Errorf("could not tell if domain %s was blocked or not: %s", requestingPublicKeyID.Host, err) c.AbortWithStatus(http.StatusInternalServerError) return } - if blockedDomain { + if blocked { l.Infof("domain %s is blocked", requestingPublicKeyID.Host) c.AbortWithStatus(http.StatusForbidden) return @@ -50,20 +48,3 @@ func (m *Module) SignatureCheck(c *gin.Context) { } } } - -func (m *Module) blockedDomain(host string) (bool, error) { - b := >smodel.DomainBlock{} - err := m.db.GetWhere([]db.Where{{Key: "domain", Value: host, CaseInsensitive: true}}, b) - if err == nil { - // block exists - return true, nil - } - - if _, ok := err.(db.ErrNoEntries); ok { - // there are no entries so there's no block - return false, nil - } - - // there's an actual error - return false, err -} diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 1d2d0533b..eb3744cfe 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -18,8 +18,28 @@ package cache +import ( + "time" + + "github.com/ReneKroon/ttlcache" +) + // Cache defines an in-memory cache that is safe to be wiped when the application is restarted type Cache interface { Store(k string, v interface{}) error Fetch(k string) (interface{}, error) } + +type cache struct { + c *ttlcache.Cache +} + +// New returns a new in-memory cache. +func New() Cache { + c := ttlcache.NewCache() + c.SetTTL(30 * time.Second) + cache := &cache{ + c: c, + } + return cache +} diff --git a/internal/cache/error.go b/internal/cache/error.go new file mode 100644 index 000000000..3f32aa7ce --- /dev/null +++ b/internal/cache/error.go @@ -0,0 +1,27 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package cache + +import "errors" + +// Error models an error returned by the in-memory cache. +type Error error + +// ErrNotFound means that a value for the requested key was not found in the cache. +var ErrNotFound = errors.New("value not found in cache") diff --git a/internal/db/pg/put.go b/internal/cache/fetch.go similarity index 69% rename from internal/db/pg/put.go rename to internal/cache/fetch.go index 09beca14b..c107b9b26 100644 --- a/internal/db/pg/put.go +++ b/internal/cache/fetch.go @@ -16,18 +16,13 @@ along with this program. If not, see . */ -package pg +package cache -import ( - "strings" - - "github.com/superseriousbusiness/gotosocial/internal/db" -) - -func (ps *postgresService) Put(i interface{}) error { - _, err := ps.conn.Model(i).Insert(i) - if err != nil && strings.Contains(err.Error(), "duplicate key value violates unique constraint") { - return db.ErrAlreadyExists{} +func (c *cache) Fetch(k string) (interface{}, error) { + i, stored := c.c.Get(k) + if !stored { + return nil, ErrNotFound } - return err + + return i, nil } diff --git a/internal/cache/store.go b/internal/cache/store.go new file mode 100644 index 000000000..6b4024476 --- /dev/null +++ b/internal/cache/store.go @@ -0,0 +1,24 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package cache + +func (c *cache) Store(k string, v interface{}) error { + c.c.Set(k, v) + return nil +} diff --git a/internal/cliactions/admin/account/account.go b/internal/cliactions/admin/account/account.go index 732527aa4..0ae7f32de 100644 --- a/internal/cliactions/admin/account/account.go +++ b/internal/cliactions/admin/account/account.go @@ -88,8 +88,8 @@ var Confirm cliactions.GTSAction = func(ctx context.Context, c *config.Config, l return err } - a := >smodel.Account{} - if err := dbConn.GetLocalAccountByUsername(username, a); err != nil { + a, err := dbConn.GetLocalAccountByUsername(username) + if err != nil { return err } @@ -123,8 +123,8 @@ var Promote cliactions.GTSAction = func(ctx context.Context, c *config.Config, l return err } - a := >smodel.Account{} - if err := dbConn.GetLocalAccountByUsername(username, a); err != nil { + a, err := dbConn.GetLocalAccountByUsername(username) + if err != nil { return err } @@ -155,8 +155,8 @@ var Demote cliactions.GTSAction = func(ctx context.Context, c *config.Config, lo return err } - a := >smodel.Account{} - if err := dbConn.GetLocalAccountByUsername(username, a); err != nil { + a, err := dbConn.GetLocalAccountByUsername(username) + if err != nil { return err } @@ -187,8 +187,8 @@ var Disable cliactions.GTSAction = func(ctx context.Context, c *config.Config, l return err } - a := >smodel.Account{} - if err := dbConn.GetLocalAccountByUsername(username, a); err != nil { + a, err := dbConn.GetLocalAccountByUsername(username) + if err != nil { return err } @@ -233,8 +233,8 @@ var Password cliactions.GTSAction = func(ctx context.Context, c *config.Config, return err } - a := >smodel.Account{} - if err := dbConn.GetLocalAccountByUsername(username, a); err != nil { + a, err := dbConn.GetLocalAccountByUsername(username) + if err != nil { return err } diff --git a/internal/cliactions/server/server.go b/internal/cliactions/server/server.go index 2314e2608..72c6cfadf 100644 --- a/internal/cliactions/server/server.go +++ b/internal/cliactions/server/server.go @@ -62,6 +62,8 @@ var models []interface{} = []interface{}{ >smodel.MediaAttachment{}, >smodel.Mention{}, >smodel.Status{}, + >smodel.StatusToEmoji{}, + >smodel.StatusToTag{}, >smodel.StatusFave{}, >smodel.StatusBookmark{}, >smodel.StatusMute{}, diff --git a/internal/db/account.go b/internal/db/account.go new file mode 100644 index 000000000..0e1575f9b --- /dev/null +++ b/internal/db/account.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 . +*/ + +package db + +import ( + "time" + + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +// Account contains functions related to account getting/setting/creation. +type Account interface { + // GetAccountByID returns one account with the given ID, or an error if something goes wrong. + GetAccountByID(id string) (*gtsmodel.Account, Error) + + // GetAccountByURI returns one account with the given URI, or an error if something goes wrong. + GetAccountByURI(uri string) (*gtsmodel.Account, Error) + + // GetAccountByURL returns one account with the given URL, or an error if something goes wrong. + GetAccountByURL(uri string) (*gtsmodel.Account, Error) + + // GetLocalAccountByUsername returns an account on this instance by its username. + GetLocalAccountByUsername(username string) (*gtsmodel.Account, Error) + + // GetAccountFaves fetches faves/likes created by the target accountID. + GetAccountFaves(accountID string) ([]*gtsmodel.StatusFave, Error) + + // GetAccountStatusesCount is a shortcut for the common action of counting statuses produced by accountID. + CountAccountStatuses(accountID string) (int, Error) + + // GetAccountStatuses is a shortcut for getting the most recent statuses. accountID is optional, if not provided + // then all statuses will be returned. If limit is set to 0, the size of the returned slice will not be limited. This can + // be very memory intensive so you probably shouldn't do this! + // In case of no entries, a 'no entries' error will be returned + GetAccountStatuses(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, Error) + + GetAccountBlocks(accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, Error) + + // GetAccountLastPosted simply gets the timestamp of the most recent post by the account. + // + // The returned time will be zero if account has never posted anything. + GetAccountLastPosted(accountID string) (time.Time, Error) + + // SetAccountHeaderOrAvatar sets the header or avatar for the given accountID to the given media attachment. + SetAccountHeaderOrAvatar(mediaAttachment *gtsmodel.MediaAttachment, accountID string) Error + + // GetInstanceAccount returns the instance account for the given domain. + // If domain is empty, this instance account will be returned. + GetInstanceAccount(domain string) (*gtsmodel.Account, Error) +} diff --git a/internal/db/admin.go b/internal/db/admin.go new file mode 100644 index 000000000..aa2b22f47 --- /dev/null +++ b/internal/db/admin.go @@ -0,0 +1,53 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package db + +import ( + "net" + + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +// Admin contains functions related to instance administration (new signups etc). +type Admin interface { + // IsUsernameAvailable checks whether a given username is available on our domain. + // Returns an error if the username is already taken, or something went wrong in the db. + IsUsernameAvailable(username string) Error + + // IsEmailAvailable checks whether a given email address for a new account is available to be used on our domain. + // Return an error if: + // A) the email is already associated with an account + // B) we block signups from this email domain + // C) something went wrong in the db + IsEmailAvailable(email string) Error + + // NewSignup creates a new user in the database with the given parameters. + // By the time this function is called, it should be assumed that all the parameters have passed validation! + NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string, emailVerified bool, admin bool) (*gtsmodel.User, Error) + + // CreateInstanceAccount creates an account in the database with the same username as the instance host value. + // Ie., if the instance is hosted at 'example.org' the instance user will have a username of 'example.org'. + // This is needed for things like serving files that belong to the instance and not an individual user/account. + CreateInstanceAccount() Error + + // CreateInstanceInstance creates an instance in the database with the same domain as the instance host value. + // Ie., if the instance is hosted at 'example.org' the instance will have a domain of 'example.org'. + // This is needed for things like serving instance information through /api/v1/instance + CreateInstanceInstance() Error +} diff --git a/internal/db/basic.go b/internal/db/basic.go new file mode 100644 index 000000000..729920bba --- /dev/null +++ b/internal/db/basic.go @@ -0,0 +1,87 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package db + +import "context" + +// Basic wraps basic database functionality. +type Basic interface { + // CreateTable creates a table for the given interface. + // For implementations that don't use tables, this can just return nil. + CreateTable(i interface{}) Error + + // DropTable drops the table for the given interface. + // For implementations that don't use tables, this can just return nil. + DropTable(i interface{}) Error + + // RegisterTable registers a table for use in many2many relations. + // For implementations that don't use tables, or many2many relations, this can just return nil. + RegisterTable(i interface{}) Error + + // Stop should stop and close the database connection cleanly, returning an error if this is not possible. + // If the database implementation doesn't need to be stopped, this can just return nil. + Stop(ctx context.Context) Error + + // IsHealthy should return nil if the database connection is healthy, or an error if not. + IsHealthy(ctx context.Context) Error + + // GetByID gets one entry by its id. In a database like postgres, this might be the 'id' field of the entry, + // for other implementations (for example, in-memory) it might just be the key of a map. + // The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. + // In case of no entries, a 'no entries' error will be returned + GetByID(id string, i interface{}) Error + + // GetWhere gets one entry where key = value. This is similar to GetByID but allows the caller to specify the + // name of the key to select from. + // The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. + // In case of no entries, a 'no entries' error will be returned + GetWhere(where []Where, i interface{}) Error + + // GetAll will try to get all entries of type i. + // The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. + // In case of no entries, a 'no entries' error will be returned + GetAll(i interface{}) Error + + // Put simply stores i. It is up to the implementation to figure out how to store it, and using what key. + // The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. + Put(i interface{}) Error + + // Upsert stores or updates i based on the given conflict column, as in https://www.postgresqltutorial.com/postgresql-upsert/ + // It is up to the implementation to figure out how to store it, and using what key. + // The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. + Upsert(i interface{}, conflictColumn string) Error + + // UpdateByID updates i with id id. + // The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. + UpdateByID(id string, i interface{}) Error + + // UpdateOneByID updates interface i with database the given database id. It will update one field of key key and value value. + UpdateOneByID(id string, key string, value interface{}, i interface{}) Error + + // UpdateWhere updates column key of interface i with the given value, where the given parameters apply. + UpdateWhere(where []Where, key string, value interface{}, i interface{}) Error + + // DeleteByID removes i with id id. + // If i didn't exist anyway, then no error should be returned. + DeleteByID(id string, i interface{}) Error + + // DeleteWhere deletes i where key = value + // If i didn't exist anyway, then no error should be returned. + DeleteWhere(where []Where, i interface{}) Error +} diff --git a/internal/db/db.go b/internal/db/db.go index d0b23fbc6..d6ac883e4 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -19,9 +19,6 @@ package db import ( - "context" - "net" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) @@ -30,257 +27,19 @@ const ( DBTypePostgres string = "POSTGRES" ) -// 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. +// DB provides methods for interacting with an underlying database or other storage mechanism. type DB interface { - /* - BASIC DB FUNCTIONALITY - */ - - // CreateTable creates a table for the given interface. - // For implementations that don't use tables, this can just return nil. - CreateTable(i interface{}) error - - // DropTable drops the table for the given interface. - // For implementations that don't use tables, this can just return nil. - DropTable(i interface{}) error - - // Stop should stop and close the database connection cleanly, returning an error if this is not possible. - // If the database implementation doesn't need to be stopped, this can just return nil. - Stop(ctx context.Context) error - - // IsHealthy should return nil if the database connection is healthy, or an error if not. - IsHealthy(ctx context.Context) error - - // GetByID gets one entry by its id. In a database like postgres, this might be the 'id' field of the entry, - // for other implementations (for example, in-memory) it might just be the key of a map. - // The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. - // In case of no entries, a 'no entries' error will be returned - GetByID(id string, i interface{}) error - - // GetWhere gets one entry where key = value. This is similar to GetByID but allows the caller to specify the - // name of the key to select from. - // The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. - // In case of no entries, a 'no entries' error will be returned - GetWhere(where []Where, i interface{}) error - - // GetAll will try to get all entries of type i. - // The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. - // In case of no entries, a 'no entries' error will be returned - GetAll(i interface{}) error - - // Put simply stores i. It is up to the implementation to figure out how to store it, and using what key. - // The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. - Put(i interface{}) error - - // Upsert stores or updates i based on the given conflict column, as in https://www.postgresqltutorial.com/postgresql-upsert/ - // It is up to the implementation to figure out how to store it, and using what key. - // The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. - Upsert(i interface{}, conflictColumn string) error - - // UpdateByID updates i with id id. - // The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. - UpdateByID(id string, i interface{}) error - - // UpdateOneByID updates interface i with database the given database id. It will update one field of key key and value value. - UpdateOneByID(id string, key string, value interface{}, i interface{}) error - - // UpdateWhere updates column key of interface i with the given value, where the given parameters apply. - UpdateWhere(where []Where, key string, value interface{}, i interface{}) error - - // DeleteByID removes i with id id. - // If i didn't exist anyway, then no error should be returned. - DeleteByID(id string, i interface{}) error - - // DeleteWhere deletes i where key = value - // If i didn't exist anyway, then no error should be returned. - DeleteWhere(where []Where, i interface{}) error - - /* - HANDY SHORTCUTS - */ - - // AcceptFollowRequest moves a follow request in the database from the follow_requests table to the follows table. - // In other words, it should create the follow, and delete the existing follow request. - // - // It will return the newly created follow for further processing. - AcceptFollowRequest(originAccountID string, targetAccountID string) (*gtsmodel.Follow, error) - - // CreateInstanceAccount creates an account in the database with the same username as the instance host value. - // Ie., if the instance is hosted at 'example.org' the instance user will have a username of 'example.org'. - // This is needed for things like serving files that belong to the instance and not an individual user/account. - CreateInstanceAccount() error - - // CreateInstanceInstance creates an instance in the database with the same domain as the instance host value. - // Ie., if the instance is hosted at 'example.org' the instance will have a domain of 'example.org'. - // This is needed for things like serving instance information through /api/v1/instance - CreateInstanceInstance() error - - // GetAccountByUserID is a shortcut for the common action of fetching an account corresponding to a user ID. - // The given account pointer will be set to the result of the query, whatever it is. - // In case of no entries, a 'no entries' error will be returned - GetAccountByUserID(userID string, account *gtsmodel.Account) error - - // GetLocalAccountByUsername is a shortcut for the common action of fetching an account ON THIS INSTANCE - // according to its username, which should be unique. - // The given account pointer will be set to the result of the query, whatever it is. - // In case of no entries, a 'no entries' error will be returned - GetLocalAccountByUsername(username string, account *gtsmodel.Account) error - - // GetFollowRequestsForAccountID is a shortcut for the common action of fetching a list of follow requests targeting the given account ID. - // The given slice 'followRequests' will be set to the result of the query, whatever it is. - // In case of no entries, a 'no entries' error will be returned - GetFollowRequestsForAccountID(accountID string, followRequests *[]gtsmodel.FollowRequest) error - - // GetFollowingByAccountID is a shortcut for the common action of fetching a list of accounts that accountID is following. - // The given slice 'following' will be set to the result of the query, whatever it is. - // In case of no entries, a 'no entries' error will be returned - GetFollowingByAccountID(accountID string, following *[]gtsmodel.Follow) error - - // GetFollowersByAccountID is a shortcut for the common action of fetching a list of accounts that accountID is followed by. - // The given slice 'followers' will be set to the result of the query, whatever it is. - // In case of no entries, a 'no entries' error will be returned - // - // If localOnly is set to true, then only followers from *this instance* will be returned. - GetFollowersByAccountID(accountID string, followers *[]gtsmodel.Follow, localOnly bool) error - - // GetFavesByAccountID is a shortcut for the common action of fetching a list of faves made by the given accountID. - // The given slice 'faves' will be set to the result of the query, whatever it is. - // In case of no entries, a 'no entries' error will be returned - GetFavesByAccountID(accountID string, faves *[]gtsmodel.StatusFave) error - - // CountStatusesByAccountID is a shortcut for the common action of counting statuses produced by accountID. - CountStatusesByAccountID(accountID string) (int, error) - - // GetStatusesForAccount is a shortcut for getting the most recent statuses. accountID is optional, if not provided - // then all statuses will be returned. If limit is set to 0, the size of the returned slice will not be limited. This can - // be very memory intensive so you probably shouldn't do this! - // In case of no entries, a 'no entries' error will be returned - GetStatusesForAccount(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, error) - - GetBlocksForAccount(accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, error) - - // GetLastStatusForAccountID simply gets the most recent status by the given account. - // The given slice 'status' pointer will be set to the result of the query, whatever it is. - // In case of no entries, a 'no entries' error will be returned - GetLastStatusForAccountID(accountID string, status *gtsmodel.Status) error - - // IsUsernameAvailable checks whether a given username is available on our domain. - // Returns an error if the username is already taken, or something went wrong in the db. - IsUsernameAvailable(username string) error - - // IsEmailAvailable checks whether a given email address for a new account is available to be used on our domain. - // Return an error if: - // A) the email is already associated with an account - // B) we block signups from this email domain - // C) something went wrong in the db - IsEmailAvailable(email string) error - - // NewSignup creates a new user in the database with the given parameters. - // By the time this function is called, it should be assumed that all the parameters have passed validation! - NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string, emailVerified bool, admin bool) (*gtsmodel.User, error) - - // SetHeaderOrAvatarForAccountID sets the header or avatar for the given accountID to the given media attachment. - SetHeaderOrAvatarForAccountID(mediaAttachment *gtsmodel.MediaAttachment, accountID string) error - - // GetHeaderAvatarForAccountID gets the current avatar for the given account ID. - // The passed mediaAttachment pointer will be populated with the value of the avatar, if it exists. - GetAvatarForAccountID(avatar *gtsmodel.MediaAttachment, accountID string) error - - // GetHeaderForAccountID gets the current header for the given account ID. - // The passed mediaAttachment pointer will be populated with the value of the header, if it exists. - GetHeaderForAccountID(header *gtsmodel.MediaAttachment, accountID string) error - - // Blocked checks whether a block exists in eiher direction between two accounts. - // That is, it returns true if account1 blocks account2, OR if account2 blocks account1. - Blocked(account1 string, account2 string) (bool, error) - - // GetRelationship retrieves the relationship of the targetAccount to the requestingAccount. - GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, error) - - // Follows returns true if sourceAccount follows target account, or an error if something goes wrong while finding out. - Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) - - // FollowRequested returns true if sourceAccount has requested to follow target account, or an error if something goes wrong while finding out. - FollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) - - // Mutuals returns true if account1 and account2 both follow each other, or an error if something goes wrong while finding out. - Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error) - - // GetReplyCountForStatus returns the amount of replies recorded for a status, or an error if something goes wrong - GetReplyCountForStatus(status *gtsmodel.Status) (int, error) - - // GetReblogCountForStatus returns the amount of reblogs/boosts recorded for a status, or an error if something goes wrong - GetReblogCountForStatus(status *gtsmodel.Status) (int, error) - - // GetFaveCountForStatus returns the amount of faves/likes recorded for a status, or an error if something goes wrong - GetFaveCountForStatus(status *gtsmodel.Status) (int, error) - - // StatusParents get the parent statuses of a given status. - // - // If onlyDirect is true, only the immediate parent will be returned. - StatusParents(status *gtsmodel.Status, onlyDirect bool) ([]*gtsmodel.Status, error) - - // StatusChildren gets the child statuses of a given status. - // - // If onlyDirect is true, only the immediate children will be returned. - StatusChildren(status *gtsmodel.Status, onlyDirect bool, minID string) ([]*gtsmodel.Status, error) - - // StatusFavedBy checks if a given status has been faved by a given account ID - StatusFavedBy(status *gtsmodel.Status, accountID string) (bool, error) - - // StatusRebloggedBy checks if a given status has been reblogged/boosted by a given account ID - StatusRebloggedBy(status *gtsmodel.Status, accountID string) (bool, error) - - // StatusMutedBy checks if a given status has been muted by a given account ID - StatusMutedBy(status *gtsmodel.Status, accountID string) (bool, error) - - // StatusBookmarkedBy checks if a given status has been bookmarked by a given account ID - StatusBookmarkedBy(status *gtsmodel.Status, accountID string) (bool, error) - - // WhoFavedStatus returns a slice of accounts who faved the given status. - // This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user. - WhoFavedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error) - - // WhoBoostedStatus returns a slice of accounts who boosted the given status. - // This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user. - WhoBoostedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error) - - // GetHomeTimelineForAccount returns a slice of statuses from accounts that are followed by the given account id. - // - // Statuses should be returned in descending order of when they were created (newest first). - GetHomeTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error) - - // GetPublicTimelineForAccount fetches the account's PUBLIC timeline -- ie., posts and replies that are public. - // It will use the given filters and try to return as many statuses as possible up to the limit. - // - // Statuses should be returned in descending order of when they were created (newest first). - GetPublicTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error) - - // GetFavedTimelineForAccount fetches the account's FAVED timeline -- ie., posts and replies that the requesting account has faved. - // It will use the given filters and try to return as many statuses as possible up to the limit. - // - // Note that unlike the other GetTimeline functions, the returned statuses will be arranged by their FAVE id, not the STATUS id. - // In other words, they'll be returned in descending order of when they were faved by the requesting user, not when they were created. - // - // Also note the extra return values, which correspond to the nextMaxID and prevMinID for building Link headers. - GetFavedTimelineForAccount(accountID string, maxID string, minID string, limit int) ([]*gtsmodel.Status, string, string, error) - - // GetNotificationsForAccount returns a list of notifications that pertain to the given accountID. - GetNotificationsForAccount(accountID string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, error) - - // GetUserCountForInstance returns the number of known accounts registered with the given domain. - GetUserCountForInstance(domain string) (int, error) - - // GetStatusCountForInstance returns the number of known statuses posted from the given domain. - GetStatusCountForInstance(domain string) (int, error) - - // GetDomainCountForInstance returns the number of known instances known that the given domain federates with. - GetDomainCountForInstance(domain string) (int, error) - - // GetAccountsForInstance returns a slice of accounts from the given instance, arranged by ID. - GetAccountsForInstance(domain string, maxID string, limit int) ([]*gtsmodel.Account, error) + Account + Admin + Basic + Domain + Instance + Media + Mention + Notification + Relationship + Status + Timeline /* USEFUL CONVERSION FUNCTIONS diff --git a/internal/db/domain.go b/internal/db/domain.go new file mode 100644 index 000000000..a6583c80c --- /dev/null +++ b/internal/db/domain.go @@ -0,0 +1,36 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package db + +import "net/url" + +// Domain contains DB functions related to domains and domain blocks. +type Domain interface { + // IsDomainBlocked checks if an instance-level domain block exists for the given domain string (eg., `example.org`). + IsDomainBlocked(domain string) (bool, Error) + + // AreDomainsBlocked checks if an instance-level domain block exists for any of the given domains strings, and returns true if even one is found. + AreDomainsBlocked(domains []string) (bool, Error) + + // IsURIBlocked checks if an instance-level domain block exists for the `host` in the given URI (eg., `https://example.org/users/whatever`). + IsURIBlocked(uri *url.URL) (bool, Error) + + // AreURIsBlocked checks if an instance-level domain block exists for any `host` in the given URI slice, and returns true if even one is found. + AreURIsBlocked(uris []*url.URL) (bool, Error) +} diff --git a/internal/db/error.go b/internal/db/error.go index 197c7bd68..c13bd78dd 100644 --- a/internal/db/error.go +++ b/internal/db/error.go @@ -18,16 +18,18 @@ package db -// ErrNoEntries is to be returned from the DB interface when no entries are found for a given query. -type ErrNoEntries struct{} +import "fmt" -func (e ErrNoEntries) Error() string { - return "no entries" -} +// Error denotes a database error. +type Error error -// 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" -} +var ( + // ErrNoEntries is returned when a caller expected an entry for a query, but none was found. + ErrNoEntries Error = fmt.Errorf("no entries") + // ErrMultipleEntries is returned when a caller expected ONE entry for a query, but multiples were found. + ErrMultipleEntries Error = fmt.Errorf("multiple entries") + // ErrAlreadyExists is returned when a caller tries to insert a database entry that already exists in the db. + ErrAlreadyExists Error = fmt.Errorf("already exists") + // ErrUnknown denotes an unknown database error. + ErrUnknown Error = fmt.Errorf("unknown error") +) diff --git a/internal/db/instance.go b/internal/db/instance.go new file mode 100644 index 000000000..1f7c83e4f --- /dev/null +++ b/internal/db/instance.go @@ -0,0 +1,36 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package db + +import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + +// Instance contains functions for instance-level actions (counting instance users etc.). +type Instance interface { + // CountInstanceUsers returns the number of known accounts registered with the given domain. + CountInstanceUsers(domain string) (int, Error) + + // CountInstanceStatuses returns the number of known statuses posted from the given domain. + CountInstanceStatuses(domain string) (int, Error) + + // CountInstanceDomains returns the number of known instances known that the given domain federates with. + CountInstanceDomains(domain string) (int, Error) + + // GetInstanceAccounts returns a slice of accounts from the given instance, arranged by ID. + GetInstanceAccounts(domain string, maxID string, limit int) ([]*gtsmodel.Account, Error) +} diff --git a/internal/db/media.go b/internal/db/media.go new file mode 100644 index 000000000..db4db3411 --- /dev/null +++ b/internal/db/media.go @@ -0,0 +1,27 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package db + +import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + +// Media contains functions related to creating/getting/removing media attachments. +type Media interface { + // GetAttachmentByID gets a single attachment by its ID + GetAttachmentByID(id string) (*gtsmodel.MediaAttachment, Error) +} diff --git a/internal/db/mention.go b/internal/db/mention.go new file mode 100644 index 000000000..cb1c56dc1 --- /dev/null +++ b/internal/db/mention.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 . +*/ + +package db + +import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + +// Mention contains functions for getting/creating mentions in the database. +type Mention interface { + // GetMention gets a single mention by ID + GetMention(id string) (*gtsmodel.Mention, Error) + + // GetMentions gets multiple mentions. + GetMentions(ids []string) ([]*gtsmodel.Mention, Error) +} diff --git a/internal/db/notification.go b/internal/db/notification.go new file mode 100644 index 000000000..326f0f149 --- /dev/null +++ b/internal/db/notification.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 . +*/ + +package db + +import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + +// Notification contains functions for creating and getting notifications. +type Notification interface { + // GetNotifications returns a slice of notifications that pertain to the given accountID. + // + // Returned notifications will be ordered ID descending (ie., highest/newest to lowest/oldest). + GetNotifications(accountID string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, Error) + // GetNotification returns one notification according to its id. + GetNotification(id string) (*gtsmodel.Notification, Error) +} diff --git a/internal/db/pg/account.go b/internal/db/pg/account.go new file mode 100644 index 000000000..3889c6601 --- /dev/null +++ b/internal/db/pg/account.go @@ -0,0 +1,256 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package pg + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/go-pg/pg/v10" + "github.com/go-pg/pg/v10/orm" + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +type accountDB struct { + config *config.Config + conn *pg.DB + log *logrus.Logger + cancel context.CancelFunc +} + +func (a *accountDB) newAccountQ(account *gtsmodel.Account) *orm.Query { + return a.conn.Model(account). + Relation("AvatarMediaAttachment"). + Relation("HeaderMediaAttachment") +} + +func (a *accountDB) GetAccountByID(id string) (*gtsmodel.Account, db.Error) { + account := >smodel.Account{} + + q := a.newAccountQ(account). + Where("account.id = ?", id) + + err := processErrorResponse(q.Select()) + + return account, err +} + +func (a *accountDB) GetAccountByURI(uri string) (*gtsmodel.Account, db.Error) { + account := >smodel.Account{} + + q := a.newAccountQ(account). + Where("account.uri = ?", uri) + + err := processErrorResponse(q.Select()) + + return account, err +} + +func (a *accountDB) GetAccountByURL(uri string) (*gtsmodel.Account, db.Error) { + account := >smodel.Account{} + + q := a.newAccountQ(account). + Where("account.url = ?", uri) + + err := processErrorResponse(q.Select()) + + return account, err +} + +func (a *accountDB) GetInstanceAccount(domain string) (*gtsmodel.Account, db.Error) { + account := >smodel.Account{} + + q := a.newAccountQ(account) + + if domain == "" { + q = q. + Where("account.username = ?", domain). + Where("account.domain = ?", domain) + } else { + q = q. + Where("account.username = ?", domain). + Where("? IS NULL", pg.Ident("domain")) + } + + err := processErrorResponse(q.Select()) + + return account, err +} + +func (a *accountDB) GetAccountLastPosted(accountID string) (time.Time, db.Error) { + status := >smodel.Status{} + + q := a.conn.Model(status). + Order("id DESC"). + Limit(1). + Where("account_id = ?", accountID). + Column("created_at") + + err := processErrorResponse(q.Select()) + + return status.CreatedAt, err +} + +func (a *accountDB) SetAccountHeaderOrAvatar(mediaAttachment *gtsmodel.MediaAttachment, accountID string) db.Error { + if mediaAttachment.Avatar && mediaAttachment.Header { + return errors.New("one media attachment cannot be both header and avatar") + } + + var headerOrAVI string + if mediaAttachment.Avatar { + headerOrAVI = "avatar" + } else if mediaAttachment.Header { + headerOrAVI = "header" + } else { + return errors.New("given media attachment was neither a header nor an avatar") + } + + // TODO: there are probably more side effects here that need to be handled + if _, err := a.conn.Model(mediaAttachment).OnConflict("(id) DO UPDATE").Insert(); err != nil { + return err + } + + if _, err := a.conn.Model(>smodel.Account{}).Set(fmt.Sprintf("%s_media_attachment_id = ?", headerOrAVI), mediaAttachment.ID).Where("id = ?", accountID).Update(); err != nil { + return err + } + return nil +} + +func (a *accountDB) GetLocalAccountByUsername(username string) (*gtsmodel.Account, db.Error) { + account := >smodel.Account{} + + q := a.newAccountQ(account). + Where("username = ?", username). + Where("? IS NULL", pg.Ident("domain")) + + err := processErrorResponse(q.Select()) + + return account, err +} + +func (a *accountDB) GetAccountFaves(accountID string) ([]*gtsmodel.StatusFave, db.Error) { + faves := []*gtsmodel.StatusFave{} + + if err := a.conn.Model(&faves). + Where("account_id = ?", accountID). + Select(); err != nil { + if err == pg.ErrNoRows { + return faves, nil + } + return nil, err + } + return faves, nil +} + +func (a *accountDB) CountAccountStatuses(accountID string) (int, db.Error) { + return a.conn.Model(>smodel.Status{}).Where("account_id = ?", accountID).Count() +} + +func (a *accountDB) GetAccountStatuses(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, db.Error) { + a.log.Debugf("getting statuses for account %s", accountID) + statuses := []*gtsmodel.Status{} + + q := a.conn.Model(&statuses).Order("id DESC") + if accountID != "" { + q = q.Where("account_id = ?", accountID) + } + + if limit != 0 { + q = q.Limit(limit) + } + + if excludeReplies { + q = q.Where("? IS NULL", pg.Ident("in_reply_to_id")) + } + + if pinnedOnly { + q = q.Where("pinned = ?", true) + } + + if mediaOnly { + q = q.WhereGroup(func(q *pg.Query) (*pg.Query, error) { + return q.Where("? IS NOT NULL", pg.Ident("attachments")).Where("attachments != '{}'"), nil + }) + } + + if maxID != "" { + q = q.Where("id < ?", maxID) + } + + if err := q.Select(); err != nil { + if err == pg.ErrNoRows { + return nil, db.ErrNoEntries + } + return nil, err + } + + if len(statuses) == 0 { + return nil, db.ErrNoEntries + } + + a.log.Debugf("returning statuses for account %s", accountID) + return statuses, nil +} + +func (a *accountDB) GetAccountBlocks(accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, db.Error) { + blocks := []*gtsmodel.Block{} + + fq := a.conn.Model(&blocks). + Where("block.account_id = ?", accountID). + Relation("TargetAccount"). + Order("block.id DESC") + + if maxID != "" { + fq = fq.Where("block.id < ?", maxID) + } + + if sinceID != "" { + fq = fq.Where("block.id > ?", sinceID) + } + + if limit > 0 { + fq = fq.Limit(limit) + } + + err := fq.Select() + if err != nil { + if err == pg.ErrNoRows { + return nil, "", "", db.ErrNoEntries + } + return nil, "", "", err + } + + if len(blocks) == 0 { + return nil, "", "", db.ErrNoEntries + } + + accounts := []*gtsmodel.Account{} + for _, b := range blocks { + accounts = append(accounts, b.TargetAccount) + } + + nextMaxID := blocks[len(blocks)-1].ID + prevMinID := blocks[0].ID + return accounts, nextMaxID, prevMinID, nil +} diff --git a/internal/db/pg/account_test.go b/internal/db/pg/account_test.go new file mode 100644 index 000000000..7ea5ff39a --- /dev/null +++ b/internal/db/pg/account_test.go @@ -0,0 +1,70 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package pg_test + +import ( + "testing" + + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/testrig" +) + +type AccountTestSuite struct { + PGStandardTestSuite +} + +func (suite *AccountTestSuite) SetupSuite() { + suite.testTokens = testrig.NewTestTokens() + suite.testClients = testrig.NewTestClients() + suite.testApplications = testrig.NewTestApplications() + suite.testUsers = testrig.NewTestUsers() + suite.testAccounts = testrig.NewTestAccounts() + suite.testAttachments = testrig.NewTestAttachments() + suite.testStatuses = testrig.NewTestStatuses() + suite.testTags = testrig.NewTestTags() + suite.testMentions = testrig.NewTestMentions() +} + +func (suite *AccountTestSuite) SetupTest() { + suite.config = testrig.NewTestConfig() + suite.db = testrig.NewTestDB() + suite.log = testrig.NewTestLog() + + testrig.StandardDBSetup(suite.db, suite.testAccounts) +} + +func (suite *AccountTestSuite) TearDownTest() { + testrig.StandardDBTeardown(suite.db) +} + +func (suite *AccountTestSuite) TestGetAccountByIDWithExtras() { + account, err := suite.db.GetAccountByID(suite.testAccounts["local_account_1"].ID) + if err != nil { + suite.FailNow(err.Error()) + } + suite.NotNil(account) + suite.NotNil(account.AvatarMediaAttachment) + suite.NotEmpty(account.AvatarMediaAttachment.URL) + suite.NotNil(account.HeaderMediaAttachment) + suite.NotEmpty(account.HeaderMediaAttachment.URL) +} + +func TestAccountTestSuite(t *testing.T) { + suite.Run(t, new(AccountTestSuite)) +} diff --git a/internal/db/pg/admin.go b/internal/db/pg/admin.go new file mode 100644 index 000000000..854f56ef0 --- /dev/null +++ b/internal/db/pg/admin.go @@ -0,0 +1,235 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package pg + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "fmt" + "net" + "net/mail" + "strings" + "time" + + "github.com/go-pg/pg/v10" + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/util" + "golang.org/x/crypto/bcrypt" +) + +type adminDB struct { + config *config.Config + conn *pg.DB + log *logrus.Logger + cancel context.CancelFunc +} + +func (a *adminDB) IsUsernameAvailable(username string) db.Error { + // if no error we fail because it means we found something + // if error but it's not pg.ErrNoRows then we fail + // if err is pg.ErrNoRows we're good, we found nothing so continue + if err := a.conn.Model(>smodel.Account{}).Where("username = ?", username).Where("domain = ?", nil).Select(); err == nil { + return fmt.Errorf("username %s already in use", username) + } else if err != pg.ErrNoRows { + return fmt.Errorf("db error: %s", err) + } + return nil +} + +func (a *adminDB) IsEmailAvailable(email string) db.Error { + // parse the domain from the email + m, err := mail.ParseAddress(email) + if err != nil { + return fmt.Errorf("error parsing email address %s: %s", email, err) + } + domain := strings.Split(m.Address, "@")[1] // domain will always be the second part after @ + + // check if the email domain is blocked + if err := a.conn.Model(>smodel.EmailDomainBlock{}).Where("domain = ?", domain).Select(); err == nil { + // fail because we found something + return fmt.Errorf("email domain %s is blocked", domain) + } else if err != pg.ErrNoRows { + // fail because we got an unexpected error + return fmt.Errorf("db error: %s", err) + } + + // check if this email is associated with a user already + if err := a.conn.Model(>smodel.User{}).Where("email = ?", email).WhereOr("unconfirmed_email = ?", email).Select(); err == nil { + // fail because we found something + return fmt.Errorf("email %s already in use", email) + } else if err != pg.ErrNoRows { + // fail because we got an unexpected error + return fmt.Errorf("db error: %s", err) + } + return nil +} + +func (a *adminDB) NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string, emailVerified bool, admin bool) (*gtsmodel.User, db.Error) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + a.log.Errorf("error creating new rsa key: %s", err) + return nil, err + } + + // if something went wrong while creating a user, we might already have an account, so check here first... + acct := >smodel.Account{} + err = a.conn.Model(acct).Where("username = ?", username).Where("? IS NULL", pg.Ident("domain")).Select() + if err != nil { + // there's been an actual error + if err != pg.ErrNoRows { + return nil, fmt.Errorf("db error checking existence of account: %s", err) + } + + // we just don't have an account yet create one + newAccountURIs := util.GenerateURIsForAccount(username, a.config.Protocol, a.config.Host) + newAccountID, err := id.NewRandomULID() + if err != nil { + return nil, err + } + + acct = >smodel.Account{ + ID: newAccountID, + Username: username, + DisplayName: username, + Reason: reason, + URL: newAccountURIs.UserURL, + PrivateKey: key, + PublicKey: &key.PublicKey, + PublicKeyURI: newAccountURIs.PublicKeyURI, + ActorType: gtsmodel.ActivityStreamsPerson, + URI: newAccountURIs.UserURI, + InboxURI: newAccountURIs.InboxURI, + OutboxURI: newAccountURIs.OutboxURI, + FollowersURI: newAccountURIs.FollowersURI, + FollowingURI: newAccountURIs.FollowingURI, + FeaturedCollectionURI: newAccountURIs.CollectionURI, + } + if _, err = a.conn.Model(acct).Insert(); err != nil { + return nil, err + } + } + + pw, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return nil, fmt.Errorf("error hashing password: %s", err) + } + + newUserID, err := id.NewRandomULID() + if err != nil { + return nil, err + } + + u := >smodel.User{ + ID: newUserID, + AccountID: acct.ID, + EncryptedPassword: string(pw), + SignUpIP: signUpIP.To4(), + Locale: locale, + UnconfirmedEmail: email, + CreatedByApplicationID: appID, + Approved: !requireApproval, // if we don't require moderator approval, just pre-approve the user + } + + if emailVerified { + u.ConfirmedAt = time.Now() + u.Email = email + } + + if admin { + u.Admin = true + u.Moderator = true + } + + if _, err = a.conn.Model(u).Insert(); err != nil { + return nil, err + } + + return u, nil +} + +func (a *adminDB) CreateInstanceAccount() db.Error { + username := a.config.Host + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + a.log.Errorf("error creating new rsa key: %s", err) + return err + } + + aID, err := id.NewRandomULID() + if err != nil { + return err + } + + newAccountURIs := util.GenerateURIsForAccount(username, a.config.Protocol, a.config.Host) + acct := >smodel.Account{ + ID: aID, + Username: a.config.Host, + DisplayName: username, + URL: newAccountURIs.UserURL, + PrivateKey: key, + PublicKey: &key.PublicKey, + PublicKeyURI: newAccountURIs.PublicKeyURI, + ActorType: gtsmodel.ActivityStreamsPerson, + URI: newAccountURIs.UserURI, + InboxURI: newAccountURIs.InboxURI, + OutboxURI: newAccountURIs.OutboxURI, + FollowersURI: newAccountURIs.FollowersURI, + FollowingURI: newAccountURIs.FollowingURI, + FeaturedCollectionURI: newAccountURIs.CollectionURI, + } + inserted, err := a.conn.Model(acct).Where("username = ?", username).SelectOrInsert() + if err != nil { + return err + } + if inserted { + a.log.Infof("created instance account %s with id %s", username, acct.ID) + } else { + a.log.Infof("instance account %s already exists with id %s", username, acct.ID) + } + return nil +} + +func (a *adminDB) CreateInstanceInstance() db.Error { + iID, err := id.NewRandomULID() + if err != nil { + return err + } + + i := >smodel.Instance{ + ID: iID, + Domain: a.config.Host, + Title: a.config.Host, + URI: fmt.Sprintf("%s://%s", a.config.Protocol, a.config.Host), + } + inserted, err := a.conn.Model(i).Where("domain = ?", a.config.Host).SelectOrInsert() + if err != nil { + return err + } + if inserted { + a.log.Infof("created instance instance %s with id %s", a.config.Host, i.ID) + } else { + a.log.Infof("instance instance %s already exists with id %s", a.config.Host, i.ID) + } + return nil +} diff --git a/internal/db/pg/basic.go b/internal/db/pg/basic.go new file mode 100644 index 000000000..6e76b4450 --- /dev/null +++ b/internal/db/pg/basic.go @@ -0,0 +1,205 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package pg + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/go-pg/pg/v10" + "github.com/go-pg/pg/v10/orm" + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" +) + +type basicDB struct { + config *config.Config + conn *pg.DB + log *logrus.Logger + cancel context.CancelFunc +} + +func (b *basicDB) Put(i interface{}) db.Error { + _, err := b.conn.Model(i).Insert(i) + if err != nil && strings.Contains(err.Error(), "duplicate key value violates unique constraint") { + return db.ErrAlreadyExists + } + return err +} + +func (b *basicDB) GetByID(id string, i interface{}) db.Error { + if err := b.conn.Model(i).Where("id = ?", id).Select(); err != nil { + if err == pg.ErrNoRows { + return db.ErrNoEntries + } + return err + + } + return nil +} + +func (b *basicDB) GetWhere(where []db.Where, i interface{}) db.Error { + if len(where) == 0 { + return errors.New("no queries provided") + } + + q := b.conn.Model(i) + for _, w := range where { + + if w.Value == nil { + q = q.Where("? IS NULL", pg.Ident(w.Key)) + } else { + if w.CaseInsensitive { + q = q.Where("LOWER(?) = LOWER(?)", pg.Safe(w.Key), w.Value) + } else { + q = q.Where("? = ?", pg.Safe(w.Key), w.Value) + } + } + } + + if err := q.Select(); err != nil { + if err == pg.ErrNoRows { + return db.ErrNoEntries + } + return err + } + return nil +} + +func (b *basicDB) GetAll(i interface{}) db.Error { + if err := b.conn.Model(i).Select(); err != nil { + if err == pg.ErrNoRows { + return db.ErrNoEntries + } + return err + } + return nil +} + +func (b *basicDB) DeleteByID(id string, i interface{}) db.Error { + if _, err := b.conn.Model(i).Where("id = ?", id).Delete(); err != nil { + // if there are no rows *anyway* then that's fine + // just return err if there's an actual error + if err != pg.ErrNoRows { + return err + } + } + return nil +} + +func (b *basicDB) DeleteWhere(where []db.Where, i interface{}) db.Error { + if len(where) == 0 { + return errors.New("no queries provided") + } + + q := b.conn.Model(i) + for _, w := range where { + q = q.Where("? = ?", pg.Safe(w.Key), w.Value) + } + + if _, err := q.Delete(); err != nil { + // if there are no rows *anyway* then that's fine + // just return err if there's an actual error + if err != pg.ErrNoRows { + return err + } + } + return nil +} + +func (b *basicDB) Upsert(i interface{}, conflictColumn string) db.Error { + if _, err := b.conn.Model(i).OnConflict(fmt.Sprintf("(%s) DO UPDATE", conflictColumn)).Insert(); err != nil { + if err == pg.ErrNoRows { + return db.ErrNoEntries + } + return err + } + return nil +} + +func (b *basicDB) UpdateByID(id string, i interface{}) db.Error { + if _, err := b.conn.Model(i).Where("id = ?", id).OnConflict("(id) DO UPDATE").Insert(); err != nil { + if err == pg.ErrNoRows { + return db.ErrNoEntries + } + return err + } + return nil +} + +func (b *basicDB) UpdateOneByID(id string, key string, value interface{}, i interface{}) db.Error { + _, err := b.conn.Model(i).Set("? = ?", pg.Safe(key), value).Where("id = ?", id).Update() + return err +} + +func (b *basicDB) UpdateWhere(where []db.Where, key string, value interface{}, i interface{}) db.Error { + q := b.conn.Model(i) + + for _, w := range where { + if w.Value == nil { + q = q.Where("? IS NULL", pg.Ident(w.Key)) + } else { + if w.CaseInsensitive { + q = q.Where("LOWER(?) = LOWER(?)", pg.Safe(w.Key), w.Value) + } else { + q = q.Where("? = ?", pg.Safe(w.Key), w.Value) + } + } + } + + q = q.Set("? = ?", pg.Safe(key), value) + + _, err := q.Update() + + return err +} + +func (b *basicDB) CreateTable(i interface{}) db.Error { + return b.conn.Model(i).CreateTable(&orm.CreateTableOptions{ + IfNotExists: true, + }) +} + +func (b *basicDB) DropTable(i interface{}) db.Error { + return b.conn.Model(i).DropTable(&orm.DropTableOptions{ + IfExists: true, + }) +} + +func (b *basicDB) RegisterTable(i interface{}) db.Error { + orm.RegisterTable(i) + return nil +} + +func (b *basicDB) IsHealthy(ctx context.Context) db.Error { + return b.conn.Ping(ctx) +} + +func (b *basicDB) Stop(ctx context.Context) db.Error { + b.log.Info("closing db connection") + if err := b.conn.Close(); err != nil { + // only cancel if there's a problem closing the db + b.cancel() + return err + } + return nil +} diff --git a/internal/db/pg/blocks.go b/internal/db/pg/blocks.go deleted file mode 100644 index a6fc1f859..000000000 --- a/internal/db/pg/blocks.go +++ /dev/null @@ -1,67 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ - -package pg - -import ( - "github.com/go-pg/pg/v10" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func (ps *postgresService) GetBlocksForAccount(accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, error) { - blocks := []*gtsmodel.Block{} - - fq := ps.conn.Model(&blocks). - Where("block.account_id = ?", accountID). - Relation("TargetAccount"). - Order("block.id DESC") - - if maxID != "" { - fq = fq.Where("block.id < ?", maxID) - } - - if sinceID != "" { - fq = fq.Where("block.id > ?", sinceID) - } - - if limit > 0 { - fq = fq.Limit(limit) - } - - err := fq.Select() - if err != nil { - if err == pg.ErrNoRows { - return nil, "", "", db.ErrNoEntries{} - } - return nil, "", "", err - } - - if len(blocks) == 0 { - return nil, "", "", db.ErrNoEntries{} - } - - accounts := []*gtsmodel.Account{} - for _, b := range blocks { - accounts = append(accounts, b.TargetAccount) - } - - nextMaxID := blocks[len(blocks)-1].ID - prevMinID := blocks[0].ID - return accounts, nextMaxID, prevMinID, nil -} diff --git a/internal/db/pg/domain.go b/internal/db/pg/domain.go new file mode 100644 index 000000000..4e9b2ab48 --- /dev/null +++ b/internal/db/pg/domain.go @@ -0,0 +1,83 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package pg + +import ( + "context" + "net/url" + + "github.com/go-pg/pg/v10" + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/util" +) + +type domainDB struct { + config *config.Config + conn *pg.DB + log *logrus.Logger + cancel context.CancelFunc +} + +func (d *domainDB) IsDomainBlocked(domain string) (bool, db.Error) { + if domain == "" { + return false, nil + } + + blocked, err := d.conn. + Model(>smodel.DomainBlock{}). + Where("LOWER(domain) = LOWER(?)", domain). + Exists() + + err = processErrorResponse(err) + + return blocked, err +} + +func (d *domainDB) AreDomainsBlocked(domains []string) (bool, db.Error) { + // filter out any doubles + uniqueDomains := util.UniqueStrings(domains) + + for _, domain := range uniqueDomains { + if blocked, err := d.IsDomainBlocked(domain); err != nil { + return false, err + } else if blocked { + return blocked, nil + } + } + + // no blocks found + return false, nil +} + +func (d *domainDB) IsURIBlocked(uri *url.URL) (bool, db.Error) { + domain := uri.Hostname() + return d.IsDomainBlocked(domain) +} + +func (d *domainDB) AreURIsBlocked(uris []*url.URL) (bool, db.Error) { + domains := []string{} + for _, uri := range uris { + domains = append(domains, uri.Hostname()) + } + + return d.AreDomainsBlocked(domains) +} diff --git a/internal/db/pg/get.go b/internal/db/pg/get.go deleted file mode 100644 index d48c43520..000000000 --- a/internal/db/pg/get.go +++ /dev/null @@ -1,75 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ - -package pg - -import ( - "errors" - - "github.com/go-pg/pg/v10" - "github.com/superseriousbusiness/gotosocial/internal/db" -) - -func (ps *postgresService) GetByID(id string, i interface{}) error { - if err := ps.conn.Model(i).Where("id = ?", id).Select(); err != nil { - if err == pg.ErrNoRows { - return db.ErrNoEntries{} - } - return err - - } - return nil -} - -func (ps *postgresService) GetWhere(where []db.Where, i interface{}) error { - if len(where) == 0 { - return errors.New("no queries provided") - } - - q := ps.conn.Model(i) - for _, w := range where { - - if w.Value == nil { - q = q.Where("? IS NULL", pg.Ident(w.Key)) - } else { - if w.CaseInsensitive { - q = q.Where("LOWER(?) = LOWER(?)", pg.Safe(w.Key), w.Value) - } else { - q = q.Where("? = ?", pg.Safe(w.Key), w.Value) - } - } - } - - if err := q.Select(); err != nil { - if err == pg.ErrNoRows { - return db.ErrNoEntries{} - } - return err - } - return nil -} - -func (ps *postgresService) GetAll(i interface{}) error { - if err := ps.conn.Model(i).Select(); err != nil { - if err == pg.ErrNoRows { - return db.ErrNoEntries{} - } - return err - } - return nil -} diff --git a/internal/db/pg/instance.go b/internal/db/pg/instance.go index c551b2a49..968832ca5 100644 --- a/internal/db/pg/instance.go +++ b/internal/db/pg/instance.go @@ -19,15 +19,26 @@ package pg import ( + "context" + "github.com/go-pg/pg/v10" + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) -func (ps *postgresService) GetUserCountForInstance(domain string) (int, error) { - q := ps.conn.Model(&[]*gtsmodel.Account{}) +type instanceDB struct { + config *config.Config + conn *pg.DB + log *logrus.Logger + cancel context.CancelFunc +} - if domain == ps.config.Host { +func (i *instanceDB) CountInstanceUsers(domain string) (int, db.Error) { + q := i.conn.Model(&[]*gtsmodel.Account{}) + + if domain == i.config.Host { // if the domain is *this* domain, just count where the domain field is null q = q.Where("? IS NULL", pg.Ident("domain")) } else { @@ -40,10 +51,10 @@ func (ps *postgresService) GetUserCountForInstance(domain string) (int, error) { return q.Count() } -func (ps *postgresService) GetStatusCountForInstance(domain string) (int, error) { - q := ps.conn.Model(&[]*gtsmodel.Status{}) +func (i *instanceDB) CountInstanceStatuses(domain string) (int, db.Error) { + q := i.conn.Model(&[]*gtsmodel.Status{}) - if domain == ps.config.Host { + if domain == i.config.Host { // if the domain is *this* domain, just count where local is true q = q.Where("local = ?", true) } else { @@ -55,10 +66,10 @@ func (ps *postgresService) GetStatusCountForInstance(domain string) (int, error) return q.Count() } -func (ps *postgresService) GetDomainCountForInstance(domain string) (int, error) { - q := ps.conn.Model(&[]*gtsmodel.Instance{}) +func (i *instanceDB) CountInstanceDomains(domain string) (int, db.Error) { + q := i.conn.Model(&[]*gtsmodel.Instance{}) - if domain == ps.config.Host { + if domain == i.config.Host { // if the domain is *this* domain, just count other instances it knows about // exclude domains that are blocked q = q.Where("domain != ?", domain).Where("? IS NULL", pg.Ident("suspended_at")) @@ -70,12 +81,12 @@ func (ps *postgresService) GetDomainCountForInstance(domain string) (int, error) return q.Count() } -func (ps *postgresService) GetAccountsForInstance(domain string, maxID string, limit int) ([]*gtsmodel.Account, error) { - ps.log.Debug("GetAccountsForInstance") +func (i *instanceDB) GetInstanceAccounts(domain string, maxID string, limit int) ([]*gtsmodel.Account, db.Error) { + i.log.Debug("GetAccountsForInstance") accounts := []*gtsmodel.Account{} - q := ps.conn.Model(&accounts).Where("domain = ?", domain).Order("id DESC") + q := i.conn.Model(&accounts).Where("domain = ?", domain).Order("id DESC") if maxID != "" { q = q.Where("id < ?", maxID) @@ -88,13 +99,13 @@ func (ps *postgresService) GetAccountsForInstance(domain string, maxID string, l err := q.Select() if err != nil { if err == pg.ErrNoRows { - return nil, db.ErrNoEntries{} + return nil, db.ErrNoEntries } return nil, err } if len(accounts) == 0 { - return nil, db.ErrNoEntries{} + return nil, db.ErrNoEntries } return accounts, nil diff --git a/internal/db/pg/delete.go b/internal/db/pg/media.go similarity index 53% rename from internal/db/pg/delete.go rename to internal/db/pg/media.go index 0f288353e..618030af3 100644 --- a/internal/db/pg/delete.go +++ b/internal/db/pg/media.go @@ -19,39 +19,35 @@ package pg import ( - "errors" + "context" "github.com/go-pg/pg/v10" + "github.com/go-pg/pg/v10/orm" + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) -func (ps *postgresService) DeleteByID(id string, i interface{}) error { - if _, err := ps.conn.Model(i).Where("id = ?", id).Delete(); err != nil { - // if there are no rows *anyway* then that's fine - // just return err if there's an actual error - if err != pg.ErrNoRows { - return err - } - } - return nil +type mediaDB struct { + config *config.Config + conn *pg.DB + log *logrus.Logger + cancel context.CancelFunc } -func (ps *postgresService) DeleteWhere(where []db.Where, i interface{}) error { - if len(where) == 0 { - return errors.New("no queries provided") - } - - q := ps.conn.Model(i) - for _, w := range where { - q = q.Where("? = ?", pg.Safe(w.Key), w.Value) - } - - if _, err := q.Delete(); err != nil { - // if there are no rows *anyway* then that's fine - // just return err if there's an actual error - if err != pg.ErrNoRows { - return err - } - } - return nil +func (m *mediaDB) newMediaQ(i interface{}) *orm.Query { + return m.conn.Model(i). + Relation("Account") +} + +func (m *mediaDB) GetAttachmentByID(id string) (*gtsmodel.MediaAttachment, db.Error) { + attachment := >smodel.MediaAttachment{} + + q := m.newMediaQ(attachment). + Where("media_attachment.id = ?", id) + + err := processErrorResponse(q.Select()) + + return attachment, err } diff --git a/internal/db/pg/mention.go b/internal/db/pg/mention.go new file mode 100644 index 000000000..b31f07b67 --- /dev/null +++ b/internal/db/pg/mention.go @@ -0,0 +1,108 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package pg + +import ( + "context" + + "github.com/go-pg/pg/v10" + "github.com/go-pg/pg/v10/orm" + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/cache" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +type mentionDB struct { + config *config.Config + conn *pg.DB + log *logrus.Logger + cancel context.CancelFunc + cache cache.Cache +} + +func (m *mentionDB) cacheMention(id string, mention *gtsmodel.Mention) { + if m.cache == nil { + m.cache = cache.New() + } + + if err := m.cache.Store(id, mention); err != nil { + m.log.Panicf("mentionDB: error storing in cache: %s", err) + } +} + +func (m *mentionDB) mentionCached(id string) (*gtsmodel.Mention, bool) { + if m.cache == nil { + m.cache = cache.New() + return nil, false + } + + mI, err := m.cache.Fetch(id) + if err != nil || mI == nil { + return nil, false + } + + mention, ok := mI.(*gtsmodel.Mention) + if !ok { + m.log.Panicf("mentionDB: cached interface with key %s was not a mention", id) + } + + return mention, true +} + +func (m *mentionDB) newMentionQ(i interface{}) *orm.Query { + return m.conn.Model(i). + Relation("Status"). + Relation("OriginAccount"). + Relation("TargetAccount") +} + +func (m *mentionDB) GetMention(id string) (*gtsmodel.Mention, db.Error) { + if mention, cached := m.mentionCached(id); cached { + return mention, nil + } + + mention := >smodel.Mention{} + + q := m.newMentionQ(mention). + Where("mention.id = ?", id) + + err := processErrorResponse(q.Select()) + + if err == nil && mention != nil { + m.cacheMention(id, mention) + } + + return mention, err +} + +func (m *mentionDB) GetMentions(ids []string) ([]*gtsmodel.Mention, db.Error) { + mentions := []*gtsmodel.Mention{} + + for _, i := range ids { + mention, err := m.GetMention(i) + if err != nil { + return nil, processErrorResponse(err) + } + mentions = append(mentions, mention) + } + + return mentions, nil +} diff --git a/internal/db/pg/notification.go b/internal/db/pg/notification.go new file mode 100644 index 000000000..281a76d85 --- /dev/null +++ b/internal/db/pg/notification.go @@ -0,0 +1,135 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package pg + +import ( + "context" + + "github.com/go-pg/pg/v10" + "github.com/go-pg/pg/v10/orm" + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/cache" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +type notificationDB struct { + config *config.Config + conn *pg.DB + log *logrus.Logger + cancel context.CancelFunc + cache cache.Cache +} + +func (n *notificationDB) cacheNotification(id string, notification *gtsmodel.Notification) { + if n.cache == nil { + n.cache = cache.New() + } + + if err := n.cache.Store(id, notification); err != nil { + n.log.Panicf("notificationDB: error storing in cache: %s", err) + } +} + +func (n *notificationDB) notificationCached(id string) (*gtsmodel.Notification, bool) { + if n.cache == nil { + n.cache = cache.New() + return nil, false + } + + nI, err := n.cache.Fetch(id) + if err != nil || nI == nil { + return nil, false + } + + notification, ok := nI.(*gtsmodel.Notification) + if !ok { + n.log.Panicf("notificationDB: cached interface with key %s was not a notification", id) + } + + return notification, true +} + +func (n *notificationDB) newNotificationQ(i interface{}) *orm.Query { + return n.conn.Model(i). + Relation("OriginAccount"). + Relation("TargetAccount"). + Relation("Status") +} + +func (n *notificationDB) GetNotification(id string) (*gtsmodel.Notification, db.Error) { + if notification, cached := n.notificationCached(id); cached { + return notification, nil + } + + notification := >smodel.Notification{} + + q := n.newNotificationQ(notification). + Where("notification.id = ?", id) + + err := processErrorResponse(q.Select()) + + if err == nil && notification != nil { + n.cacheNotification(id, notification) + } + + return notification, err +} + +func (n *notificationDB) GetNotifications(accountID string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, db.Error) { + // begin by selecting just the IDs + notifIDs := []*gtsmodel.Notification{} + q := n.conn. + Model(¬ifIDs). + Column("id"). + Where("target_account_id = ?", accountID). + Order("id DESC") + + if maxID != "" { + q = q.Where("id < ?", maxID) + } + + if sinceID != "" { + q = q.Where("id > ?", sinceID) + } + + if limit != 0 { + q = q.Limit(limit) + } + + err := processErrorResponse(q.Select()) + if err != nil { + return nil, err + } + + // now we have the IDs, select the notifs one by one + // reason for this is that for each notif, we can instead get it from our cache if it's cached + notifications := []*gtsmodel.Notification{} + for _, notifID := range notifIDs { + notif, err := n.GetNotification(notifID.ID) + errP := processErrorResponse(err) + if errP != nil { + return nil, errP + } + notifications = append(notifications, notif) + } + + return notifications, nil +} diff --git a/internal/db/pg/pg.go b/internal/db/pg/pg.go index d49c50114..0437baf02 100644 --- a/internal/db/pg/pg.go +++ b/internal/db/pg/pg.go @@ -20,15 +20,11 @@ package pg import ( "context" - "crypto/rand" - "crypto/rsa" "crypto/tls" "crypto/x509" "encoding/pem" "errors" "fmt" - "net" - "net/mail" "os" "strings" "time" @@ -41,12 +37,26 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" - "github.com/superseriousbusiness/gotosocial/internal/util" - "golang.org/x/crypto/bcrypt" ) +var registerTables []interface{} = []interface{}{ + >smodel.StatusToEmoji{}, + >smodel.StatusToTag{}, +} + // postgresService satisfies the DB interface type postgresService struct { + db.Account + db.Admin + db.Basic + db.Domain + db.Instance + db.Media + db.Mention + db.Notification + db.Relationship + db.Status + db.Timeline config *config.Config conn *pg.DB log *logrus.Logger @@ -56,6 +66,11 @@ type postgresService struct { // NewPostgresService returns a postgresService derived from the provided config, which implements the go-fed DB interface. // Under the hood, it uses https://github.com/go-pg/pg to create and maintain a database connection. func NewPostgresService(ctx context.Context, c *config.Config, log *logrus.Logger) (db.DB, error) { + for _, t := range registerTables { + // https://pg.uptrace.dev/orm/many-to-many-relation/ + orm.RegisterTable(t) + } + opts, err := derivePGOptions(c) if err != nil { return nil, fmt.Errorf("could not create postgres service: %s", err) @@ -91,6 +106,72 @@ func NewPostgresService(ctx context.Context, c *config.Config, log *logrus.Logge log.Infof("connected to postgres version: %s", version) ps := &postgresService{ + Account: &accountDB{ + config: c, + conn: conn, + log: log, + cancel: cancel, + }, + Admin: &adminDB{ + config: c, + conn: conn, + log: log, + cancel: cancel, + }, + Basic: &basicDB{ + config: c, + conn: conn, + log: log, + cancel: cancel, + }, + Domain: &domainDB{ + config: c, + conn: conn, + log: log, + cancel: cancel, + }, + Instance: &instanceDB{ + config: c, + conn: conn, + log: log, + cancel: cancel, + }, + Media: &mediaDB{ + config: c, + conn: conn, + log: log, + cancel: cancel, + }, + Mention: &mentionDB{ + config: c, + conn: conn, + log: log, + cancel: cancel, + }, + Notification: ¬ificationDB{ + config: c, + conn: conn, + log: log, + cancel: cancel, + }, + Relationship: &relationshipDB{ + config: c, + conn: conn, + log: log, + cancel: cancel, + }, + Status: &statusDB{ + config: c, + conn: conn, + log: log, + cancel: cancel, + }, + Timeline: &timelineDB{ + config: c, + conn: conn, + log: log, + cancel: cancel, + }, config: c, conn: conn, log: log, @@ -199,724 +280,6 @@ func derivePGOptions(c *config.Config) (*pg.Options, error) { return options, nil } -/* - BASIC DB FUNCTIONALITY -*/ - -func (ps *postgresService) CreateTable(i interface{}) error { - return ps.conn.Model(i).CreateTable(&orm.CreateTableOptions{ - IfNotExists: true, - }) -} - -func (ps *postgresService) DropTable(i interface{}) error { - return ps.conn.Model(i).DropTable(&orm.DropTableOptions{ - IfExists: true, - }) -} - -func (ps *postgresService) Stop(ctx context.Context) error { - ps.log.Info("closing db connection") - if err := ps.conn.Close(); err != nil { - // only cancel if there's a problem closing the db - ps.cancel() - return err - } - return nil -} - -func (ps *postgresService) IsHealthy(ctx context.Context) error { - return ps.conn.Ping(ctx) -} - -func (ps *postgresService) CreateSchema(ctx context.Context) error { - models := []interface{}{ - (*gtsmodel.Account)(nil), - (*gtsmodel.Status)(nil), - (*gtsmodel.User)(nil), - } - ps.log.Info("creating db schema") - - for _, model := range models { - err := ps.conn.Model(model).CreateTable(&orm.CreateTableOptions{ - IfNotExists: true, - }) - if err != nil { - return err - } - } - - ps.log.Info("db schema created") - return nil -} - -/* - HANDY SHORTCUTS -*/ - -func (ps *postgresService) AcceptFollowRequest(originAccountID string, targetAccountID string) (*gtsmodel.Follow, error) { - // make sure the original follow request exists - fr := >smodel.FollowRequest{} - if err := ps.conn.Model(fr).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Select(); err != nil { - if err == pg.ErrMultiRows { - return nil, db.ErrNoEntries{} - } - return nil, err - } - - // create a new follow to 'replace' the request with - follow := >smodel.Follow{ - ID: fr.ID, - AccountID: originAccountID, - TargetAccountID: targetAccountID, - URI: fr.URI, - } - - // if the follow already exists, just update the URI -- we don't need to do anything else - if _, err := ps.conn.Model(follow).OnConflict("ON CONSTRAINT follows_account_id_target_account_id_key DO UPDATE set uri = ?", follow.URI).Insert(); err != nil { - return nil, err - } - - // now remove the follow request - if _, err := ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Delete(); err != nil { - return nil, err - } - - return follow, nil -} - -func (ps *postgresService) CreateInstanceAccount() error { - username := ps.config.Host - key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - ps.log.Errorf("error creating new rsa key: %s", err) - return err - } - - aID, err := id.NewRandomULID() - if err != nil { - return err - } - - newAccountURIs := util.GenerateURIsForAccount(username, ps.config.Protocol, ps.config.Host) - a := >smodel.Account{ - ID: aID, - Username: ps.config.Host, - DisplayName: username, - URL: newAccountURIs.UserURL, - PrivateKey: key, - PublicKey: &key.PublicKey, - PublicKeyURI: newAccountURIs.PublicKeyURI, - ActorType: gtsmodel.ActivityStreamsPerson, - URI: newAccountURIs.UserURI, - InboxURI: newAccountURIs.InboxURI, - OutboxURI: newAccountURIs.OutboxURI, - FollowersURI: newAccountURIs.FollowersURI, - FollowingURI: newAccountURIs.FollowingURI, - FeaturedCollectionURI: newAccountURIs.CollectionURI, - } - inserted, err := ps.conn.Model(a).Where("username = ?", username).SelectOrInsert() - if err != nil { - return err - } - if inserted { - ps.log.Infof("created instance account %s with id %s", username, a.ID) - } else { - ps.log.Infof("instance account %s already exists with id %s", username, a.ID) - } - return nil -} - -func (ps *postgresService) CreateInstanceInstance() error { - iID, err := id.NewRandomULID() - if err != nil { - return err - } - - i := >smodel.Instance{ - ID: iID, - Domain: ps.config.Host, - Title: ps.config.Host, - URI: fmt.Sprintf("%s://%s", ps.config.Protocol, ps.config.Host), - } - inserted, err := ps.conn.Model(i).Where("domain = ?", ps.config.Host).SelectOrInsert() - if err != nil { - return err - } - if inserted { - ps.log.Infof("created instance instance %s with id %s", ps.config.Host, i.ID) - } else { - ps.log.Infof("instance instance %s already exists with id %s", ps.config.Host, i.ID) - } - return nil -} - -func (ps *postgresService) GetAccountByUserID(userID string, account *gtsmodel.Account) error { - user := >smodel.User{ - ID: userID, - } - if err := ps.conn.Model(user).Where("id = ?", userID).Select(); err != nil { - if err == pg.ErrNoRows { - return db.ErrNoEntries{} - } - return err - } - if err := ps.conn.Model(account).Where("id = ?", user.AccountID).Select(); err != nil { - if err == pg.ErrNoRows { - return db.ErrNoEntries{} - } - return err - } - return nil -} - -func (ps *postgresService) GetLocalAccountByUsername(username string, account *gtsmodel.Account) error { - if err := ps.conn.Model(account).Where("username = ?", username).Where("? IS NULL", pg.Ident("domain")).Select(); err != nil { - if err == pg.ErrNoRows { - return db.ErrNoEntries{} - } - return err - } - return nil -} - -func (ps *postgresService) GetFollowRequestsForAccountID(accountID string, followRequests *[]gtsmodel.FollowRequest) error { - if err := ps.conn.Model(followRequests).Where("target_account_id = ?", accountID).Select(); err != nil { - if err == pg.ErrNoRows { - return nil - } - return err - } - return nil -} - -func (ps *postgresService) GetFollowingByAccountID(accountID string, following *[]gtsmodel.Follow) error { - if err := ps.conn.Model(following).Where("account_id = ?", accountID).Select(); err != nil { - if err == pg.ErrNoRows { - return nil - } - return err - } - return nil -} - -func (ps *postgresService) GetFollowersByAccountID(accountID string, followers *[]gtsmodel.Follow, localOnly bool) error { - - q := ps.conn.Model(followers) - - if localOnly { - // for local accounts let's get where domain is null OR where domain is an empty string, just to be safe - whereGroup := func(q *pg.Query) (*pg.Query, error) { - q = q. - WhereOr("? IS NULL", pg.Ident("a.domain")). - WhereOr("a.domain = ?", "") - return q, nil - } - - q = q.ColumnExpr("follow.*"). - Join("JOIN accounts AS a ON follow.account_id = TEXT(a.id)"). - Where("follow.target_account_id = ?", accountID). - WhereGroup(whereGroup) - } else { - q = q.Where("target_account_id = ?", accountID) - } - - if err := q.Select(); err != nil { - if err == pg.ErrNoRows { - return nil - } - return err - } - return nil -} - -func (ps *postgresService) GetFavesByAccountID(accountID string, faves *[]gtsmodel.StatusFave) error { - if err := ps.conn.Model(faves).Where("account_id = ?", accountID).Select(); err != nil { - if err == pg.ErrNoRows { - return nil - } - return err - } - return nil -} - -func (ps *postgresService) CountStatusesByAccountID(accountID string) (int, error) { - count, err := ps.conn.Model(>smodel.Status{}).Where("account_id = ?", accountID).Count() - if err != nil { - if err == pg.ErrNoRows { - return 0, nil - } - return 0, err - } - return count, nil -} - -func (ps *postgresService) GetStatusesForAccount(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, error) { - ps.log.Debugf("getting statuses for account %s", accountID) - statuses := []*gtsmodel.Status{} - - q := ps.conn.Model(&statuses).Order("id DESC") - if accountID != "" { - q = q.Where("account_id = ?", accountID) - } - - if limit != 0 { - q = q.Limit(limit) - } - - if excludeReplies { - q = q.Where("? IS NULL", pg.Ident("in_reply_to_id")) - } - - if pinnedOnly { - q = q.Where("pinned = ?", true) - } - - if mediaOnly { - q = q.WhereGroup(func(q *pg.Query) (*pg.Query, error) { - return q.Where("? IS NOT NULL", pg.Ident("attachments")).Where("attachments != '{}'"), nil - }) - } - - if maxID != "" { - q = q.Where("id < ?", maxID) - } - - if err := q.Select(); err != nil { - if err == pg.ErrNoRows { - return nil, db.ErrNoEntries{} - } - return nil, err - } - - if len(statuses) == 0 { - return nil, db.ErrNoEntries{} - } - - ps.log.Debugf("returning statuses for account %s", accountID) - return statuses, nil -} - -func (ps *postgresService) GetLastStatusForAccountID(accountID string, status *gtsmodel.Status) error { - if err := ps.conn.Model(status).Order("created_at DESC").Limit(1).Where("account_id = ?", accountID).Select(); err != nil { - if err == pg.ErrNoRows { - return db.ErrNoEntries{} - } - return err - } - return nil - -} - -func (ps *postgresService) IsUsernameAvailable(username string) error { - // if no error we fail because it means we found something - // if error but it's not pg.ErrNoRows then we fail - // if err is pg.ErrNoRows we're good, we found nothing so continue - if err := ps.conn.Model(>smodel.Account{}).Where("username = ?", username).Where("domain = ?", nil).Select(); err == nil { - return fmt.Errorf("username %s already in use", username) - } else if err != pg.ErrNoRows { - return fmt.Errorf("db error: %s", err) - } - return nil -} - -func (ps *postgresService) IsEmailAvailable(email string) error { - // parse the domain from the email - m, err := mail.ParseAddress(email) - if err != nil { - return fmt.Errorf("error parsing email address %s: %s", email, err) - } - domain := strings.Split(m.Address, "@")[1] // domain will always be the second part after @ - - // check if the email domain is blocked - if err := ps.conn.Model(>smodel.EmailDomainBlock{}).Where("domain = ?", domain).Select(); err == nil { - // fail because we found something - return fmt.Errorf("email domain %s is blocked", domain) - } else if err != pg.ErrNoRows { - // fail because we got an unexpected error - return fmt.Errorf("db error: %s", err) - } - - // check if this email is associated with a user already - if err := ps.conn.Model(>smodel.User{}).Where("email = ?", email).WhereOr("unconfirmed_email = ?", email).Select(); err == nil { - // fail because we found something - return fmt.Errorf("email %s already in use", email) - } else if err != pg.ErrNoRows { - // fail because we got an unexpected error - return fmt.Errorf("db error: %s", err) - } - return nil -} - -func (ps *postgresService) NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string, emailVerified bool, admin bool) (*gtsmodel.User, error) { - key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - ps.log.Errorf("error creating new rsa key: %s", err) - return nil, err - } - - // if something went wrong while creating a user, we might already have an account, so check here first... - a := >smodel.Account{} - err = ps.conn.Model(a).Where("username = ?", username).Where("? IS NULL", pg.Ident("domain")).Select() - if err != nil { - // there's been an actual error - if err != pg.ErrNoRows { - return nil, fmt.Errorf("db error checking existence of account: %s", err) - } - - // we just don't have an account yet create one - newAccountURIs := util.GenerateURIsForAccount(username, ps.config.Protocol, ps.config.Host) - newAccountID, err := id.NewRandomULID() - if err != nil { - return nil, err - } - - a = >smodel.Account{ - ID: newAccountID, - Username: username, - DisplayName: username, - Reason: reason, - URL: newAccountURIs.UserURL, - PrivateKey: key, - PublicKey: &key.PublicKey, - PublicKeyURI: newAccountURIs.PublicKeyURI, - ActorType: gtsmodel.ActivityStreamsPerson, - URI: newAccountURIs.UserURI, - InboxURI: newAccountURIs.InboxURI, - OutboxURI: newAccountURIs.OutboxURI, - FollowersURI: newAccountURIs.FollowersURI, - FollowingURI: newAccountURIs.FollowingURI, - FeaturedCollectionURI: newAccountURIs.CollectionURI, - } - if _, err = ps.conn.Model(a).Insert(); err != nil { - return nil, err - } - } - - pw, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - return nil, fmt.Errorf("error hashing password: %s", err) - } - - newUserID, err := id.NewRandomULID() - if err != nil { - return nil, err - } - - u := >smodel.User{ - ID: newUserID, - AccountID: a.ID, - EncryptedPassword: string(pw), - SignUpIP: signUpIP.To4(), - Locale: locale, - UnconfirmedEmail: email, - CreatedByApplicationID: appID, - Approved: !requireApproval, // if we don't require moderator approval, just pre-approve the user - } - - if emailVerified { - u.ConfirmedAt = time.Now() - u.Email = email - } - - if admin { - u.Admin = true - u.Moderator = true - } - - if _, err = ps.conn.Model(u).Insert(); err != nil { - return nil, err - } - - return u, nil -} - -func (ps *postgresService) SetHeaderOrAvatarForAccountID(mediaAttachment *gtsmodel.MediaAttachment, accountID string) error { - if mediaAttachment.Avatar && mediaAttachment.Header { - return errors.New("one media attachment cannot be both header and avatar") - } - - var headerOrAVI string - if mediaAttachment.Avatar { - headerOrAVI = "avatar" - } else if mediaAttachment.Header { - headerOrAVI = "header" - } else { - return errors.New("given media attachment was neither a header nor an avatar") - } - - // TODO: there are probably more side effects here that need to be handled - if _, err := ps.conn.Model(mediaAttachment).OnConflict("(id) DO UPDATE").Insert(); err != nil { - return err - } - - if _, err := ps.conn.Model(>smodel.Account{}).Set(fmt.Sprintf("%s_media_attachment_id = ?", headerOrAVI), mediaAttachment.ID).Where("id = ?", accountID).Update(); err != nil { - return err - } - return nil -} - -func (ps *postgresService) GetHeaderForAccountID(header *gtsmodel.MediaAttachment, accountID string) error { - acct := >smodel.Account{} - if err := ps.conn.Model(acct).Where("id = ?", accountID).Select(); err != nil { - if err == pg.ErrNoRows { - return db.ErrNoEntries{} - } - return err - } - - if acct.HeaderMediaAttachmentID == "" { - return db.ErrNoEntries{} - } - - if err := ps.conn.Model(header).Where("id = ?", acct.HeaderMediaAttachmentID).Select(); err != nil { - if err == pg.ErrNoRows { - return db.ErrNoEntries{} - } - return err - } - return nil -} - -func (ps *postgresService) GetAvatarForAccountID(avatar *gtsmodel.MediaAttachment, accountID string) error { - acct := >smodel.Account{} - if err := ps.conn.Model(acct).Where("id = ?", accountID).Select(); err != nil { - if err == pg.ErrNoRows { - return db.ErrNoEntries{} - } - return err - } - - if acct.AvatarMediaAttachmentID == "" { - return db.ErrNoEntries{} - } - - if err := ps.conn.Model(avatar).Where("id = ?", acct.AvatarMediaAttachmentID).Select(); err != nil { - if err == pg.ErrNoRows { - return db.ErrNoEntries{} - } - return err - } - return nil -} - -func (ps *postgresService) Blocked(account1 string, account2 string) (bool, error) { - // TODO: check domain blocks as well - var blocked bool - if err := ps.conn.Model(>smodel.Block{}). - Where("account_id = ?", account1).Where("target_account_id = ?", account2). - WhereOr("target_account_id = ?", account1).Where("account_id = ?", account2). - Select(); err != nil { - if err == pg.ErrNoRows { - blocked = false - return blocked, nil - } - return blocked, err - } - blocked = true - return blocked, nil -} - -func (ps *postgresService) GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, error) { - r := >smodel.Relationship{ - ID: targetAccount, - } - - // check if the requesting account follows the target account - follow := >smodel.Follow{} - if err := ps.conn.Model(follow).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Select(); err != nil { - if err != pg.ErrNoRows { - // a proper error - return nil, fmt.Errorf("getrelationship: error checking follow existence: %s", err) - } - // no follow exists so these are all false - r.Following = false - r.ShowingReblogs = false - r.Notifying = false - } else { - // follow exists so we can fill these fields out... - r.Following = true - r.ShowingReblogs = follow.ShowReblogs - r.Notifying = follow.Notify - } - - // check if the target account follows the requesting account - followedBy, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", targetAccount).Where("target_account_id = ?", requestingAccount).Exists() - if err != nil { - return nil, fmt.Errorf("getrelationship: error checking followed_by existence: %s", err) - } - r.FollowedBy = followedBy - - // check if the requesting account blocks the target account - blocking, err := ps.conn.Model(>smodel.Block{}).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Exists() - if err != nil { - return nil, fmt.Errorf("getrelationship: error checking blocking existence: %s", err) - } - r.Blocking = blocking - - // check if the target account blocks the requesting account - blockedBy, err := ps.conn.Model(>smodel.Block{}).Where("account_id = ?", targetAccount).Where("target_account_id = ?", requestingAccount).Exists() - if err != nil { - return nil, fmt.Errorf("getrelationship: error checking blocked existence: %s", err) - } - r.BlockedBy = blockedBy - - // check if there's a pending following request from requesting account to target account - requested, err := ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Exists() - if err != nil { - return nil, fmt.Errorf("getrelationship: error checking blocked existence: %s", err) - } - r.Requested = requested - - return r, nil -} - -func (ps *postgresService) Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) { - if sourceAccount == nil || targetAccount == nil { - return false, nil - } - - return ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists() -} - -func (ps *postgresService) FollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) { - if sourceAccount == nil || targetAccount == nil { - return false, nil - } - - return ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists() -} - -func (ps *postgresService) Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error) { - if account1 == nil || account2 == nil { - return false, nil - } - - // make sure account 1 follows account 2 - f1, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", account1.ID).Where("target_account_id = ?", account2.ID).Exists() - if err != nil { - if err == pg.ErrNoRows { - return false, nil - } - return false, err - } - - // make sure account 2 follows account 1 - f2, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", account2.ID).Where("target_account_id = ?", account1.ID).Exists() - if err != nil { - if err == pg.ErrNoRows { - return false, nil - } - return false, err - } - - return f1 && f2, nil -} - -func (ps *postgresService) GetReplyCountForStatus(status *gtsmodel.Status) (int, error) { - return ps.conn.Model(>smodel.Status{}).Where("in_reply_to_id = ?", status.ID).Count() -} - -func (ps *postgresService) GetReblogCountForStatus(status *gtsmodel.Status) (int, error) { - return ps.conn.Model(>smodel.Status{}).Where("boost_of_id = ?", status.ID).Count() -} - -func (ps *postgresService) GetFaveCountForStatus(status *gtsmodel.Status) (int, error) { - return ps.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Count() -} - -func (ps *postgresService) StatusFavedBy(status *gtsmodel.Status, accountID string) (bool, error) { - return ps.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() -} - -func (ps *postgresService) StatusRebloggedBy(status *gtsmodel.Status, accountID string) (bool, error) { - return ps.conn.Model(>smodel.Status{}).Where("boost_of_id = ?", status.ID).Where("account_id = ?", accountID).Exists() -} - -func (ps *postgresService) StatusMutedBy(status *gtsmodel.Status, accountID string) (bool, error) { - return ps.conn.Model(>smodel.StatusMute{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() -} - -func (ps *postgresService) StatusBookmarkedBy(status *gtsmodel.Status, accountID string) (bool, error) { - return ps.conn.Model(>smodel.StatusBookmark{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() -} - -func (ps *postgresService) WhoFavedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error) { - accounts := []*gtsmodel.Account{} - - faves := []*gtsmodel.StatusFave{} - if err := ps.conn.Model(&faves).Where("status_id = ?", status.ID).Select(); err != nil { - if err == pg.ErrNoRows { - return accounts, nil // no rows just means nobody has faved this status, so that's fine - } - return nil, err // an actual error has occurred - } - - for _, f := range faves { - acc := >smodel.Account{} - if err := ps.conn.Model(acc).Where("id = ?", f.AccountID).Select(); err != nil { - if err == pg.ErrNoRows { - continue // the account doesn't exist for some reason??? but this isn't the place to worry about that so just skip it - } - return nil, err // an actual error has occurred - } - accounts = append(accounts, acc) - } - return accounts, nil -} - -func (ps *postgresService) WhoBoostedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error) { - accounts := []*gtsmodel.Account{} - - boosts := []*gtsmodel.Status{} - if err := ps.conn.Model(&boosts).Where("boost_of_id = ?", status.ID).Select(); err != nil { - if err == pg.ErrNoRows { - return accounts, nil // no rows just means nobody has boosted this status, so that's fine - } - return nil, err // an actual error has occurred - } - - for _, f := range boosts { - acc := >smodel.Account{} - if err := ps.conn.Model(acc).Where("id = ?", f.AccountID).Select(); err != nil { - if err == pg.ErrNoRows { - continue // the account doesn't exist for some reason??? but this isn't the place to worry about that so just skip it - } - return nil, err // an actual error has occurred - } - accounts = append(accounts, acc) - } - return accounts, nil -} - -func (ps *postgresService) GetNotificationsForAccount(accountID string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, error) { - notifications := []*gtsmodel.Notification{} - - q := ps.conn.Model(¬ifications).Where("target_account_id = ?", accountID) - - if maxID != "" { - q = q.Where("id < ?", maxID) - } - - if sinceID != "" { - q = q.Where("id > ?", sinceID) - } - - if limit != 0 { - q = q.Limit(limit) - } - - q = q.Order("created_at DESC") - - if err := q.Select(); err != nil { - if err != pg.ErrNoRows { - return nil, err - } - - } - return notifications, nil -} - /* CONVERSION FUNCTIONS */ @@ -988,14 +351,14 @@ func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, ori // id, createdAt and updatedAt will be populated by the db, so we have everything we need! menchies = append(menchies, >smodel.Mention{ - StatusID: statusID, - OriginAccountID: ogAccount.ID, - OriginAccountURI: ogAccount.URI, - TargetAccountID: mentionedAccount.ID, - NameString: a, - MentionedAccountURI: mentionedAccount.URI, - MentionedAccountURL: mentionedAccount.URL, - GTSAccount: mentionedAccount, + StatusID: statusID, + OriginAccountID: ogAccount.ID, + OriginAccountURI: ogAccount.URI, + TargetAccountID: mentionedAccount.ID, + NameString: a, + TargetAccountURI: mentionedAccount.URI, + TargetAccountURL: mentionedAccount.URL, + OriginAccount: mentionedAccount, }) } return menchies, nil diff --git a/internal/db/pg/pg_test.go b/internal/db/pg/pg_test.go new file mode 100644 index 000000000..c1e10abdf --- /dev/null +++ b/internal/db/pg/pg_test.go @@ -0,0 +1,47 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package pg_test + +import ( + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +type PGStandardTestSuite struct { + // standard suite interfaces + suite.Suite + config *config.Config + db db.DB + log *logrus.Logger + + // standard suite models + testTokens map[string]*oauth.Token + testClients map[string]*oauth.Client + testApplications map[string]*gtsmodel.Application + testUsers map[string]*gtsmodel.User + testAccounts map[string]*gtsmodel.Account + testAttachments map[string]*gtsmodel.MediaAttachment + testStatuses map[string]*gtsmodel.Status + testTags map[string]*gtsmodel.Tag + testMentions map[string]*gtsmodel.Mention +} diff --git a/internal/db/pg/relationship.go b/internal/db/pg/relationship.go new file mode 100644 index 000000000..76bd50c76 --- /dev/null +++ b/internal/db/pg/relationship.go @@ -0,0 +1,276 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package pg + +import ( + "context" + "fmt" + + "github.com/go-pg/pg/v10" + "github.com/go-pg/pg/v10/orm" + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +type relationshipDB struct { + config *config.Config + conn *pg.DB + log *logrus.Logger + cancel context.CancelFunc +} + +func (r *relationshipDB) newBlockQ(block *gtsmodel.Block) *orm.Query { + return r.conn.Model(block). + Relation("Account"). + Relation("TargetAccount") +} + +func (r *relationshipDB) newFollowQ(follow interface{}) *orm.Query { + return r.conn.Model(follow). + Relation("Account"). + Relation("TargetAccount") +} + +func (r *relationshipDB) IsBlocked(account1 string, account2 string, eitherDirection bool) (bool, db.Error) { + q := r.conn. + Model(>smodel.Block{}). + Where("account_id = ?", account1). + Where("target_account_id = ?", account2) + + if eitherDirection { + q = q. + WhereOr("target_account_id = ?", account1). + Where("account_id = ?", account2) + } + + return q.Exists() +} + +func (r *relationshipDB) GetBlock(account1 string, account2 string) (*gtsmodel.Block, db.Error) { + block := >smodel.Block{} + + q := r.newBlockQ(block). + Where("block.account_id = ?", account1). + Where("block.target_account_id = ?", account2) + + err := processErrorResponse(q.Select()) + + return block, err +} + +func (r *relationshipDB) GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, db.Error) { + rel := >smodel.Relationship{ + ID: targetAccount, + } + + // check if the requesting account follows the target account + follow := >smodel.Follow{} + if err := r.conn.Model(follow).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Select(); err != nil { + if err != pg.ErrNoRows { + // a proper error + return nil, fmt.Errorf("getrelationship: error checking follow existence: %s", err) + } + // no follow exists so these are all false + rel.Following = false + rel.ShowingReblogs = false + rel.Notifying = false + } else { + // follow exists so we can fill these fields out... + rel.Following = true + rel.ShowingReblogs = follow.ShowReblogs + rel.Notifying = follow.Notify + } + + // check if the target account follows the requesting account + followedBy, err := r.conn.Model(>smodel.Follow{}).Where("account_id = ?", targetAccount).Where("target_account_id = ?", requestingAccount).Exists() + if err != nil { + return nil, fmt.Errorf("getrelationship: error checking followed_by existence: %s", err) + } + rel.FollowedBy = followedBy + + // check if the requesting account blocks the target account + blocking, err := r.conn.Model(>smodel.Block{}).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Exists() + if err != nil { + return nil, fmt.Errorf("getrelationship: error checking blocking existence: %s", err) + } + rel.Blocking = blocking + + // check if the target account blocks the requesting account + blockedBy, err := r.conn.Model(>smodel.Block{}).Where("account_id = ?", targetAccount).Where("target_account_id = ?", requestingAccount).Exists() + if err != nil { + return nil, fmt.Errorf("getrelationship: error checking blocked existence: %s", err) + } + rel.BlockedBy = blockedBy + + // check if there's a pending following request from requesting account to target account + requested, err := r.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Exists() + if err != nil { + return nil, fmt.Errorf("getrelationship: error checking blocked existence: %s", err) + } + rel.Requested = requested + + return rel, nil +} + +func (r *relationshipDB) IsFollowing(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, db.Error) { + if sourceAccount == nil || targetAccount == nil { + return false, nil + } + + q := r.conn. + Model(>smodel.Follow{}). + Where("account_id = ?", sourceAccount.ID). + Where("target_account_id = ?", targetAccount.ID) + + return q.Exists() +} + +func (r *relationshipDB) IsFollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, db.Error) { + if sourceAccount == nil || targetAccount == nil { + return false, nil + } + + q := r.conn. + Model(>smodel.FollowRequest{}). + Where("account_id = ?", sourceAccount.ID). + Where("target_account_id = ?", targetAccount.ID) + + return q.Exists() +} + +func (r *relationshipDB) IsMutualFollowing(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, db.Error) { + if account1 == nil || account2 == nil { + return false, nil + } + + // make sure account 1 follows account 2 + f1, err := r.IsFollowing(account1, account2) + if err != nil { + return false, processErrorResponse(err) + } + + // make sure account 2 follows account 1 + f2, err := r.IsFollowing(account2, account1) + if err != nil { + return false, processErrorResponse(err) + } + + return f1 && f2, nil +} + +func (r *relationshipDB) AcceptFollowRequest(originAccountID string, targetAccountID string) (*gtsmodel.Follow, db.Error) { + // make sure the original follow request exists + fr := >smodel.FollowRequest{} + if err := r.conn.Model(fr).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Select(); err != nil { + if err == pg.ErrMultiRows { + return nil, db.ErrNoEntries + } + return nil, err + } + + // create a new follow to 'replace' the request with + follow := >smodel.Follow{ + ID: fr.ID, + AccountID: originAccountID, + TargetAccountID: targetAccountID, + URI: fr.URI, + } + + // if the follow already exists, just update the URI -- we don't need to do anything else + if _, err := r.conn.Model(follow).OnConflict("ON CONSTRAINT follows_account_id_target_account_id_key DO UPDATE set uri = ?", follow.URI).Insert(); err != nil { + return nil, err + } + + // now remove the follow request + if _, err := r.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Delete(); err != nil { + return nil, err + } + + return follow, nil +} + +func (r *relationshipDB) GetAccountFollowRequests(accountID string) ([]*gtsmodel.FollowRequest, db.Error) { + followRequests := []*gtsmodel.FollowRequest{} + + q := r.newFollowQ(&followRequests). + Where("target_account_id = ?", accountID) + + err := processErrorResponse(q.Select()) + + return followRequests, err +} + +func (r *relationshipDB) GetAccountFollows(accountID string) ([]*gtsmodel.Follow, db.Error) { + follows := []*gtsmodel.Follow{} + + q := r.newFollowQ(&follows). + Where("account_id = ?", accountID) + + err := processErrorResponse(q.Select()) + + return follows, err +} + +func (r *relationshipDB) CountAccountFollows(accountID string, localOnly bool) (int, db.Error) { + return r.conn. + Model(&[]*gtsmodel.Follow{}). + Where("account_id = ?", accountID). + Count() +} + +func (r *relationshipDB) GetAccountFollowedBy(accountID string, localOnly bool) ([]*gtsmodel.Follow, db.Error) { + + follows := []*gtsmodel.Follow{} + + q := r.conn.Model(&follows) + + if localOnly { + // for local accounts let's get where domain is null OR where domain is an empty string, just to be safe + whereGroup := func(q *pg.Query) (*pg.Query, error) { + q = q. + WhereOr("? IS NULL", pg.Ident("a.domain")). + WhereOr("a.domain = ?", "") + return q, nil + } + + q = q.ColumnExpr("follow.*"). + Join("JOIN accounts AS a ON follow.account_id = TEXT(a.id)"). + Where("follow.target_account_id = ?", accountID). + WhereGroup(whereGroup) + } else { + q = q.Where("target_account_id = ?", accountID) + } + + if err := q.Select(); err != nil { + if err == pg.ErrNoRows { + return follows, nil + } + return nil, err + } + return follows, nil +} + +func (r *relationshipDB) CountAccountFollowedBy(accountID string, localOnly bool) (int, db.Error) { + return r.conn. + Model(&[]*gtsmodel.Follow{}). + Where("target_account_id = ?", accountID). + Count() +} diff --git a/internal/db/pg/status.go b/internal/db/pg/status.go new file mode 100644 index 000000000..99790428e --- /dev/null +++ b/internal/db/pg/status.go @@ -0,0 +1,318 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package pg + +import ( + "container/list" + "context" + "errors" + "time" + + "github.com/go-pg/pg/v10" + "github.com/go-pg/pg/v10/orm" + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/cache" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +type statusDB struct { + config *config.Config + conn *pg.DB + log *logrus.Logger + cancel context.CancelFunc + cache cache.Cache +} + +func (s *statusDB) cacheStatus(id string, status *gtsmodel.Status) { + if s.cache == nil { + s.cache = cache.New() + } + + if err := s.cache.Store(id, status); err != nil { + s.log.Panicf("statusDB: error storing in cache: %s", err) + } +} + +func (s *statusDB) statusCached(id string) (*gtsmodel.Status, bool) { + if s.cache == nil { + s.cache = cache.New() + return nil, false + } + + sI, err := s.cache.Fetch(id) + if err != nil || sI == nil { + return nil, false + } + + status, ok := sI.(*gtsmodel.Status) + if !ok { + s.log.Panicf("statusDB: cached interface with key %s was not a status", id) + } + + return status, true +} + +func (s *statusDB) newStatusQ(status interface{}) *orm.Query { + return s.conn.Model(status). + Relation("Attachments"). + Relation("Tags"). + Relation("Mentions"). + Relation("Emojis"). + Relation("Account"). + Relation("InReplyTo"). + Relation("InReplyToAccount"). + Relation("BoostOf"). + Relation("BoostOfAccount"). + Relation("CreatedWithApplication") +} + +func (s *statusDB) newFaveQ(faves interface{}) *orm.Query { + return s.conn.Model(faves). + Relation("Account"). + Relation("TargetAccount"). + Relation("Status") +} + +func (s *statusDB) GetStatusByID(id string) (*gtsmodel.Status, db.Error) { + if status, cached := s.statusCached(id); cached { + return status, nil + } + + status := >smodel.Status{} + + q := s.newStatusQ(status). + Where("status.id = ?", id) + + err := processErrorResponse(q.Select()) + + if err == nil && status != nil { + s.cacheStatus(id, status) + } + + return status, err +} + +func (s *statusDB) GetStatusByURI(uri string) (*gtsmodel.Status, db.Error) { + if status, cached := s.statusCached(uri); cached { + return status, nil + } + + status := >smodel.Status{} + + q := s.newStatusQ(status). + Where("LOWER(status.uri) = LOWER(?)", uri) + + err := processErrorResponse(q.Select()) + + if err == nil && status != nil { + s.cacheStatus(uri, status) + } + + return status, err +} + +func (s *statusDB) GetStatusByURL(uri string) (*gtsmodel.Status, db.Error) { + if status, cached := s.statusCached(uri); cached { + return status, nil + } + + status := >smodel.Status{} + + q := s.newStatusQ(status). + Where("LOWER(status.url) = LOWER(?)", uri) + + err := processErrorResponse(q.Select()) + + if err == nil && status != nil { + s.cacheStatus(uri, status) + } + + return status, err +} + +func (s *statusDB) PutStatus(status *gtsmodel.Status) db.Error { + transaction := func(tx *pg.Tx) error { + // create links between this status and any emojis it uses + for _, i := range status.EmojiIDs { + if _, err := tx.Model(>smodel.StatusToEmoji{ + StatusID: status.ID, + EmojiID: i, + }).Insert(); err != nil { + return err + } + } + + // create links between this status and any tags it uses + for _, i := range status.TagIDs { + if _, err := tx.Model(>smodel.StatusToTag{ + StatusID: status.ID, + TagID: i, + }).Insert(); err != nil { + return err + } + } + + // change the status ID of the media attachments to the new status + for _, a := range status.Attachments { + a.StatusID = status.ID + a.UpdatedAt = time.Now() + if _, err := s.conn.Model(a). + Where("id = ?", a.ID). + Update(); err != nil { + return err + } + } + + _, err := tx.Model(status).Insert() + return err + } + + return processErrorResponse(s.conn.RunInTransaction(context.Background(), transaction)) +} + +func (s *statusDB) GetStatusParents(status *gtsmodel.Status, onlyDirect bool) ([]*gtsmodel.Status, db.Error) { + parents := []*gtsmodel.Status{} + s.statusParent(status, &parents, onlyDirect) + + return parents, nil +} + +func (s *statusDB) statusParent(status *gtsmodel.Status, foundStatuses *[]*gtsmodel.Status, onlyDirect bool) { + if status.InReplyToID == "" { + return + } + + parentStatus, err := s.GetStatusByID(status.InReplyToID) + if err == nil { + *foundStatuses = append(*foundStatuses, parentStatus) + } + + if onlyDirect { + return + } + + s.statusParent(parentStatus, foundStatuses, false) +} + +func (s *statusDB) GetStatusChildren(status *gtsmodel.Status, onlyDirect bool, minID string) ([]*gtsmodel.Status, db.Error) { + foundStatuses := &list.List{} + foundStatuses.PushFront(status) + s.statusChildren(status, foundStatuses, onlyDirect, minID) + + children := []*gtsmodel.Status{} + for e := foundStatuses.Front(); e != nil; e = e.Next() { + entry, ok := e.Value.(*gtsmodel.Status) + if !ok { + panic(errors.New("entry in foundStatuses was not a *gtsmodel.Status")) + } + + // only append children, not the overall parent status + if entry.ID != status.ID { + children = append(children, entry) + } + } + + return children, nil +} + +func (s *statusDB) statusChildren(status *gtsmodel.Status, foundStatuses *list.List, onlyDirect bool, minID string) { + immediateChildren := []*gtsmodel.Status{} + + q := s.conn.Model(&immediateChildren).Where("in_reply_to_id = ?", status.ID) + if minID != "" { + q = q.Where("status.id > ?", minID) + } + + if err := q.Select(); err != nil { + return + } + + for _, child := range immediateChildren { + insertLoop: + for e := foundStatuses.Front(); e != nil; e = e.Next() { + entry, ok := e.Value.(*gtsmodel.Status) + if !ok { + panic(errors.New("entry in foundStatuses was not a *gtsmodel.Status")) + } + + if child.InReplyToAccountID != "" && entry.ID == child.InReplyToID { + foundStatuses.InsertAfter(child, e) + break insertLoop + } + } + + // only do one loop if we only want direct children + if onlyDirect { + return + } + s.statusChildren(child, foundStatuses, false, minID) + } +} + +func (s *statusDB) CountStatusReplies(status *gtsmodel.Status) (int, db.Error) { + return s.conn.Model(>smodel.Status{}).Where("in_reply_to_id = ?", status.ID).Count() +} + +func (s *statusDB) CountStatusReblogs(status *gtsmodel.Status) (int, db.Error) { + return s.conn.Model(>smodel.Status{}).Where("boost_of_id = ?", status.ID).Count() +} + +func (s *statusDB) CountStatusFaves(status *gtsmodel.Status) (int, db.Error) { + return s.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Count() +} + +func (s *statusDB) IsStatusFavedBy(status *gtsmodel.Status, accountID string) (bool, db.Error) { + return s.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() +} + +func (s *statusDB) IsStatusRebloggedBy(status *gtsmodel.Status, accountID string) (bool, db.Error) { + return s.conn.Model(>smodel.Status{}).Where("boost_of_id = ?", status.ID).Where("account_id = ?", accountID).Exists() +} + +func (s *statusDB) IsStatusMutedBy(status *gtsmodel.Status, accountID string) (bool, db.Error) { + return s.conn.Model(>smodel.StatusMute{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() +} + +func (s *statusDB) IsStatusBookmarkedBy(status *gtsmodel.Status, accountID string) (bool, db.Error) { + return s.conn.Model(>smodel.StatusBookmark{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() +} + +func (s *statusDB) GetStatusFaves(status *gtsmodel.Status) ([]*gtsmodel.StatusFave, db.Error) { + faves := []*gtsmodel.StatusFave{} + + q := s.newFaveQ(&faves). + Where("status_id = ?", status.ID) + + err := processErrorResponse(q.Select()) + + return faves, err +} + +func (s *statusDB) GetStatusReblogs(status *gtsmodel.Status) ([]*gtsmodel.Status, db.Error) { + reblogs := []*gtsmodel.Status{} + + q := s.newStatusQ(&reblogs). + Where("boost_of_id = ?", status.ID) + + err := processErrorResponse(q.Select()) + + return reblogs, err +} diff --git a/internal/db/pg/status_test.go b/internal/db/pg/status_test.go new file mode 100644 index 000000000..8a185757c --- /dev/null +++ b/internal/db/pg/status_test.go @@ -0,0 +1,134 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package pg_test + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/testrig" +) + +type StatusTestSuite struct { + PGStandardTestSuite +} + +func (suite *StatusTestSuite) SetupSuite() { + suite.testTokens = testrig.NewTestTokens() + suite.testClients = testrig.NewTestClients() + suite.testApplications = testrig.NewTestApplications() + suite.testUsers = testrig.NewTestUsers() + suite.testAccounts = testrig.NewTestAccounts() + suite.testAttachments = testrig.NewTestAttachments() + suite.testStatuses = testrig.NewTestStatuses() + suite.testTags = testrig.NewTestTags() + suite.testMentions = testrig.NewTestMentions() +} + +func (suite *StatusTestSuite) SetupTest() { + suite.config = testrig.NewTestConfig() + suite.db = testrig.NewTestDB() + suite.log = testrig.NewTestLog() + + testrig.StandardDBSetup(suite.db, suite.testAccounts) +} + +func (suite *StatusTestSuite) TearDownTest() { + testrig.StandardDBTeardown(suite.db) +} + +func (suite *StatusTestSuite) TestGetStatusByID() { + status, err := suite.db.GetStatusByID(suite.testStatuses["local_account_1_status_1"].ID) + if err != nil { + suite.FailNow(err.Error()) + } + suite.NotNil(status) + suite.NotNil(status.Account) + suite.NotNil(status.CreatedWithApplication) + suite.Nil(status.BoostOf) + suite.Nil(status.BoostOfAccount) + suite.Nil(status.InReplyTo) + suite.Nil(status.InReplyToAccount) +} + +func (suite *StatusTestSuite) TestGetStatusByURI() { + status, err := suite.db.GetStatusByURI(suite.testStatuses["local_account_1_status_1"].URI) + if err != nil { + suite.FailNow(err.Error()) + } + suite.NotNil(status) + suite.NotNil(status.Account) + suite.NotNil(status.CreatedWithApplication) + suite.Nil(status.BoostOf) + suite.Nil(status.BoostOfAccount) + suite.Nil(status.InReplyTo) + suite.Nil(status.InReplyToAccount) +} + +func (suite *StatusTestSuite) TestGetStatusWithExtras() { + status, err := suite.db.GetStatusByID(suite.testStatuses["admin_account_status_1"].ID) + if err != nil { + suite.FailNow(err.Error()) + } + suite.NotNil(status) + suite.NotNil(status.Account) + suite.NotNil(status.CreatedWithApplication) + suite.NotEmpty(status.Tags) + suite.NotEmpty(status.Attachments) + suite.NotEmpty(status.Emojis) +} + +func (suite *StatusTestSuite) TestGetStatusWithMention() { + status, err := suite.db.GetStatusByID(suite.testStatuses["local_account_2_status_5"].ID) + if err != nil { + suite.FailNow(err.Error()) + } + suite.NotNil(status) + suite.NotNil(status.Account) + suite.NotNil(status.CreatedWithApplication) + suite.NotEmpty(status.Mentions) + suite.NotEmpty(status.MentionIDs) + suite.NotNil(status.InReplyTo) + suite.NotNil(status.InReplyToAccount) +} + +func (suite *StatusTestSuite) TestGetStatusTwice() { + before1 := time.Now() + _, err := suite.db.GetStatusByURI(suite.testStatuses["local_account_1_status_1"].URI) + suite.NoError(err) + after1 := time.Now() + duration1 := after1.Sub(before1) + fmt.Println(duration1.Nanoseconds()) + + before2 := time.Now() + _, err = suite.db.GetStatusByURI(suite.testStatuses["local_account_1_status_1"].URI) + suite.NoError(err) + after2 := time.Now() + duration2 := after2.Sub(before2) + fmt.Println(duration2.Nanoseconds()) + + // second retrieval should be several orders faster since it will be cached now + suite.Less(duration2, duration1) +} + +func TestStatusTestSuite(t *testing.T) { + suite.Run(t, new(StatusTestSuite)) +} diff --git a/internal/db/pg/statuscontext.go b/internal/db/pg/statuscontext.go deleted file mode 100644 index 2ff1a20bb..000000000 --- a/internal/db/pg/statuscontext.go +++ /dev/null @@ -1,104 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ - -package pg - -import ( - "container/list" - "errors" - - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func (ps *postgresService) StatusParents(status *gtsmodel.Status, onlyDirect bool) ([]*gtsmodel.Status, error) { - parents := []*gtsmodel.Status{} - ps.statusParent(status, &parents, onlyDirect) - - return parents, nil -} - -func (ps *postgresService) statusParent(status *gtsmodel.Status, foundStatuses *[]*gtsmodel.Status, onlyDirect bool) { - if status.InReplyToID == "" { - return - } - - parentStatus := >smodel.Status{} - if err := ps.conn.Model(parentStatus).Where("id = ?", status.InReplyToID).Select(); err == nil { - *foundStatuses = append(*foundStatuses, parentStatus) - } - - if onlyDirect { - return - } - ps.statusParent(parentStatus, foundStatuses, false) -} - -func (ps *postgresService) StatusChildren(status *gtsmodel.Status, onlyDirect bool, minID string) ([]*gtsmodel.Status, error) { - foundStatuses := &list.List{} - foundStatuses.PushFront(status) - ps.statusChildren(status, foundStatuses, onlyDirect, minID) - - children := []*gtsmodel.Status{} - for e := foundStatuses.Front(); e != nil; e = e.Next() { - entry, ok := e.Value.(*gtsmodel.Status) - if !ok { - panic(errors.New("entry in foundStatuses was not a *gtsmodel.Status")) - } - - // only append children, not the overall parent status - if entry.ID != status.ID { - children = append(children, entry) - } - } - - return children, nil -} - -func (ps *postgresService) statusChildren(status *gtsmodel.Status, foundStatuses *list.List, onlyDirect bool, minID string) { - immediateChildren := []*gtsmodel.Status{} - - q := ps.conn.Model(&immediateChildren).Where("in_reply_to_id = ?", status.ID) - if minID != "" { - q = q.Where("status.id > ?", minID) - } - - if err := q.Select(); err != nil { - return - } - - for _, child := range immediateChildren { - insertLoop: - for e := foundStatuses.Front(); e != nil; e = e.Next() { - entry, ok := e.Value.(*gtsmodel.Status) - if !ok { - panic(errors.New("entry in foundStatuses was not a *gtsmodel.Status")) - } - - if child.InReplyToAccountID != "" && entry.ID == child.InReplyToID { - foundStatuses.InsertAfter(child, e) - break insertLoop - } - } - - // only do one loop if we only want direct children - if onlyDirect { - return - } - ps.statusChildren(child, foundStatuses, false, minID) - } -} diff --git a/internal/db/pg/timeline.go b/internal/db/pg/timeline.go index 585ca3067..fa8b07aab 100644 --- a/internal/db/pg/timeline.go +++ b/internal/db/pg/timeline.go @@ -19,16 +19,26 @@ package pg import ( + "context" "sort" "github.com/go-pg/pg/v10" + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) -func (ps *postgresService) GetHomeTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error) { +type timelineDB struct { + config *config.Config + conn *pg.DB + log *logrus.Logger + cancel context.CancelFunc +} + +func (t *timelineDB) GetHomeTimeline(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, db.Error) { statuses := []*gtsmodel.Status{} - q := ps.conn.Model(&statuses) + q := t.conn.Model(&statuses) q = q.ColumnExpr("status.*"). // Find out who accountID follows. @@ -74,22 +84,22 @@ func (ps *postgresService) GetHomeTimelineForAccount(accountID string, maxID str err := q.Select() if err != nil { if err == pg.ErrNoRows { - return nil, db.ErrNoEntries{} + return nil, db.ErrNoEntries } return nil, err } if len(statuses) == 0 { - return nil, db.ErrNoEntries{} + return nil, db.ErrNoEntries } return statuses, nil } -func (ps *postgresService) GetPublicTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error) { +func (t *timelineDB) GetPublicTimeline(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, db.Error) { statuses := []*gtsmodel.Status{} - q := ps.conn.Model(&statuses). + q := t.conn.Model(&statuses). Where("visibility = ?", gtsmodel.VisibilityPublic). Where("? IS NULL", pg.Ident("in_reply_to_id")). Where("? IS NULL", pg.Ident("in_reply_to_uri")). @@ -119,13 +129,13 @@ func (ps *postgresService) GetPublicTimelineForAccount(accountID string, maxID s err := q.Select() if err != nil { if err == pg.ErrNoRows { - return nil, db.ErrNoEntries{} + return nil, db.ErrNoEntries } return nil, err } if len(statuses) == 0 { - return nil, db.ErrNoEntries{} + return nil, db.ErrNoEntries } return statuses, nil @@ -133,11 +143,11 @@ func (ps *postgresService) GetPublicTimelineForAccount(accountID string, maxID s // TODO optimize this query and the logic here, because it's slow as balls -- it takes like a literal second to return with a limit of 20! // It might be worth serving it through a timeline instead of raw DB queries, like we do for Home feeds. -func (ps *postgresService) GetFavedTimelineForAccount(accountID string, maxID string, minID string, limit int) ([]*gtsmodel.Status, string, string, error) { +func (t *timelineDB) GetFavedTimeline(accountID string, maxID string, minID string, limit int) ([]*gtsmodel.Status, string, string, db.Error) { faves := []*gtsmodel.StatusFave{} - fq := ps.conn.Model(&faves). + fq := t.conn.Model(&faves). Where("account_id = ?", accountID). Order("id DESC") @@ -156,13 +166,13 @@ func (ps *postgresService) GetFavedTimelineForAccount(accountID string, maxID st err := fq.Select() if err != nil { if err == pg.ErrNoRows { - return nil, "", "", db.ErrNoEntries{} + return nil, "", "", db.ErrNoEntries } return nil, "", "", err } if len(faves) == 0 { - return nil, "", "", db.ErrNoEntries{} + return nil, "", "", db.ErrNoEntries } // map[statusID]faveID -- we need this to sort statuses by fave ID rather than their own ID @@ -175,16 +185,16 @@ func (ps *postgresService) GetFavedTimelineForAccount(accountID string, maxID st } statuses := []*gtsmodel.Status{} - err = ps.conn.Model(&statuses).Where("id IN (?)", pg.In(in)).Select() + err = t.conn.Model(&statuses).Where("id IN (?)", pg.In(in)).Select() if err != nil { if err == pg.ErrNoRows { - return nil, "", "", db.ErrNoEntries{} + return nil, "", "", db.ErrNoEntries } return nil, "", "", err } if len(statuses) == 0 { - return nil, "", "", db.ErrNoEntries{} + return nil, "", "", db.ErrNoEntries } // arrange statuses by fave ID diff --git a/internal/db/pg/update.go b/internal/db/pg/update.go deleted file mode 100644 index f6bc70ad9..000000000 --- a/internal/db/pg/update.go +++ /dev/null @@ -1,73 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ - -package pg - -import ( - "fmt" - - "github.com/go-pg/pg/v10" - "github.com/superseriousbusiness/gotosocial/internal/db" -) - -func (ps *postgresService) Upsert(i interface{}, conflictColumn string) error { - if _, err := ps.conn.Model(i).OnConflict(fmt.Sprintf("(%s) DO UPDATE", conflictColumn)).Insert(); err != nil { - if err == pg.ErrNoRows { - return db.ErrNoEntries{} - } - return err - } - return nil -} - -func (ps *postgresService) UpdateByID(id string, i interface{}) error { - if _, err := ps.conn.Model(i).Where("id = ?", id).OnConflict("(id) DO UPDATE").Insert(); err != nil { - if err == pg.ErrNoRows { - return db.ErrNoEntries{} - } - return err - } - return nil -} - -func (ps *postgresService) UpdateOneByID(id string, key string, value interface{}, i interface{}) error { - _, err := ps.conn.Model(i).Set("? = ?", pg.Safe(key), value).Where("id = ?", id).Update() - return err -} - -func (ps *postgresService) UpdateWhere(where []db.Where, key string, value interface{}, i interface{}) error { - q := ps.conn.Model(i) - - for _, w := range where { - if w.Value == nil { - q = q.Where("? IS NULL", pg.Ident(w.Key)) - } else { - if w.CaseInsensitive { - q = q.Where("LOWER(?) = LOWER(?)", pg.Safe(w.Key), w.Value) - } else { - q = q.Where("? = ?", pg.Safe(w.Key), w.Value) - } - } - } - - q = q.Set("? = ?", pg.Safe(key), value) - - _, err := q.Update() - - return err -} diff --git a/internal/db/pg/util.go b/internal/db/pg/util.go new file mode 100644 index 000000000..17c09b720 --- /dev/null +++ b/internal/db/pg/util.go @@ -0,0 +1,25 @@ +package pg + +import ( + "strings" + + "github.com/go-pg/pg/v10" + "github.com/superseriousbusiness/gotosocial/internal/db" +) + +// processErrorResponse parses the given error and returns an appropriate DBError. +func processErrorResponse(err error) db.Error { + switch err { + case nil: + return nil + case pg.ErrNoRows: + return db.ErrNoEntries + case pg.ErrMultiRows: + return db.ErrMultipleEntries + default: + if strings.Contains(err.Error(), "duplicate key value violates unique constraint") { + return db.ErrAlreadyExists + } + return err + } +} diff --git a/internal/db/relationship.go b/internal/db/relationship.go new file mode 100644 index 000000000..85f64d72b --- /dev/null +++ b/internal/db/relationship.go @@ -0,0 +1,71 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package db + +import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + +// Relationship contains functions for getting or modifying the relationship between two accounts. +type Relationship interface { + // IsBlocked checks whether account 1 has a block in place against block2. + // If eitherDirection is true, then the function returns true if account1 blocks account2, OR if account2 blocks account1. + IsBlocked(account1 string, account2 string, eitherDirection bool) (bool, Error) + + // GetBlock returns the block from account1 targeting account2, if it exists, or an error if it doesn't. + // + // Because this is slower than Blocked, only use it if you need the actual Block struct for some reason, + // not if you're just checking for the existence of a block. + GetBlock(account1 string, account2 string) (*gtsmodel.Block, Error) + + // GetRelationship retrieves the relationship of the targetAccount to the requestingAccount. + GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, Error) + + // IsFollowing returns true if sourceAccount follows target account, or an error if something goes wrong while finding out. + IsFollowing(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, Error) + + // IsFollowRequested returns true if sourceAccount has requested to follow target account, or an error if something goes wrong while finding out. + IsFollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, Error) + + // IsMutualFollowing returns true if account1 and account2 both follow each other, or an error if something goes wrong while finding out. + IsMutualFollowing(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, Error) + + // AcceptFollowRequest moves a follow request in the database from the follow_requests table to the follows table. + // In other words, it should create the follow, and delete the existing follow request. + // + // It will return the newly created follow for further processing. + AcceptFollowRequest(originAccountID string, targetAccountID string) (*gtsmodel.Follow, Error) + + // GetAccountFollowRequests returns all follow requests targeting the given account. + GetAccountFollowRequests(accountID string) ([]*gtsmodel.FollowRequest, Error) + + // GetAccountFollows returns a slice of follows owned by the given accountID. + GetAccountFollows(accountID string) ([]*gtsmodel.Follow, Error) + + // CountAccountFollows returns the amount of accounts that the given accountID is following. + // + // If localOnly is set to true, then only follows from *this instance* will be returned. + CountAccountFollows(accountID string, localOnly bool) (int, Error) + + // GetAccountFollowedBy fetches follows that target given accountID. + // + // If localOnly is set to true, then only follows from *this instance* will be returned. + GetAccountFollowedBy(accountID string, localOnly bool) ([]*gtsmodel.Follow, Error) + + // CountAccountFollowedBy returns the amounts that the given ID is followed by. + CountAccountFollowedBy(accountID string, localOnly bool) (int, Error) +} diff --git a/internal/db/status.go b/internal/db/status.go new file mode 100644 index 000000000..9d206c198 --- /dev/null +++ b/internal/db/status.go @@ -0,0 +1,75 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package db + +import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + +// Status contains functions for getting statuses, creating statuses, and checking various other fields on statuses. +type Status interface { + // GetStatusByID returns one status from the database, with all rel fields populated (if possible). + GetStatusByID(id string) (*gtsmodel.Status, Error) + + // GetStatusByURI returns one status from the database, with all rel fields populated (if possible). + GetStatusByURI(uri string) (*gtsmodel.Status, Error) + + // GetStatusByURL returns one status from the database, with all rel fields populated (if possible). + GetStatusByURL(uri string) (*gtsmodel.Status, Error) + + // PutStatus stores one status in the database. + PutStatus(status *gtsmodel.Status) Error + + // CountStatusReplies returns the amount of replies recorded for a status, or an error if something goes wrong + CountStatusReplies(status *gtsmodel.Status) (int, Error) + + // CountStatusReblogs returns the amount of reblogs/boosts recorded for a status, or an error if something goes wrong + CountStatusReblogs(status *gtsmodel.Status) (int, Error) + + // CountStatusFaves returns the amount of faves/likes recorded for a status, or an error if something goes wrong + CountStatusFaves(status *gtsmodel.Status) (int, Error) + + // GetStatusParents gets the parent statuses of a given status. + // + // If onlyDirect is true, only the immediate parent will be returned. + GetStatusParents(status *gtsmodel.Status, onlyDirect bool) ([]*gtsmodel.Status, Error) + + // GetStatusChildren gets the child statuses of a given status. + // + // If onlyDirect is true, only the immediate children will be returned. + GetStatusChildren(status *gtsmodel.Status, onlyDirect bool, minID string) ([]*gtsmodel.Status, Error) + + // IsStatusFavedBy checks if a given status has been faved by a given account ID + IsStatusFavedBy(status *gtsmodel.Status, accountID string) (bool, Error) + + // IsStatusRebloggedBy checks if a given status has been reblogged/boosted by a given account ID + IsStatusRebloggedBy(status *gtsmodel.Status, accountID string) (bool, Error) + + // IsStatusMutedBy checks if a given status has been muted by a given account ID + IsStatusMutedBy(status *gtsmodel.Status, accountID string) (bool, Error) + + // IsStatusBookmarkedBy checks if a given status has been bookmarked by a given account ID + IsStatusBookmarkedBy(status *gtsmodel.Status, accountID string) (bool, Error) + + // GetStatusFaves returns a slice of faves/likes of the given status. + // This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user. + GetStatusFaves(status *gtsmodel.Status) ([]*gtsmodel.StatusFave, Error) + + // GetStatusReblogs returns a slice of statuses that are a boost/reblog of the given status. + // This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user. + GetStatusReblogs(status *gtsmodel.Status) ([]*gtsmodel.Status, Error) +} diff --git a/internal/db/timeline.go b/internal/db/timeline.go new file mode 100644 index 000000000..74aa5c781 --- /dev/null +++ b/internal/db/timeline.go @@ -0,0 +1,44 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package db + +import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + +// Timeline contains functionality for retrieving home/public/faved etc timelines for an account. +type Timeline interface { + // GetHomeTimeline returns a slice of statuses from accounts that are followed by the given account id. + // + // Statuses should be returned in descending order of when they were created (newest first). + GetHomeTimeline(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, Error) + + // GetPublicTimeline fetches the account's PUBLIC timeline -- ie., posts and replies that are public. + // It will use the given filters and try to return as many statuses as possible up to the limit. + // + // Statuses should be returned in descending order of when they were created (newest first). + GetPublicTimeline(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, Error) + + // GetFavedTimeline fetches the account's FAVED timeline -- ie., posts and replies that the requesting account has faved. + // It will use the given filters and try to return as many statuses as possible up to the limit. + // + // Note that unlike the other GetTimeline functions, the returned statuses will be arranged by their FAVE id, not the STATUS id. + // In other words, they'll be returned in descending order of when they were faved by the requesting user, not when they were created. + // + // Also note the extra return values, which correspond to the nextMaxID and prevMinID for building Link headers. + GetFavedTimeline(accountID string, maxID string, minID string, limit int) ([]*gtsmodel.Status, string, string, Error) +} diff --git a/internal/federation/dereference.go b/internal/federation/dereference.go index 07901d5b1..96a662e32 100644 --- a/internal/federation/dereference.go +++ b/internal/federation/dereference.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package federation import ( diff --git a/internal/federation/dereferencing/account.go b/internal/federation/dereferencing/account.go index 72d2e44d7..ba6766061 100644 --- a/internal/federation/dereferencing/account.go +++ b/internal/federation/dereferencing/account.go @@ -29,7 +29,6 @@ import ( "github.com/go-fed/activity/streams/vocab" "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/ap" - "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/transport" @@ -65,8 +64,8 @@ func (d *deref) GetRemoteAccount(username string, remoteAccountID *url.URL, refr new := true // check if we already have the account in our db - maybeAccount := >smodel.Account{} - if err := d.db.GetWhere([]db.Where{{Key: "uri", Value: remoteAccountID.String()}}, maybeAccount); err == nil { + maybeAccount, err := d.db.GetAccountByURI(remoteAccountID.String()) + if err == nil { // we've seen this account before so it's not new new = false if !refresh { diff --git a/internal/federation/dereferencing/announce.go b/internal/federation/dereferencing/announce.go index 2522a4034..6773db425 100644 --- a/internal/federation/dereferencing/announce.go +++ b/internal/federation/dereferencing/announce.go @@ -27,14 +27,14 @@ import ( ) func (d *deref) DereferenceAnnounce(announce *gtsmodel.Status, requestingUsername string) error { - if announce.GTSBoostedStatus == nil || announce.GTSBoostedStatus.URI == "" { + if announce.BoostOf == nil || announce.BoostOf.URI == "" { // we can't do anything unfortunately return errors.New("DereferenceAnnounce: no URI to dereference") } - boostedStatusURI, err := url.Parse(announce.GTSBoostedStatus.URI) + boostedStatusURI, err := url.Parse(announce.BoostOf.URI) if err != nil { - return fmt.Errorf("DereferenceAnnounce: couldn't parse boosted status URI %s: %s", announce.GTSBoostedStatus.URI, err) + return fmt.Errorf("DereferenceAnnounce: couldn't parse boosted status URI %s: %s", announce.BoostOf.URI, err) } if blocked, err := d.blockedDomain(boostedStatusURI.Host); blocked || err != nil { return fmt.Errorf("DereferenceAnnounce: domain %s is blocked", boostedStatusURI.Host) @@ -47,7 +47,7 @@ func (d *deref) DereferenceAnnounce(announce *gtsmodel.Status, requestingUsernam boostedStatus, _, _, err := d.GetRemoteStatus(requestingUsername, boostedStatusURI, false) if err != nil { - return fmt.Errorf("DereferenceAnnounce: error dereferencing remote status with id %s: %s", announce.GTSBoostedStatus.URI, err) + return fmt.Errorf("DereferenceAnnounce: error dereferencing remote status with id %s: %s", announce.BoostOf.URI, err) } announce.Content = boostedStatus.Content @@ -60,6 +60,6 @@ func (d *deref) DereferenceAnnounce(announce *gtsmodel.Status, requestingUsernam announce.BoostOfAccountID = boostedStatus.AccountID announce.Visibility = boostedStatus.Visibility announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced - announce.GTSBoostedStatus = boostedStatus + announce.BoostOf = boostedStatus return nil } diff --git a/internal/federation/dereferencing/blocked.go b/internal/federation/dereferencing/blocked.go index a66afbb60..c8a4c6ade 100644 --- a/internal/federation/dereferencing/blocked.go +++ b/internal/federation/dereferencing/blocked.go @@ -31,7 +31,7 @@ func (d *deref) blockedDomain(host string) (bool, error) { return true, nil } - if _, ok := err.(db.ErrNoEntries); ok { + if err == db.ErrNoEntries { // there are no entries so there's no block return false, nil } diff --git a/internal/federation/dereferencing/status.go b/internal/federation/dereferencing/status.go index b05f6e72c..68693c021 100644 --- a/internal/federation/dereferencing/status.go +++ b/internal/federation/dereferencing/status.go @@ -66,8 +66,8 @@ func (d *deref) GetRemoteStatus(username string, remoteStatusID *url.URL, refres new := true // check if we already have the status in our db - maybeStatus := >smodel.Status{} - if err := d.db.GetWhere([]db.Where{{Key: "uri", Value: remoteStatusID.String()}}, maybeStatus); err == nil { + maybeStatus, err := d.db.GetStatusByURI(remoteStatusID.String()) + if err == nil { // we've seen this status before so it's not new new = false @@ -109,7 +109,7 @@ func (d *deref) GetRemoteStatus(username string, remoteStatusID *url.URL, refres return nil, statusable, new, fmt.Errorf("GetRemoteStatus: error populating status fields: %s", err) } - if err := d.db.Put(gtsStatus); err != nil { + if err := d.db.PutStatus(gtsStatus); err != nil { return nil, statusable, new, fmt.Errorf("GetRemoteStatus: error putting new status: %s", err) } } else { @@ -276,7 +276,7 @@ func (d *deref) populateStatusFields(status *gtsmodel.Status, requestingUsername // * the remote URL (a.RemoteURL) // This should be enough to pass along to the media processor. attachmentIDs := []string{} - for _, a := range status.GTSMediaAttachments { + for _, a := range status.Attachments { l.Tracef("dereferencing attachment: %+v", a) // it might have been processed elsewhere so check first if it's already in the database or not @@ -288,7 +288,7 @@ func (d *deref) populateStatusFields(status *gtsmodel.Status, requestingUsername attachmentIDs = append(attachmentIDs, maybeAttachment.ID) continue } - if _, ok := err.(db.ErrNoEntries); !ok { + if err != db.ErrNoEntries { // we have a real error return fmt.Errorf("error checking db for existence of attachment with remote url %s: %s", a.RemoteURL, err) } @@ -307,7 +307,7 @@ func (d *deref) populateStatusFields(status *gtsmodel.Status, requestingUsername } attachmentIDs = append(attachmentIDs, deferencedAttachment.ID) } - status.Attachments = attachmentIDs + status.AttachmentIDs = attachmentIDs // 2. Hashtags @@ -317,53 +317,84 @@ func (d *deref) populateStatusFields(status *gtsmodel.Status, requestingUsername // 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 { - + mentionIDs := []string{} + for _, m := range status.Mentions { if m.ID != "" { - continue // we've already populated this mention, since it has an ID + l.Debug("mention already populated") + continue + } + + if m.TargetAccountURI == "" { + // can't do anything with this mention + l.Debug("target URI not set on mention") + continue + } + + targetAccountURI, err := url.Parse(m.TargetAccountURI) + if err != nil { + l.Debugf("error parsing mentioned account uri %s: %s", m.TargetAccountURI, err) + continue + } + + var targetAccount *gtsmodel.Account + if a, err := d.db.GetAccountByURL(targetAccountURI.String()); err == nil { + targetAccount = a + } else if a, _, err := d.GetRemoteAccount(requestingUsername, targetAccountURI, false); err == nil { + targetAccount = a + } else { + // we can't find the target account so bail + l.Debug("can't retrieve account targeted by mention") + continue } mID, err := id.NewRandomULID() if err != nil { return err } - m.ID = mID - uri, err := url.Parse(m.MentionedAccountURI) - if err != nil { - l.Debugf("error parsing mentioned account uri %s: %s", m.MentionedAccountURI, err) - continue + m = >smodel.Mention{ + ID: mID, + StatusID: status.ID, + Status: m.Status, + CreatedAt: status.CreatedAt, + UpdatedAt: status.UpdatedAt, + OriginAccountID: status.Account.ID, + OriginAccountURI: status.AccountURI, + OriginAccount: status.Account, + TargetAccountID: targetAccount.ID, + TargetAccount: targetAccount, + NameString: m.NameString, + TargetAccountURI: targetAccount.URI, + TargetAccountURL: targetAccount.URL, } - m.StatusID = status.ID - m.OriginAccountID = status.GTSAuthorAccount.ID - m.OriginAccountURI = status.GTSAuthorAccount.URI - - targetAccount, _, err := d.GetRemoteAccount(requestingUsername, uri, false) - if err != nil { - continue - } - - // by this point, we know the targetAccount exists in our database with an ID :) - m.TargetAccountID = targetAccount.ID if err := d.db.Put(m); err != nil { return fmt.Errorf("error creating mention: %s", err) } - mentions = append(mentions, m.ID) + mentionIDs = append(mentionIDs, m.ID) } - status.Mentions = mentions + status.MentionIDs = mentionIDs // status has replyToURI but we don't have an ID yet for the status it replies to if status.InReplyToURI != "" && status.InReplyToID == "" { - replyToStatus := >smodel.Status{} - if err := d.db.GetWhere([]db.Where{{Key: "uri", Value: status.InReplyToURI}}, replyToStatus); err == nil { + statusURI, err := url.Parse(status.InReplyToURI) + if err != nil { + return err + } + if replyToStatus, err := d.db.GetStatusByURI(status.InReplyToURI); err == nil { // we have the status status.InReplyToID = replyToStatus.ID + status.InReplyTo = replyToStatus status.InReplyToAccountID = replyToStatus.AccountID + status.InReplyToAccount = replyToStatus.Account + } else if replyToStatus, _, _, err := d.GetRemoteStatus(requestingUsername, statusURI, false); err == nil { + // we got the status + status.InReplyToID = replyToStatus.ID + status.InReplyTo = replyToStatus + status.InReplyToAccountID = replyToStatus.AccountID + status.InReplyToAccount = replyToStatus.Account } } - return nil } diff --git a/internal/federation/federatingdb/accept.go b/internal/federation/federatingdb/accept.go index 4d11ea62a..91d9df86f 100644 --- a/internal/federation/federatingdb/accept.go +++ b/internal/federation/federatingdb/accept.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package federatingdb import ( diff --git a/internal/federation/federatingdb/announce.go b/internal/federation/federatingdb/announce.go index a70c0c3a6..981eaf1ef 100644 --- a/internal/federation/federatingdb/announce.go +++ b/internal/federation/federatingdb/announce.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package federatingdb import ( diff --git a/internal/federation/federatingdb/create.go b/internal/federation/federatingdb/create.go index 2ac4890e8..fb4353cd4 100644 --- a/internal/federation/federatingdb/create.go +++ b/internal/federation/federatingdb/create.go @@ -112,8 +112,8 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error { } status.ID = statusID - if err := f.db.Put(status); err != nil { - if _, ok := err.(db.ErrAlreadyExists); ok { + if err := f.db.PutStatus(status); err != nil { + if err == db.ErrAlreadyExists { // the status already exists in the database, which means we've already handled everything else, // so we can just return nil here and be done with it. return nil diff --git a/internal/federation/federatingdb/delete.go b/internal/federation/federatingdb/delete.go index 02ce43620..ee9310789 100644 --- a/internal/federation/federatingdb/delete.go +++ b/internal/federation/federatingdb/delete.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package federatingdb import ( @@ -6,7 +24,6 @@ import ( "net/url" "github.com/sirupsen/logrus" - "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -52,10 +69,8 @@ func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error { // in a delete we only get the URI, we can't know if we have a status or a profile or something else, // so we have to try a few different things... - where := []db.Where{{Key: "uri", Value: id.String()}} - - s := >smodel.Status{} - if err := f.db.GetWhere(where, s); err == nil { + s, err := f.db.GetStatusByURI(id.String()) + if err == nil { // it's a status l.Debugf("uri is for status with id: %s", s.ID) if err := f.db.DeleteByID(s.ID, >smodel.Status{}); err != nil { @@ -69,8 +84,8 @@ func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error { } } - a := >smodel.Account{} - if err := f.db.GetWhere(where, a); err == nil { + a, err := f.db.GetAccountByURI(id.String()) + if err == nil { // it's an account l.Debugf("uri is for an account with id: %s", s.ID) if err := f.db.DeleteByID(a.ID, >smodel.Account{}); err != nil { diff --git a/internal/federation/federatingdb/exists.go b/internal/federation/federatingdb/exists.go index b5c10b895..0e13c1196 100644 --- a/internal/federation/federatingdb/exists.go +++ b/internal/federation/federatingdb/exists.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package federatingdb import ( diff --git a/internal/federation/federatingdb/followers.go b/internal/federation/federatingdb/followers.go index 1f111dd34..241362fc1 100644 --- a/internal/federation/federatingdb/followers.go +++ b/internal/federation/federatingdb/followers.go @@ -31,7 +31,8 @@ func (f *federatingDB) Followers(c context.Context, actorIRI *url.URL) (follower acct := >smodel.Account{} if util.IsUserPath(actorIRI) { - if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: actorIRI.String()}}, acct); err != nil { + acct, err = f.db.GetAccountByURI(actorIRI.String()) + if err != nil { return nil, fmt.Errorf("FOLLOWERS: db error getting account with uri %s: %s", actorIRI.String(), err) } } else if util.IsFollowersPath(actorIRI) { @@ -42,8 +43,8 @@ func (f *federatingDB) Followers(c context.Context, actorIRI *url.URL) (follower return nil, fmt.Errorf("FOLLOWERS: could not parse actor IRI %s as users or followers path", actorIRI.String()) } - acctFollowers := []gtsmodel.Follow{} - if err := f.db.GetFollowersByAccountID(acct.ID, &acctFollowers, false); err != nil { + acctFollowers, err := f.db.GetAccountFollowedBy(acct.ID, false) + if err != nil { return nil, fmt.Errorf("FOLLOWERS: db error getting followers for account id %s: %s", acct.ID, err) } diff --git a/internal/federation/federatingdb/following.go b/internal/federation/federatingdb/following.go index f92041e1e..45785c671 100644 --- a/internal/federation/federatingdb/following.go +++ b/internal/federation/federatingdb/following.go @@ -8,7 +8,6 @@ import ( "github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams/vocab" "github.com/sirupsen/logrus" - "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -28,21 +27,37 @@ func (f *federatingDB) Following(c context.Context, actorIRI *url.URL) (followin ) l.Debugf("entering FOLLOWING function with actorIRI %s", actorIRI.String()) - acct := >smodel.Account{} + var acct *gtsmodel.Account if util.IsUserPath(actorIRI) { - if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: actorIRI.String()}}, acct); err != nil { + username, err := util.ParseUserPath(actorIRI) + if err != nil { + return nil, fmt.Errorf("FOLLOWING: error parsing user path: %s", err) + } + + a, err := f.db.GetLocalAccountByUsername(username) + if err != nil { return nil, fmt.Errorf("FOLLOWING: db error getting account with uri %s: %s", actorIRI.String(), err) } + + acct = a } else if util.IsFollowingPath(actorIRI) { - if err := f.db.GetWhere([]db.Where{{Key: "following_uri", Value: actorIRI.String()}}, acct); err != nil { + username, err := util.ParseFollowingPath(actorIRI) + if err != nil { + return nil, fmt.Errorf("FOLLOWING: error parsing following path: %s", err) + } + + a, err := f.db.GetLocalAccountByUsername(username) + if err != nil { return nil, fmt.Errorf("FOLLOWING: db error getting account with following uri %s: %s", actorIRI.String(), err) } + + acct = a } else { return nil, fmt.Errorf("FOLLOWING: could not parse actor IRI %s as users or following path", actorIRI.String()) } - acctFollowing := []gtsmodel.Follow{} - if err := f.db.GetFollowingByAccountID(acct.ID, &acctFollowing); err != nil { + acctFollowing, err := f.db.GetAccountFollows(acct.ID) + if err != nil { return nil, fmt.Errorf("FOLLOWING: db error getting following for account id %s: %s", acct.ID, err) } diff --git a/internal/federation/federatingdb/get.go b/internal/federation/federatingdb/get.go index 77a24bf43..0265080f9 100644 --- a/internal/federation/federatingdb/get.go +++ b/internal/federation/federatingdb/get.go @@ -43,8 +43,8 @@ func (f *federatingDB) Get(c context.Context, id *url.URL) (value vocab.Type, er l.Debug("entering GET function") if util.IsUserPath(id) { - acct := >smodel.Account{} - if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: id.String()}}, acct); err != nil { + acct, err := f.db.GetAccountByURI(id.String()) + if err != nil { return nil, err } l.Debug("is user path! returning account") diff --git a/internal/federation/federatingdb/inbox.go b/internal/federation/federatingdb/inbox.go index 25ef2cda5..4390a8b4b 100644 --- a/internal/federation/federatingdb/inbox.go +++ b/internal/federation/federatingdb/inbox.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package federatingdb import ( diff --git a/internal/federation/federatingdb/liked.go b/internal/federation/federatingdb/liked.go index 5645d6f1e..b85398fef 100644 --- a/internal/federation/federatingdb/liked.go +++ b/internal/federation/federatingdb/liked.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package federatingdb import ( diff --git a/internal/federation/federatingdb/outbox.go b/internal/federation/federatingdb/outbox.go index 1568e0017..849014432 100644 --- a/internal/federation/federatingdb/outbox.go +++ b/internal/federation/federatingdb/outbox.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package federatingdb import ( @@ -62,7 +80,7 @@ func (f *federatingDB) OutboxForInbox(c context.Context, inboxIRI *url.URL) (out } acct := >smodel.Account{} if err := f.db.GetWhere([]db.Where{{Key: "inbox_uri", Value: inboxIRI.String()}}, acct); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + if err == db.ErrNoEntries { return nil, fmt.Errorf("no actor found that corresponds to inbox %s", inboxIRI.String()) } return nil, fmt.Errorf("db error searching for actor with inbox %s", inboxIRI.String()) diff --git a/internal/federation/federatingdb/owns.go b/internal/federation/federatingdb/owns.go index 51b20151a..0a65397ff 100644 --- a/internal/federation/federatingdb/owns.go +++ b/internal/federation/federatingdb/owns.go @@ -54,16 +54,16 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) { if err != nil { return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err) } - if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uid}}, >smodel.Status{}); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + status, err := f.db.GetStatusByURI(uid) + if err != nil { + if err == db.ErrNoEntries { // there are no entries for this status return false, nil } // an actual error happened return false, fmt.Errorf("database error fetching status with id %s: %s", uid, err) } - l.Debugf("we own url %s", id.String()) - return true, nil + return status.Local, nil } if util.IsUserPath(id) { @@ -71,8 +71,8 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) { if err != nil { return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err) } - if err := f.db.GetLocalAccountByUsername(username, >smodel.Account{}); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + if _, err := f.db.GetLocalAccountByUsername(username); err != nil { + if err == db.ErrNoEntries { // there are no entries for this username return false, nil } @@ -88,8 +88,8 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) { if err != nil { return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err) } - if err := f.db.GetLocalAccountByUsername(username, >smodel.Account{}); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + if _, err := f.db.GetLocalAccountByUsername(username); err != nil { + if err == db.ErrNoEntries { // there are no entries for this username return false, nil } @@ -105,8 +105,8 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) { if err != nil { return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err) } - if err := f.db.GetLocalAccountByUsername(username, >smodel.Account{}); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + if _, err := f.db.GetLocalAccountByUsername(username); err != nil { + if err == db.ErrNoEntries { // there are no entries for this username return false, nil } @@ -122,8 +122,8 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) { if err != nil { return false, fmt.Errorf("error parsing like path for url %s: %s", id.String(), err) } - if err := f.db.GetLocalAccountByUsername(username, >smodel.Account{}); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + if _, err := f.db.GetLocalAccountByUsername(username); err != nil { + if err == db.ErrNoEntries { // there are no entries for this username return false, nil } @@ -131,7 +131,7 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) { return false, fmt.Errorf("database error fetching account with username %s: %s", username, err) } if err := f.db.GetByID(likeID, >smodel.StatusFave{}); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + if err == db.ErrNoEntries { // there are no entries return false, nil } @@ -147,8 +147,8 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) { if err != nil { return false, fmt.Errorf("error parsing block path for url %s: %s", id.String(), err) } - if err := f.db.GetLocalAccountByUsername(username, >smodel.Account{}); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + if _, err := f.db.GetLocalAccountByUsername(username); err != nil { + if err == db.ErrNoEntries { // there are no entries for this username return false, nil } @@ -156,7 +156,7 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) { return false, fmt.Errorf("database error fetching account with username %s: %s", username, err) } if err := f.db.GetByID(blockID, >smodel.Block{}); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + if err == db.ErrNoEntries { // there are no entries return false, nil } diff --git a/internal/federation/federatingdb/undo.go b/internal/federation/federatingdb/undo.go index dd82e7bac..c527833b4 100644 --- a/internal/federation/federatingdb/undo.go +++ b/internal/federation/federatingdb/undo.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package federatingdb import ( diff --git a/internal/federation/federatingdb/update.go b/internal/federation/federatingdb/update.go index 3f4e3e413..88ffc23b4 100644 --- a/internal/federation/federatingdb/update.go +++ b/internal/federation/federatingdb/update.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package federatingdb import ( diff --git a/internal/federation/federatingdb/util.go b/internal/federation/federatingdb/util.go index 28f4c5a21..eac70d85c 100644 --- a/internal/federation/federatingdb/util.go +++ b/internal/federation/federatingdb/util.go @@ -97,8 +97,8 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (idURL *url.URL, e for iter := actorProp.Begin(); iter != actorProp.End(); iter = iter.Next() { // take the IRI of the first actor we can find (there should only be one) if iter.IsIRI() { - actorAccount := >smodel.Account{} - if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: iter.GetIRI().String()}}, actorAccount); err == nil { // if there's an error here, just use the fallback behavior -- we don't need to return an error here + // if there's an error here, just use the fallback behavior -- we don't need to return an error here + if actorAccount, err := f.db.GetAccountByURI(iter.GetIRI().String()); err == nil { newID, err := id.NewRandomULID() if err != nil { return nil, err @@ -213,7 +213,7 @@ func (f *federatingDB) ActorForOutbox(c context.Context, outboxIRI *url.URL) (ac } acct := >smodel.Account{} if err := f.db.GetWhere([]db.Where{{Key: "outbox_uri", Value: outboxIRI.String()}}, acct); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + if err == db.ErrNoEntries { return nil, fmt.Errorf("no actor found that corresponds to outbox %s", outboxIRI.String()) } return nil, fmt.Errorf("db error searching for actor with outbox %s", outboxIRI.String()) @@ -238,7 +238,7 @@ func (f *federatingDB) ActorForInbox(c context.Context, inboxIRI *url.URL) (acto } acct := >smodel.Account{} if err := f.db.GetWhere([]db.Where{{Key: "inbox_uri", Value: inboxIRI.String()}}, acct); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + if err == db.ErrNoEntries { return nil, fmt.Errorf("no actor found that corresponds to inbox %s", inboxIRI.String()) } return nil, fmt.Errorf("db error searching for actor with inbox %s", inboxIRI.String()) diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go index 9e21b43bf..5da68afd3 100644 --- a/internal/federation/federatingprotocol.go +++ b/internal/federation/federatingprotocol.go @@ -113,8 +113,8 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr return nil, false, errors.New("username was empty") } - requestedAccount := >smodel.Account{} - if err := f.db.GetLocalAccountByUsername(username, requestedAccount); err != nil { + requestedAccount, err := f.db.GetLocalAccountByUsername(username) + if err != nil { return nil, false, fmt.Errorf("could not fetch requested account with username %s: %s", username, err) } @@ -132,7 +132,7 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr // authentication has passed, so add an instance entry for this instance if it hasn't been done already i := >smodel.Instance{} if err := f.db.GetWhere([]db.Where{{Key: "domain", Value: publicKeyOwnerURI.Host, CaseInsensitive: true}}, i); err != nil { - if _, ok := err.(db.ErrNoEntries); !ok { + if err != db.ErrNoEntries { // there's been an actual error return ctx, false, fmt.Errorf("error getting requesting account with public key id %s: %s", publicKeyOwnerURI.String(), err) } @@ -176,8 +176,6 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr // Finally, if the authentication and authorization succeeds, then // blocked must be false and error nil. The request will continue // to be processed. -// -// TODO: implement domain block checking here as well func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, error) { l := f.log.WithFields(logrus.Fields{ "func": "Blocked", @@ -191,19 +189,18 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er return false, errors.New("requested account not set on request context, so couldn't determine blocks") } - for _, uri := range actorIRIs { - blockedDomain, err := f.blockedDomain(uri.Host) - if err != nil { - return false, fmt.Errorf("error checking domain block: %s", err) - } - if blockedDomain { - return true, nil - } + blocked, err := f.db.AreURIsBlocked(actorIRIs) + if err != nil { + return false, fmt.Errorf("error checking domain blocks: %s", err) + } + if blocked { + return blocked, nil + } - requestingAccount := >smodel.Account{} - if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, requestingAccount); err != nil { - _, ok := err.(db.ErrNoEntries) - if ok { + for _, uri := range actorIRIs { + requestingAccount, err := f.db.GetAccountByURI(uri.String()) + if err != nil { + if err == db.ErrNoEntries { // we don't have an entry for this account so it's not blocked // TODO: allow a different default to be set for this behavior continue @@ -211,12 +208,11 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er return false, fmt.Errorf("error getting account with uri %s: %s", uri.String(), err) } - // check if requested account blocks requesting account - if err := f.db.GetWhere([]db.Where{ - {Key: "account_id", Value: requestedAccount.ID}, - {Key: "target_account_id", Value: requestingAccount.ID}, - }, >smodel.Block{}); err == nil { - // a block exists + blocked, err = f.db.IsBlocked(requestedAccount.ID, requestingAccount.ID, true) + if err != nil { + return false, fmt.Errorf("error checking account block: %s", err) + } + if blocked { return true, nil } } diff --git a/internal/federation/finger.go b/internal/federation/finger.go index 0ffc60e5a..a5a4fa0e7 100644 --- a/internal/federation/finger.go +++ b/internal/federation/finger.go @@ -30,7 +30,7 @@ import ( ) func (f *federator) FingerRemoteAccount(requestingUsername string, targetUsername string, targetDomain string) (*url.URL, error) { - if blocked, err := f.blockedDomain(targetDomain); blocked || err != nil { + if blocked, err := f.db.IsDomainBlocked(targetDomain); blocked || err != nil { return nil, fmt.Errorf("FingerRemoteAccount: domain %s is blocked", targetDomain) } diff --git a/internal/federation/handshake.go b/internal/federation/handshake.go index 47c8a6c84..0671e78a9 100644 --- a/internal/federation/handshake.go +++ b/internal/federation/handshake.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package federation import "net/url" diff --git a/internal/federation/transport.go b/internal/federation/transport.go index ed28749a1..20aee964b 100644 --- a/internal/federation/transport.go +++ b/internal/federation/transport.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package federation import ( diff --git a/internal/federation/util.go b/internal/federation/util.go deleted file mode 100644 index de8654d32..000000000 --- a/internal/federation/util.go +++ /dev/null @@ -1,23 +0,0 @@ -package federation - -import ( - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func (f *federator) blockedDomain(host string) (bool, error) { - b := >smodel.DomainBlock{} - err := f.db.GetWhere([]db.Where{{Key: "domain", Value: host, CaseInsensitive: true}}, b) - if err == nil { - // block exists - return true, nil - } - - if _, ok := err.(db.ErrNoEntries); ok { - // there are no entries so there's no block - return false, nil - } - - // there's an actual error - return false, err -} diff --git a/internal/gtsmodel/account.go b/internal/gtsmodel/account.go index e560601b8..435caea6d 100644 --- a/internal/gtsmodel/account.go +++ b/internal/gtsmodel/account.go @@ -45,11 +45,13 @@ type Account struct { */ // ID of the avatar as a media attachment - AvatarMediaAttachmentID string `pg:"type:CHAR(26)"` + AvatarMediaAttachmentID string `pg:"type:CHAR(26)"` + AvatarMediaAttachment *MediaAttachment `pg:"rel:has-one"` // For a non-local account, where can the header be fetched? AvatarRemoteURL string // ID of the header as a media attachment - HeaderMediaAttachmentID string `pg:"type:CHAR(26)"` + HeaderMediaAttachmentID string `pg:"type:CHAR(26)"` + HeaderMediaAttachment *MediaAttachment `pg:"rel:has-one"` // For a non-local account, where can the header be fetched? HeaderRemoteURL string // DisplayName for this account. Can be empty, then just the Username will be used for display purposes. diff --git a/internal/gtsmodel/domainblock.go b/internal/gtsmodel/domainblock.go index b32984e95..1bed86d8f 100644 --- a/internal/gtsmodel/domainblock.go +++ b/internal/gtsmodel/domainblock.go @@ -31,7 +31,8 @@ type DomainBlock struct { // When was this block updated UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // Account ID of the creator of this block - CreatedByAccountID string `pg:"type:CHAR(26),notnull"` + CreatedByAccountID string `pg:"type:CHAR(26),notnull"` + CreatedByAccount *Account `pg:"rel:belongs-to"` // Private comment on this block, viewable to admins PrivateComment string // Public comment on this block, viewable (optionally) by everyone diff --git a/internal/gtsmodel/emaildomainblock.go b/internal/gtsmodel/emaildomainblock.go index 51558550a..374454374 100644 --- a/internal/gtsmodel/emaildomainblock.go +++ b/internal/gtsmodel/emaildomainblock.go @@ -31,5 +31,6 @@ type EmailDomainBlock struct { // When was this block updated UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // Account ID of the creator of this block - CreatedByAccountID string `pg:"type:CHAR(26),notnull"` + CreatedByAccountID string `pg:"type:CHAR(26),notnull"` + CreatedByAccount *Account `pg:"rel:belongs-to"` } diff --git a/internal/gtsmodel/emoji.go b/internal/gtsmodel/emoji.go index 2fa3b7565..f0996d1a3 100644 --- a/internal/gtsmodel/emoji.go +++ b/internal/gtsmodel/emoji.go @@ -73,5 +73,6 @@ type Emoji struct { // Is this emoji visible in the admin emoji picker? VisibleInPicker bool `pg:",notnull,default:true"` // In which emoji category is this emoji visible? - CategoryID string `pg:"type:CHAR(26)"` + CategoryID string `pg:"type:CHAR(26)"` + Status *Status `pg:"rel:belongs-to"` } diff --git a/internal/gtsmodel/follow.go b/internal/gtsmodel/follow.go index f5a170ca8..8f169f8c4 100644 --- a/internal/gtsmodel/follow.go +++ b/internal/gtsmodel/follow.go @@ -29,9 +29,11 @@ type Follow struct { // When was this follow last updated? UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // Who does this follow belong to? - AccountID string `pg:"type:CHAR(26),unique:srctarget,notnull"` + AccountID string `pg:"type:CHAR(26),unique:srctarget,notnull"` + Account *Account `pg:"rel:belongs-to"` // Who does AccountID follow? - TargetAccountID string `pg:"type:CHAR(26),unique:srctarget,notnull"` + TargetAccountID string `pg:"type:CHAR(26),unique:srctarget,notnull"` + TargetAccount *Account `pg:"rel:has-one"` // Does this follow also want to see reblogs and not just posts? ShowReblogs bool `pg:"default:true"` // What is the activitypub URI of this follow? diff --git a/internal/gtsmodel/followrequest.go b/internal/gtsmodel/followrequest.go index aabb785d2..752c7d0a2 100644 --- a/internal/gtsmodel/followrequest.go +++ b/internal/gtsmodel/followrequest.go @@ -29,9 +29,11 @@ type FollowRequest struct { // When was this follow request last updated? UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // Who does this follow request originate from? - AccountID string `pg:"type:CHAR(26),unique:srctarget,notnull"` + AccountID string `pg:"type:CHAR(26),unique:srctarget,notnull"` + Account Account `pg:"rel:has-one"` // Who is the target of this follow request? - TargetAccountID string `pg:"type:CHAR(26),unique:srctarget,notnull"` + TargetAccountID string `pg:"type:CHAR(26),unique:srctarget,notnull"` + TargetAccount Account `pg:"rel:has-one"` // Does this follow also want to see reblogs and not just posts? ShowReblogs bool `pg:"default:true"` // What is the activitypub URI of this follow request? diff --git a/internal/gtsmodel/instance.go b/internal/gtsmodel/instance.go index 857831ba3..7b453a0b3 100644 --- a/internal/gtsmodel/instance.go +++ b/internal/gtsmodel/instance.go @@ -19,7 +19,8 @@ type Instance struct { // When was this instance suspended, if at all? SuspendedAt time.Time // ID of any existing domain block for this instance in the database - DomainBlockID string `pg:"type:CHAR(26)"` + DomainBlockID string `pg:"type:CHAR(26)"` + DomainBlock *DomainBlock `pg:"rel:has-one"` // Short description of this instance ShortDescription string // Longer description of this instance @@ -31,7 +32,8 @@ type Instance struct { // Username of the contact account for this instance ContactAccountUsername string // Contact account ID in the database for this instance - ContactAccountID string `pg:"type:CHAR(26)"` + ContactAccountID string `pg:"type:CHAR(26)"` + ContactAccount *Account `pg:"rel:has-one"` // Reputation score of this instance Reputation int64 `pg:",notnull,default:0"` // Version of the software used on this instance diff --git a/internal/gtsmodel/mediaattachment.go b/internal/gtsmodel/mediaattachment.go index 2aeeee962..0f12caaad 100644 --- a/internal/gtsmodel/mediaattachment.go +++ b/internal/gtsmodel/mediaattachment.go @@ -42,7 +42,8 @@ type MediaAttachment struct { // Metadata about the file FileMeta FileMeta // To which account does this attachment belong - AccountID string `pg:"type:CHAR(26),notnull"` + AccountID string `pg:"type:CHAR(26),notnull"` + Account *Account `pg:"rel:belongs-to"` // Description of the attachment (for screenreaders) Description string // To which scheduled status does this attachment belong diff --git a/internal/gtsmodel/mention.go b/internal/gtsmodel/mention.go index 47c780521..931e681db 100644 --- a/internal/gtsmodel/mention.go +++ b/internal/gtsmodel/mention.go @@ -25,17 +25,20 @@ type Mention struct { // ID of this mention in the database ID string `pg:"type:CHAR(26),pk,notnull,unique"` // ID of the status this mention originates from - StatusID string `pg:"type:CHAR(26),notnull"` + StatusID string `pg:"type:CHAR(26),notnull"` + Status *Status `pg:"rel:belongs-to"` // When was this mention created? CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // When was this mention last updated? UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // What's the internal account ID of the originator of the mention? - OriginAccountID string `pg:"type:CHAR(26),notnull"` + OriginAccountID string `pg:"type:CHAR(26),notnull"` + OriginAccount *Account `pg:"rel:has-one"` // What's the AP URI of the originator of the mention? OriginAccountURI string `pg:",notnull"` // What's the internal account ID of the mention target? - TargetAccountID string `pg:"type:CHAR(26),notnull"` + TargetAccountID string `pg:"type:CHAR(26),notnull"` + TargetAccount *Account `pg:"rel:has-one"` // Prevent this mention from generating a notification? Silent bool @@ -52,14 +55,14 @@ type Mention struct { // // This will not be put in the database, it's just for convenience. NameString string `pg:"-"` - // MentionedAccountURI is the AP ID (uri) of the user mentioned. + // TargetAccountURI is the AP ID (uri) of the user mentioned. // // This will not be put in the database, it's just for convenience. - MentionedAccountURI string `pg:"-"` - // MentionedAccountURL is the web url of the user mentioned. + TargetAccountURI string `pg:"-"` + // TargetAccountURL is the web url of the user mentioned. // // This will not be put in the database, it's just for convenience. - MentionedAccountURL string `pg:"-"` + TargetAccountURL string `pg:"-"` // A pointer to the gtsmodel account of the mentioned account. - GTSAccount *Account `pg:"-"` + } diff --git a/internal/gtsmodel/notification.go b/internal/gtsmodel/notification.go index efd4fe484..b85bc969e 100644 --- a/internal/gtsmodel/notification.go +++ b/internal/gtsmodel/notification.go @@ -29,24 +29,16 @@ type Notification struct { // Creation time of this notification CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // Which account does this notification target (ie., who will receive the notification?) - TargetAccountID string `pg:"type:CHAR(26),notnull"` + TargetAccountID string `pg:"type:CHAR(26),notnull"` + TargetAccount *Account `pg:"rel:has-one"` // Which account performed the action that created this notification? - OriginAccountID string `pg:"type:CHAR(26),notnull"` + OriginAccountID string `pg:"type:CHAR(26),notnull"` + OriginAccount *Account `pg:"rel:has-one"` // If the notification pertains to a status, what is the database ID of that status? - StatusID string `pg:"type:CHAR(26)"` + StatusID string `pg:"type:CHAR(26)"` + Status *Status `pg:"rel:has-one"` // Has this notification been read already? Read bool - - /* - NON-DATABASE fields - */ - - // gts model of the target account, won't be put in the database, it's just for convenience when passing the notification around. - GTSTargetAccount *Account `pg:"-"` - // gts model of the origin account, won't be put in the database, it's just for convenience when passing the notification around. - GTSOriginAccount *Account `pg:"-"` - // gts model of the relevant status, won't be put in the database, it's just for convenience when passing the notification around. - GTSStatus *Status `pg:"-"` } // NotificationType describes the reason/type of this notification. diff --git a/internal/gtsmodel/status.go b/internal/gtsmodel/status.go index 106298bcd..354f37e04 100644 --- a/internal/gtsmodel/status.go +++ b/internal/gtsmodel/status.go @@ -33,13 +33,17 @@ type Status struct { // the html-formatted content of this status Content string // Database IDs of any media attachments associated with this status - Attachments []string `pg:",array"` + AttachmentIDs []string `pg:"attachments,array"` + Attachments []*MediaAttachment `pg:"attached_media,rel:has-many"` // Database IDs of any tags used in this status - Tags []string `pg:",array"` + TagIDs []string `pg:"tags,array"` + Tags []*Tag `pg:"attached_tags,many2many:status_to_tags"` // https://pg.uptrace.dev/orm/many-to-many-relation/ // Database IDs of any mentions in this status - Mentions []string `pg:",array"` + MentionIDs []string `pg:"mentions,array"` + Mentions []*Mention `pg:"attached_mentions,rel:has-many"` // Database IDs of any emojis used in this status - Emojis []string `pg:",array"` + EmojiIDs []string `pg:"emojis,array"` + Emojis []*Emoji `pg:"attached_emojis,many2many:status_to_emojis"` // https://pg.uptrace.dev/orm/many-to-many-relation/ // when was this status created? CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // when was this status updated? @@ -47,19 +51,24 @@ type Status struct { // is this status from a local account? Local bool // which account posted this status? - AccountID string `pg:"type:CHAR(26),notnull"` + AccountID string `pg:"type:CHAR(26),notnull"` + Account *Account `pg:"rel:has-one"` // AP uri of the owner of this status AccountURI string // id of the status this status is a reply to - InReplyToID string `pg:"type:CHAR(26)"` + InReplyToID string `pg:"type:CHAR(26)"` + InReplyTo *Status `pg:"rel:has-one"` // AP uri of the status this status is a reply to InReplyToURI string // id of the account that this status replies to - InReplyToAccountID string `pg:"type:CHAR(26)"` + InReplyToAccountID string `pg:"type:CHAR(26)"` + InReplyToAccount *Account `pg:"rel:has-one"` // id of the status this status is a boost of - BoostOfID string `pg:"type:CHAR(26)"` + BoostOfID string `pg:"type:CHAR(26)"` + BoostOf *Status `pg:"rel:has-one"` // id of the account that owns the boosted status - BoostOfAccountID string `pg:"type:CHAR(26)"` + BoostOfAccountID string `pg:"type:CHAR(26)"` + BoostOfAccount *Account `pg:"rel:has-one"` // cw string for this status ContentWarning string // visibility entry for this status @@ -69,7 +78,8 @@ type Status struct { // what language is this status written in? Language string // Which application was used to create this status? - CreatedWithApplicationID string `pg:"type:CHAR(26)"` + CreatedWithApplicationID string `pg:"type:CHAR(26)"` + CreatedWithApplication *Application `pg:"rel:has-one"` // advanced visibility for this status VisibilityAdvanced *VisibilityAdvanced // What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types @@ -79,32 +89,18 @@ type Status struct { Text string // Has this status been pinned by its owner? Pinned bool +} - /* - INTERNAL MODEL NON-DATABASE FIELDS +// StatusToTag is an intermediate struct to facilitate the many2many relationship between a status and one or more tags. +type StatusToTag struct { + StatusID string `pg:"unique:statustag"` + TagID string `pg:"unique:statustag"` +} - These are for convenience while passing the status around internally, - but these fields should *never* be put in the db. - */ - - // Account that created this status - GTSAuthorAccount *Account `pg:"-"` - // Mentions created in this status - GTSMentions []*Mention `pg:"-"` - // Hashtags used in this status - GTSTags []*Tag `pg:"-"` - // Emojis used in this status - GTSEmojis []*Emoji `pg:"-"` - // MediaAttachments used in this status - GTSMediaAttachments []*MediaAttachment `pg:"-"` - // Status being replied to - GTSReplyToStatus *Status `pg:"-"` - // Account being replied to - GTSReplyToAccount *Account `pg:"-"` - // Status being boosted - GTSBoostedStatus *Status `pg:"-"` - // Account of the boosted status - GTSBoostedAccount *Account `pg:"-"` +// StatusToEmoji is an intermediate struct to facilitate the many2many relationship between a status and one or more emojis. +type StatusToEmoji struct { + StatusID string `pg:"unique:statusemoji"` + EmojiID string `pg:"unique:statusemoji"` } // Visibility represents the visibility granularity of a status. diff --git a/internal/gtsmodel/statusbookmark.go b/internal/gtsmodel/statusbookmark.go index 7d95067cc..468939bae 100644 --- a/internal/gtsmodel/statusbookmark.go +++ b/internal/gtsmodel/statusbookmark.go @@ -27,9 +27,11 @@ type StatusBookmark struct { // when was this bookmark created CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // id of the account that created ('did') the bookmarking - AccountID string `pg:"type:CHAR(26),notnull"` + AccountID string `pg:"type:CHAR(26),notnull"` + Account *Account `pg:"rel:belongs-to"` // id the account owning the bookmarked status - TargetAccountID string `pg:"type:CHAR(26),notnull"` + TargetAccountID string `pg:"type:CHAR(26),notnull"` + TargetAccount *Account `pg:"rel:has-one"` // database id of the status that has been bookmarked StatusID string `pg:"type:CHAR(26),notnull"` } diff --git a/internal/gtsmodel/statusfave.go b/internal/gtsmodel/statusfave.go index 7152db37a..17952673a 100644 --- a/internal/gtsmodel/statusfave.go +++ b/internal/gtsmodel/statusfave.go @@ -27,18 +27,14 @@ type StatusFave struct { // when was this fave created CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // id of the account that created ('did') the fave - AccountID string `pg:"type:CHAR(26),notnull"` + AccountID string `pg:"type:CHAR(26),notnull"` + Account *Account `pg:"rel:has-one"` // id the account owning the faved status - TargetAccountID string `pg:"type:CHAR(26),notnull"` + TargetAccountID string `pg:"type:CHAR(26),notnull"` + TargetAccount *Account `pg:"rel:has-one"` // database id of the status that has been 'faved' - StatusID string `pg:"type:CHAR(26),notnull"` + StatusID string `pg:"type:CHAR(26),notnull"` + Status *Status `pg:"rel:has-one"` // ActivityPub URI of this fave URI string `pg:",notnull"` - - // GTSStatus is the status being interacted with. It won't be put or retrieved from the db, it's just for conveniently passing a pointer around. - GTSStatus *Status `pg:"-"` - // GTSTargetAccount is the account being interacted with. It won't be put or retrieved from the db, it's just for conveniently passing a pointer around. - GTSTargetAccount *Account `pg:"-"` - // GTSFavingAccount is the account doing the faving. It won't be put or retrieved from the db, it's just for conveniently passing a pointer around. - GTSFavingAccount *Account `pg:"-"` } diff --git a/internal/gtsmodel/statusmute.go b/internal/gtsmodel/statusmute.go index 6cd2b732f..472a5ec09 100644 --- a/internal/gtsmodel/statusmute.go +++ b/internal/gtsmodel/statusmute.go @@ -27,9 +27,12 @@ type StatusMute struct { // when was this mute created CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // id of the account that created ('did') the mute - AccountID string `pg:"type:CHAR(26),notnull"` + AccountID string `pg:"type:CHAR(26),notnull"` + Account *Account `pg:"rel:belongs-to"` // id the account owning the muted status (can be the same as accountID) - TargetAccountID string `pg:"type:CHAR(26),notnull"` + TargetAccountID string `pg:"type:CHAR(26),notnull"` + TargetAccount *Account `pg:"rel:has-one"` // database id of the status that has been muted - StatusID string `pg:"type:CHAR(26),notnull"` + StatusID string `pg:"type:CHAR(26),notnull"` + Status *Status `pg:"rel:has-one"` } diff --git a/internal/gtsmodel/tag.go b/internal/gtsmodel/tag.go index c151e348f..27cce1c8b 100644 --- a/internal/gtsmodel/tag.go +++ b/internal/gtsmodel/tag.go @@ -27,7 +27,7 @@ type Tag struct { // Href of this tag, eg https://example.org/tags/somehashtag URL string // name of this tag -- the tag without the hash part - Name string `pg:",unique,pk,notnull"` + Name string `pg:",unique,notnull"` // Which account ID is the first one we saw using this tag? FirstSeenFromAccountID string `pg:"type:CHAR(26)"` // when was this tag created diff --git a/internal/gtsmodel/user.go b/internal/gtsmodel/user.go index a1e912e99..fe8ebcabe 100644 --- a/internal/gtsmodel/user.go +++ b/internal/gtsmodel/user.go @@ -35,7 +35,8 @@ type User struct { // confirmed email address for this user, this should be unique -- only one email address registered per instance, multiple users per email are not supported Email string `pg:"default:null,unique"` // The id of the local gtsmodel.Account entry for this user, if it exists (unconfirmed users don't have an account yet) - AccountID string `pg:"type:CHAR(26),unique"` + AccountID string `pg:"type:CHAR(26),unique"` + Account *Account `pg:"rel:has-one"` // The encrypted password of this user, generated using https://pkg.go.dev/golang.org/x/crypto/bcrypt#GenerateFromPassword. A salt is included so we're safe against 🌈 tables EncryptedPassword string `pg:",notnull"` @@ -68,7 +69,8 @@ type User struct { // In what timezone/locale is this user located? Locale string // Which application id created this user? See gtsmodel.Application - CreatedByApplicationID string `pg:"type:CHAR(26)"` + CreatedByApplicationID string `pg:"type:CHAR(26)"` + CreatedByApplication *Application `pg:"rel:has-one"` // When did we last contact this user LastEmailedAt time.Time `pg:"type:timestamp"` diff --git a/internal/media/handler.go b/internal/media/handler.go index 0bcf46488..c383a922e 100644 --- a/internal/media/handler.go +++ b/internal/media/handler.go @@ -142,7 +142,7 @@ func (mh *mediaHandler) ProcessHeaderOrAvatar(attachment []byte, accountID strin } // set it in the database - if err := mh.db.SetHeaderOrAvatarForAccountID(ma, accountID); err != nil { + if err := mh.db.SetAccountHeaderOrAvatar(ma, accountID); err != nil { return nil, fmt.Errorf("error putting %s in database: %s", mediaType, err) } @@ -231,8 +231,8 @@ func (mh *mediaHandler) ProcessLocalEmoji(emojiBytes []byte, shortcode string) ( // since emoji aren't 'owned' by an account, but we still want to use the same pattern for serving them through the filserver, // (ie., fileserver/ACCOUNT_ID/etc etc) we need to fetch the INSTANCE ACCOUNT from the database. That is, the account that's created // with the same username as the instance hostname, which doesn't belong to any particular user. - instanceAccount := >smodel.Account{} - if err := mh.db.GetLocalAccountByUsername(mh.config.Host, instanceAccount); err != nil { + instanceAccount, err := mh.db.GetInstanceAccount("") + if err != nil { return nil, fmt.Errorf("error fetching instance account: %s", err) } diff --git a/internal/oauth/clientstore.go b/internal/oauth/clientstore.go index 998f6784e..2e7e0ae88 100644 --- a/internal/oauth/clientstore.go +++ b/internal/oauth/clientstore.go @@ -27,11 +27,11 @@ import ( ) type clientStore struct { - db db.DB + db db.Basic } // NewClientStore returns an implementation of the oauth2 ClientStore interface, using the given db as a storage backend. -func NewClientStore(db db.DB) oauth2.ClientStore { +func NewClientStore(db db.Basic) oauth2.ClientStore { pts := &clientStore{ db: db, } diff --git a/internal/oauth/clientstore_test.go b/internal/oauth/clientstore_test.go index c515ff513..fd3452405 100644 --- a/internal/oauth/clientstore_test.go +++ b/internal/oauth/clientstore_test.go @@ -99,7 +99,7 @@ func (suite *PgClientStoreTestSuite) TestClientSetAndDelete() { // try to get the deleted client; we should get an error deletedClient, err := cs.GetByID(context.Background(), suite.testClientID) suite.Assert().Nil(deletedClient) - suite.Assert().EqualValues(db.ErrNoEntries{}, err) + suite.Assert().EqualValues(db.ErrNoEntries, err) } func TestPgClientStoreTestSuite(t *testing.T) { diff --git a/internal/oauth/server.go b/internal/oauth/server.go index 1289b18af..6d8f50064 100644 --- a/internal/oauth/server.go +++ b/internal/oauth/server.go @@ -66,7 +66,7 @@ type s struct { } // New returns a new oauth server that implements the Server interface -func New(database db.DB, log *logrus.Logger) Server { +func New(database db.Basic, log *logrus.Logger) Server { ts := newTokenStore(context.Background(), database, log) cs := NewClientStore(database) diff --git a/internal/oauth/tokenstore.go b/internal/oauth/tokenstore.go index 5f8e07882..4fd3183fc 100644 --- a/internal/oauth/tokenstore.go +++ b/internal/oauth/tokenstore.go @@ -34,7 +34,7 @@ import ( // tokenStore is an implementation of oauth2.TokenStore, which uses our db interface as a storage backend. type tokenStore struct { oauth2.TokenStore - db db.DB + db db.Basic log *logrus.Logger } @@ -42,7 +42,7 @@ type tokenStore struct { // // In order to allow tokens to 'expire', it will also set off a goroutine that iterates through // the tokens in the DB once per minute and deletes any that have expired. -func newTokenStore(ctx context.Context, db db.DB, log *logrus.Logger) oauth2.TokenStore { +func newTokenStore(ctx context.Context, db db.Basic, log *logrus.Logger) oauth2.TokenStore { pts := &tokenStore{ db: db, log: log, diff --git a/internal/processing/account/createblock.go b/internal/processing/account/createblock.go index 79ce03805..f10a2efa3 100644 --- a/internal/processing/account/createblock.go +++ b/internal/processing/account/createblock.go @@ -31,24 +31,20 @@ import ( func (p *processor) BlockCreate(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { // 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, gtserror.NewErrorNotFound(fmt.Errorf("BlockCreate: account %s not found in the db: %s", targetAccountID, err)) - } + targetAccount, err := p.db.GetAccountByID(targetAccountID) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("BlockCreate: error getting account %s from the db: %s", targetAccountID, err)) } // if requestingAccount already blocks target account, we don't need to do anything - block := >smodel.Block{} - if err := p.db.GetWhere([]db.Where{ - {Key: "account_id", Value: requestingAccount.ID}, - {Key: "target_account_id", Value: targetAccountID}, - }, block); err == nil { - // block already exists, just return relationship + if blocked, err := p.db.IsBlocked(requestingAccount.ID, targetAccountID, false); err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error checking existence of block: %s", err)) + } else if blocked { return p.RelationshipGet(requestingAccount, targetAccountID) } // make the block + block := >smodel.Block{} newBlockID, err := id.NewULID() if err != nil { return nil, gtserror.NewErrorInternalError(err) @@ -57,7 +53,7 @@ func (p *processor) BlockCreate(requestingAccount *gtsmodel.Account, targetAccou block.AccountID = requestingAccount.ID block.Account = requestingAccount block.TargetAccountID = targetAccountID - block.TargetAccount = targetAcct + block.TargetAccount = targetAccount block.URI = util.GenerateURIForBlock(requestingAccount.Username, p.config.Protocol, p.config.Host, newBlockID) // whack it in the database @@ -123,7 +119,7 @@ func (p *processor) BlockCreate(requestingAccount *gtsmodel.Account, targetAccou URI: frURI, }, OriginAccount: requestingAccount, - TargetAccount: targetAcct, + TargetAccount: targetAccount, } } @@ -138,7 +134,7 @@ func (p *processor) BlockCreate(requestingAccount *gtsmodel.Account, targetAccou URI: fURI, }, OriginAccount: requestingAccount, - TargetAccount: targetAcct, + TargetAccount: targetAccount, } } @@ -148,7 +144,7 @@ func (p *processor) BlockCreate(requestingAccount *gtsmodel.Account, targetAccou APActivityType: gtsmodel.ActivityStreamsCreate, GTSModel: block, OriginAccount: requestingAccount, - TargetAccount: targetAcct, + TargetAccount: targetAccount, } return p.RelationshipGet(requestingAccount, targetAccountID) diff --git a/internal/processing/account/createfollow.go b/internal/processing/account/createfollow.go index e89db9d47..8c856a50e 100644 --- a/internal/processing/account/createfollow.go +++ b/internal/processing/account/createfollow.go @@ -31,38 +31,33 @@ import ( func (p *processor) FollowCreate(requestingAccount *gtsmodel.Account, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) { // if there's a block between the accounts we shouldn't create the request ofc - blocked, err := p.db.Blocked(requestingAccount.ID, form.ID) - if err != nil { + if blocked, err := p.db.IsBlocked(requestingAccount.ID, form.ID, true); err != nil { return nil, gtserror.NewErrorInternalError(err) - } - if blocked { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("accountfollowcreate: block exists between accounts")) + } else if blocked { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts")) } // make sure the target account actually exists in our db - targetAcct := >smodel.Account{} - if err := p.db.GetByID(form.ID, targetAcct); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + targetAcct, err := p.db.GetAccountByID(form.ID) + if err != nil { + if err == db.ErrNoEntries { return nil, gtserror.NewErrorNotFound(fmt.Errorf("accountfollowcreate: account %s not found in the db: %s", form.ID, err)) } + return nil, gtserror.NewErrorInternalError(err) } // check if a follow exists already - follows, err := p.db.Follows(requestingAccount, targetAcct) - if err != nil { + if follows, err := p.db.IsFollowing(requestingAccount, targetAcct); err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow in db: %s", err)) - } - if follows { + } else if follows { // already follows so just return the relationship return p.RelationshipGet(requestingAccount, form.ID) } - // check if a follow exists already - followRequested, err := p.db.FollowRequested(requestingAccount, targetAcct) - if err != nil { + // check if a follow request exists already + if followRequested, err := p.db.IsFollowRequested(requestingAccount, targetAcct); err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow request in db: %s", err)) - } - if followRequested { + } else if followRequested { // already follow requested so just return the relationship return p.RelationshipGet(requestingAccount, form.ID) } diff --git a/internal/processing/account/delete.go b/internal/processing/account/delete.go index 65ac02291..e8840abae 100644 --- a/internal/processing/account/delete.go +++ b/internal/processing/account/delete.go @@ -133,9 +133,9 @@ func (p *processor) Delete(account *gtsmodel.Account, origin string) error { var maxID string selectStatusesLoop: for { - statuses, err := p.db.GetStatusesForAccount(account.ID, 20, false, maxID, false, false) + statuses, err := p.db.GetAccountStatuses(account.ID, 20, false, maxID, false, false) if err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + if err == db.ErrNoEntries { // no statuses left for this instance so we're done l.Infof("Delete: done iterating through statuses for account %s", account.Username) break selectStatusesLoop @@ -147,7 +147,7 @@ selectStatusesLoop: for i, s := range statuses { // pass the status delete through the client api channel for processing - s.GTSAuthorAccount = account + s.Account = account l.Debug("putting status in the client api channel") p.fromClientAPI <- gtsmodel.FromClientAPI{ APObjectType: gtsmodel.ActivityStreamsNote, @@ -158,7 +158,7 @@ selectStatusesLoop: } if err := p.db.DeleteByID(s.ID, s); err != nil { - if _, ok := err.(db.ErrNoEntries); !ok { + if err != db.ErrNoEntries { // actual error has occurred l.Errorf("Delete: db error status %s for account %s: %s", s.ID, account.Username, err) break selectStatusesLoop @@ -168,7 +168,7 @@ selectStatusesLoop: // if there are any boosts of this status, delete them as well boosts := []*gtsmodel.Status{} if err := p.db.GetWhere([]db.Where{{Key: "boost_of_id", Value: s.ID}}, &boosts); err != nil { - if _, ok := err.(db.ErrNoEntries); !ok { + if err != db.ErrNoEntries { // an actual error has occurred l.Errorf("Delete: db error selecting boosts of status %s for account %s: %s", s.ID, account.Username, err) break selectStatusesLoop @@ -190,7 +190,7 @@ selectStatusesLoop: } if err := p.db.DeleteByID(b.ID, b); err != nil { - if _, ok := err.(db.ErrNoEntries); !ok { + if err != db.ErrNoEntries { // actual error has occurred l.Errorf("Delete: db error deleting boost with id %s: %s", b.ID, err) break selectStatusesLoop diff --git a/internal/processing/account/get.go b/internal/processing/account/get.go index a70bf02bd..3dfc54b51 100644 --- a/internal/processing/account/get.go +++ b/internal/processing/account/get.go @@ -30,7 +30,7 @@ import ( func (p *processor) Get(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, error) { targetAccount := >smodel.Account{} if err := p.db.GetByID(targetAccountID, targetAccount); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + if err == db.ErrNoEntries { return nil, errors.New("account not found") } return nil, fmt.Errorf("db error: %s", err) @@ -39,7 +39,7 @@ func (p *processor) Get(requestingAccount *gtsmodel.Account, targetAccountID str var blocked bool var err error if requestingAccount != nil { - blocked, err = p.db.Blocked(requestingAccount.ID, targetAccountID) + blocked, err = p.db.IsBlocked(requestingAccount.ID, targetAccountID, true) if err != nil { return nil, fmt.Errorf("error checking account block: %s", err) } diff --git a/internal/processing/account/getfollowers.go b/internal/processing/account/getfollowers.go index 0806a82c0..4f66b40ee 100644 --- a/internal/processing/account/getfollowers.go +++ b/internal/processing/account/getfollowers.go @@ -28,26 +28,23 @@ import ( ) func (p *processor) FollowersGet(requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) { - blocked, err := p.db.Blocked(requestingAccount.ID, targetAccountID) - if err != nil { + if blocked, err := p.db.IsBlocked(requestingAccount.ID, targetAccountID, true); err != nil { return nil, gtserror.NewErrorInternalError(err) - } - - if blocked { + } else if blocked { return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts")) } - followers := []gtsmodel.Follow{} accounts := []apimodel.Account{} - if err := p.db.GetFollowersByAccountID(targetAccountID, &followers, false); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + follows, err := p.db.GetAccountFollowedBy(targetAccountID, false) + if err != nil { + if err == db.ErrNoEntries { return accounts, nil } return nil, gtserror.NewErrorInternalError(err) } - for _, f := range followers { - blocked, err := p.db.Blocked(requestingAccount.ID, f.AccountID) + for _, f := range follows { + blocked, err := p.db.IsBlocked(requestingAccount.ID, f.AccountID, true) if err != nil { return nil, gtserror.NewErrorInternalError(err) } @@ -55,15 +52,18 @@ func (p *processor) FollowersGet(requestingAccount *gtsmodel.Account, targetAcco continue } - a := >smodel.Account{} - if err := p.db.GetByID(f.AccountID, a); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { - continue + if f.Account == nil { + a, err := p.db.GetAccountByID(f.AccountID) + if err != nil { + if err == db.ErrNoEntries { + continue + } + return nil, gtserror.NewErrorInternalError(err) } - return nil, gtserror.NewErrorInternalError(err) + f.Account = a } - account, err := p.tc.AccountToMastoPublic(a) + account, err := p.tc.AccountToMastoPublic(f.Account) if err != nil { return nil, gtserror.NewErrorInternalError(err) } diff --git a/internal/processing/account/getfollowing.go b/internal/processing/account/getfollowing.go index 75e89dacb..c7fb426f9 100644 --- a/internal/processing/account/getfollowing.go +++ b/internal/processing/account/getfollowing.go @@ -28,26 +28,23 @@ import ( ) func (p *processor) FollowingGet(requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) { - blocked, err := p.db.Blocked(requestingAccount.ID, targetAccountID) - if err != nil { + if blocked, err := p.db.IsBlocked(requestingAccount.ID, targetAccountID, true); err != nil { return nil, gtserror.NewErrorInternalError(err) - } - - if blocked { + } else if blocked { return nil, gtserror.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 { + follows, err := p.db.GetAccountFollows(targetAccountID) + if err != nil { + if err == db.ErrNoEntries { return accounts, nil } return nil, gtserror.NewErrorInternalError(err) } - for _, f := range following { - blocked, err := p.db.Blocked(requestingAccount.ID, f.AccountID) + for _, f := range follows { + blocked, err := p.db.IsBlocked(requestingAccount.ID, f.AccountID, true) if err != nil { return nil, gtserror.NewErrorInternalError(err) } @@ -55,15 +52,18 @@ func (p *processor) FollowingGet(requestingAccount *gtsmodel.Account, targetAcco continue } - a := >smodel.Account{} - if err := p.db.GetByID(f.TargetAccountID, a); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { - continue + if f.TargetAccount == nil { + a, err := p.db.GetAccountByID(f.TargetAccountID) + if err != nil { + if err == db.ErrNoEntries { + continue + } + return nil, gtserror.NewErrorInternalError(err) } - return nil, gtserror.NewErrorInternalError(err) + f.TargetAccount = a } - account, err := p.tc.AccountToMastoPublic(a) + account, err := p.tc.AccountToMastoPublic(f.TargetAccount) if err != nil { return nil, gtserror.NewErrorInternalError(err) } diff --git a/internal/processing/account/getstatuses.go b/internal/processing/account/getstatuses.go index b8ccbc528..dc21e7006 100644 --- a/internal/processing/account/getstatuses.go +++ b/internal/processing/account/getstatuses.go @@ -28,18 +28,17 @@ import ( ) func (p *processor) StatusesGet(requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode) { - targetAccount := >smodel.Account{} - if err := p.db.GetByID(targetAccountID, targetAccount); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("no entry found for account id %s", targetAccountID)) - } + if blocked, err := p.db.IsBlocked(requestingAccount.ID, targetAccountID, true); err != nil { return nil, gtserror.NewErrorInternalError(err) + } else if blocked { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts")) } apiStatuses := []apimodel.Status{} - statuses, err := p.db.GetStatusesForAccount(targetAccountID, limit, excludeReplies, maxID, pinnedOnly, mediaOnly) + + statuses, err := p.db.GetAccountStatuses(targetAccountID, limit, excludeReplies, maxID, pinnedOnly, mediaOnly) if err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + if err == db.ErrNoEntries { return apiStatuses, nil } return nil, gtserror.NewErrorInternalError(err) diff --git a/internal/processing/account/removeblock.go b/internal/processing/account/removeblock.go index 03b0c6750..7c1f2bc17 100644 --- a/internal/processing/account/removeblock.go +++ b/internal/processing/account/removeblock.go @@ -29,11 +29,9 @@ import ( func (p *processor) BlockRemove(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { // 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, gtserror.NewErrorNotFound(fmt.Errorf("BlockRemove: account %s not found in the db: %s", targetAccountID, err)) - } + targetAccount, err := p.db.GetAccountByID(targetAccountID) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("BlockCreate: error getting account %s from the db: %s", targetAccountID, err)) } // check if a block exists, and remove it if it does (storing the URI for later) @@ -44,7 +42,7 @@ func (p *processor) BlockRemove(requestingAccount *gtsmodel.Account, targetAccou {Key: "target_account_id", Value: targetAccountID}, }, block); err == nil { block.Account = requestingAccount - block.TargetAccount = targetAcct + block.TargetAccount = targetAccount if err := p.db.DeleteByID(block.ID, >smodel.Block{}); err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockRemove: error removing block from db: %s", err)) } @@ -58,7 +56,7 @@ func (p *processor) BlockRemove(requestingAccount *gtsmodel.Account, targetAccou APActivityType: gtsmodel.ActivityStreamsUndo, GTSModel: block, OriginAccount: requestingAccount, - TargetAccount: targetAcct, + TargetAccount: targetAccount, } } diff --git a/internal/processing/account/removefollow.go b/internal/processing/account/removefollow.go index ef8994893..6646d694e 100644 --- a/internal/processing/account/removefollow.go +++ b/internal/processing/account/removefollow.go @@ -29,7 +29,7 @@ import ( func (p *processor) FollowRemove(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { // if there's a block between the accounts we shouldn't do anything - blocked, err := p.db.Blocked(requestingAccount.ID, targetAccountID) + blocked, err := p.db.IsBlocked(requestingAccount.ID, targetAccountID, true) if err != nil { return nil, gtserror.NewErrorInternalError(err) } @@ -40,7 +40,7 @@ func (p *processor) FollowRemove(requestingAccount *gtsmodel.Account, targetAcco // 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 { + if err == db.ErrNoEntries { return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: account %s not found in the db: %s", targetAccountID, err)) } } diff --git a/internal/processing/admin/createdomainblock.go b/internal/processing/admin/createdomainblock.go index df02cef94..624f632dc 100644 --- a/internal/processing/admin/createdomainblock.go +++ b/internal/processing/admin/createdomainblock.go @@ -36,7 +36,7 @@ func (p *processor) DomainBlockCreate(account *gtsmodel.Account, domain string, domainBlock := >smodel.DomainBlock{} err := p.db.GetWhere([]db.Where{{Key: "domain", Value: domain, CaseInsensitive: true}}, domainBlock) if err != nil { - if _, ok := err.(db.ErrNoEntries); !ok { + if err != db.ErrNoEntries { // something went wrong in the DB return nil, gtserror.NewErrorInternalError(fmt.Errorf("DomainBlockCreate: db error checking for existence of domain block %s: %s", domain, err)) } @@ -60,7 +60,7 @@ func (p *processor) DomainBlockCreate(account *gtsmodel.Account, domain string, // put the new block in the database if err := p.db.Put(domainBlock); err != nil { - if _, ok := err.(db.ErrAlreadyExists); !ok { + if err != db.ErrNoEntries { // there's a real error creating the block return nil, gtserror.NewErrorInternalError(fmt.Errorf("DomainBlockCreate: db error putting new domain block %s: %s", domain, err)) } @@ -123,9 +123,9 @@ func (p *processor) initiateDomainBlockSideEffects(account *gtsmodel.Account, bl selectAccountsLoop: for { - accounts, err := p.db.GetAccountsForInstance(block.Domain, maxID, limit) + accounts, err := p.db.GetInstanceAccounts(block.Domain, maxID, limit) if err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + if err == db.ErrNoEntries { // no accounts left for this instance so we're done l.Infof("domainBlockProcessSideEffects: done iterating through accounts for domain %s", block.Domain) break selectAccountsLoop diff --git a/internal/processing/admin/deletedomainblock.go b/internal/processing/admin/deletedomainblock.go index b41fedd92..edb0a58f9 100644 --- a/internal/processing/admin/deletedomainblock.go +++ b/internal/processing/admin/deletedomainblock.go @@ -32,7 +32,7 @@ func (p *processor) DomainBlockDelete(account *gtsmodel.Account, id string) (*ap domainBlock := >smodel.DomainBlock{} if err := p.db.GetByID(id, domainBlock); err != nil { - if _, ok := err.(db.ErrNoEntries); !ok { + if err != db.ErrNoEntries { // something has gone really wrong return nil, gtserror.NewErrorInternalError(err) } diff --git a/internal/processing/admin/getdomainblock.go b/internal/processing/admin/getdomainblock.go index 7d1f9e2ab..f74010627 100644 --- a/internal/processing/admin/getdomainblock.go +++ b/internal/processing/admin/getdomainblock.go @@ -31,7 +31,7 @@ func (p *processor) DomainBlockGet(account *gtsmodel.Account, id string, export domainBlock := >smodel.DomainBlock{} if err := p.db.GetByID(id, domainBlock); err != nil { - if _, ok := err.(db.ErrNoEntries); !ok { + if err != db.ErrNoEntries { // something has gone really wrong return nil, gtserror.NewErrorInternalError(err) } diff --git a/internal/processing/admin/getdomainblocks.go b/internal/processing/admin/getdomainblocks.go index 5e2241412..f827d03fc 100644 --- a/internal/processing/admin/getdomainblocks.go +++ b/internal/processing/admin/getdomainblocks.go @@ -29,7 +29,7 @@ func (p *processor) DomainBlocksGet(account *gtsmodel.Account, export bool) ([]* domainBlocks := []*gtsmodel.DomainBlock{} if err := p.db.GetAll(&domainBlocks); err != nil { - if _, ok := err.(db.ErrNoEntries); !ok { + if err != db.ErrNoEntries { // something has gone really wrong return nil, gtserror.NewErrorInternalError(err) } diff --git a/internal/processing/blocks.go b/internal/processing/blocks.go index 509600ca6..809cbde8e 100644 --- a/internal/processing/blocks.go +++ b/internal/processing/blocks.go @@ -29,9 +29,9 @@ import ( ) func (p *processor) BlocksGet(authed *oauth.Auth, maxID string, sinceID string, limit int) (*apimodel.BlocksResponse, gtserror.WithCode) { - accounts, nextMaxID, prevMinID, err := p.db.GetBlocksForAccount(authed.Account.ID, maxID, sinceID, limit) + accounts, nextMaxID, prevMinID, err := p.db.GetAccountBlocks(authed.Account.ID, maxID, sinceID, limit) if err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + if err == db.ErrNoEntries { // there are just no entries return &apimodel.BlocksResponse{ Accounts: []*apimodel.Account{}, diff --git a/internal/processing/federation.go b/internal/processing/federation.go index 765fdf862..cea14b4de 100644 --- a/internal/processing/federation.go +++ b/internal/processing/federation.go @@ -36,13 +36,12 @@ import ( func (p *processor) GetFediUser(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) { // get the account the request is referring to - requestedAccount := >smodel.Account{} - if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { + requestedAccount, err := p.db.GetLocalAccountByUsername(requestedUsername) + if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) } var requestedPerson vocab.ActivityStreamsPerson - var err error if util.IsPublicKeyPath(requestURL) { // if it's a public key path, we don't need to authenticate but we'll only serve the bare minimum user profile needed for the public key requestedPerson, err = p.tc.AccountToASMinimal(requestedAccount) @@ -63,7 +62,7 @@ func (p *processor) GetFediUser(ctx context.Context, requestedUsername string, r return nil, gtserror.NewErrorNotAuthorized(err) } - blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID) + blocked, err := p.db.IsBlocked(requestedAccount.ID, requestingAccount.ID, true) if err != nil { return nil, gtserror.NewErrorInternalError(err) } @@ -91,8 +90,8 @@ func (p *processor) GetFediUser(ctx context.Context, requestedUsername string, r func (p *processor) GetFediFollowers(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) { // get the account the request is referring to - requestedAccount := >smodel.Account{} - if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { + requestedAccount, err := p.db.GetLocalAccountByUsername(requestedUsername) + if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) } @@ -107,7 +106,7 @@ func (p *processor) GetFediFollowers(ctx context.Context, requestedUsername stri return nil, gtserror.NewErrorNotAuthorized(err) } - blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID) + blocked, err := p.db.IsBlocked(requestedAccount.ID, requestingAccount.ID, true) if err != nil { return nil, gtserror.NewErrorInternalError(err) } @@ -136,8 +135,8 @@ func (p *processor) GetFediFollowers(ctx context.Context, requestedUsername stri func (p *processor) GetFediFollowing(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) { // get the account the request is referring to - requestedAccount := >smodel.Account{} - if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { + requestedAccount, err := p.db.GetLocalAccountByUsername(requestedUsername) + if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) } @@ -152,7 +151,7 @@ func (p *processor) GetFediFollowing(ctx context.Context, requestedUsername stri return nil, gtserror.NewErrorNotAuthorized(err) } - blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID) + blocked, err := p.db.IsBlocked(requestedAccount.ID, requestingAccount.ID, true) if err != nil { return nil, gtserror.NewErrorInternalError(err) } @@ -181,8 +180,8 @@ func (p *processor) GetFediFollowing(ctx context.Context, requestedUsername stri func (p *processor) GetFediStatus(ctx context.Context, requestedUsername string, requestedStatusID string, requestURL *url.URL) (interface{}, gtserror.WithCode) { // get the account the request is referring to - requestedAccount := >smodel.Account{} - if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { + requestedAccount, err := p.db.GetLocalAccountByUsername(requestedUsername) + if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) } @@ -199,7 +198,7 @@ func (p *processor) GetFediStatus(ctx context.Context, requestedUsername string, // authorize the request: // 1. check if a block exists between the requester and the requestee - blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID) + blocked, err := p.db.IsBlocked(requestedAccount.ID, requestingAccount.ID, true) if err != nil { return nil, gtserror.NewErrorInternalError(err) } @@ -241,8 +240,8 @@ func (p *processor) GetFediStatus(ctx context.Context, requestedUsername string, func (p *processor) GetFediStatusReplies(ctx context.Context, requestedUsername string, requestedStatusID string, page bool, onlyOtherAccounts bool, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) { // get the account the request is referring to - requestedAccount := >smodel.Account{} - if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { + requestedAccount, err := p.db.GetLocalAccountByUsername(requestedUsername) + if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) } @@ -259,7 +258,7 @@ func (p *processor) GetFediStatusReplies(ctx context.Context, requestedUsername // authorize the request: // 1. check if a block exists between the requester and the requestee - blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID) + blocked, err := p.db.IsBlocked(requestedAccount.ID, requestingAccount.ID, true) if err != nil { return nil, gtserror.NewErrorInternalError(err) } @@ -321,7 +320,7 @@ func (p *processor) GetFediStatusReplies(ctx context.Context, requestedUsername } else { // scenario 3 // get immediate children - replies, err := p.db.StatusChildren(s, true, minID) + replies, err := p.db.GetStatusChildren(s, true, minID) if err != nil { return nil, gtserror.NewErrorInternalError(err) } @@ -374,8 +373,8 @@ func (p *processor) GetFediStatusReplies(ctx context.Context, requestedUsername func (p *processor) GetWebfingerAccount(ctx context.Context, requestedUsername string, requestURL *url.URL) (*apimodel.WellKnownResponse, gtserror.WithCode) { // get the account the request is referring to - requestedAccount := >smodel.Account{} - if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { + requestedAccount, err := p.db.GetLocalAccountByUsername(requestedUsername) + if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) } diff --git a/internal/processing/followrequest.go b/internal/processing/followrequest.go index 5eb9fd6ad..867725023 100644 --- a/internal/processing/followrequest.go +++ b/internal/processing/followrequest.go @@ -27,9 +27,9 @@ import ( ) func (p *processor) FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, gtserror.WithCode) { - frs := []gtsmodel.FollowRequest{} - if err := p.db.GetFollowRequestsForAccountID(auth.Account.ID, &frs); err != nil { - if _, ok := err.(db.ErrNoEntries); !ok { + frs, err := p.db.GetAccountFollowRequests(auth.Account.ID) + if err != nil { + if err != db.ErrNoEntries { return nil, gtserror.NewErrorInternalError(err) } } diff --git a/internal/processing/fromclientapi.go b/internal/processing/fromclientapi.go index 6755a9d82..beed283c1 100644 --- a/internal/processing/fromclientapi.go +++ b/internal/processing/fromclientapi.go @@ -187,19 +187,19 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error return errors.New("note was not parseable as *gtsmodel.Status") } - if statusToDelete.GTSAuthorAccount == nil { - statusToDelete.GTSAuthorAccount = clientMsg.OriginAccount + if statusToDelete.Account == nil { + statusToDelete.Account = clientMsg.OriginAccount } // delete all attachments for this status - for _, a := range statusToDelete.Attachments { + for _, a := range statusToDelete.AttachmentIDs { if err := p.mediaProcessor.Delete(a); err != nil { return err } } // delete all mentions for this status - for _, m := range statusToDelete.Mentions { + for _, m := range statusToDelete.MentionIDs { if err := p.db.DeleteByID(m, >smodel.Mention{}); err != nil { return err } @@ -237,16 +237,16 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error // TODO: move all the below functions into federation.Federator func (p *processor) federateStatus(status *gtsmodel.Status) error { - if status.GTSAuthorAccount == nil { + if status.Account == nil { a := >smodel.Account{} if err := p.db.GetByID(status.AccountID, a); err != nil { return fmt.Errorf("federateStatus: error fetching status author account: %s", err) } - status.GTSAuthorAccount = a + status.Account = a } // do nothing if this isn't our status - if status.GTSAuthorAccount.Domain != "" { + if status.Account.Domain != "" { return nil } @@ -255,9 +255,9 @@ func (p *processor) federateStatus(status *gtsmodel.Status) error { return fmt.Errorf("federateStatus: error converting status to as format: %s", err) } - outboxIRI, err := url.Parse(status.GTSAuthorAccount.OutboxURI) + outboxIRI, err := url.Parse(status.Account.OutboxURI) if err != nil { - return fmt.Errorf("federateStatus: error parsing outboxURI %s: %s", status.GTSAuthorAccount.OutboxURI, err) + return fmt.Errorf("federateStatus: error parsing outboxURI %s: %s", status.Account.OutboxURI, err) } _, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, asStatus) @@ -265,16 +265,16 @@ func (p *processor) federateStatus(status *gtsmodel.Status) error { } func (p *processor) federateStatusDelete(status *gtsmodel.Status) error { - if status.GTSAuthorAccount == nil { + if status.Account == nil { a := >smodel.Account{} if err := p.db.GetByID(status.AccountID, a); err != nil { return fmt.Errorf("federateStatus: error fetching status author account: %s", err) } - status.GTSAuthorAccount = a + status.Account = a } // do nothing if this isn't our status - if status.GTSAuthorAccount.Domain != "" { + if status.Account.Domain != "" { return nil } @@ -283,14 +283,14 @@ func (p *processor) federateStatusDelete(status *gtsmodel.Status) error { return fmt.Errorf("federateStatusDelete: error converting status to as format: %s", err) } - outboxIRI, err := url.Parse(status.GTSAuthorAccount.OutboxURI) + outboxIRI, err := url.Parse(status.Account.OutboxURI) if err != nil { - return fmt.Errorf("federateStatusDelete: error parsing outboxURI %s: %s", status.GTSAuthorAccount.OutboxURI, err) + return fmt.Errorf("federateStatusDelete: error parsing outboxURI %s: %s", status.Account.OutboxURI, err) } - actorIRI, err := url.Parse(status.GTSAuthorAccount.URI) + actorIRI, err := url.Parse(status.Account.URI) if err != nil { - return fmt.Errorf("federateStatusDelete: error parsing actorIRI %s: %s", status.GTSAuthorAccount.URI, err) + return fmt.Errorf("federateStatusDelete: error parsing actorIRI %s: %s", status.Account.URI, err) } // create a delete and set the appropriate actor on it diff --git a/internal/processing/fromcommon.go b/internal/processing/fromcommon.go index d719b7f5f..2c2635175 100644 --- a/internal/processing/fromcommon.go +++ b/internal/processing/fromcommon.go @@ -30,35 +30,31 @@ import ( func (p *processor) notifyStatus(status *gtsmodel.Status) error { // if there are no mentions in this status then just bail - if len(status.Mentions) == 0 { + if len(status.MentionIDs) == 0 { return nil } - if status.GTSMentions == nil { + if status.Mentions == 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) + menchies, err := p.db.GetMentions(status.MentionIDs) + if err != nil { + return fmt.Errorf("notifyStatus: error getting mentions for status %s from the db: %s", status.ID, err) } - status.GTSMentions = menchies + status.Mentions = menchies } // now we have mentions as full gtsmodel.Mention structs on the status we can continue - for _, m := range status.GTSMentions { + for _, m := range status.Mentions { // 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 { + if m.TargetAccount == nil { + a, err := p.db.GetAccountByID(m.TargetAccountID) + if 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 + m.TargetAccount = a } - if m.GTSAccount.Domain != "" { + if m.TargetAccount.Domain != "" { // not a local account so skip it continue } @@ -74,7 +70,7 @@ func (p *processor) notifyStatus(status *gtsmodel.Status) error { // notification exists already so just continue continue } - if _, ok := err.(db.ErrNoEntries); !ok { + if err != db.ErrNoEntries { // 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) } @@ -89,8 +85,11 @@ func (p *processor) notifyStatus(status *gtsmodel.Status) error { ID: notifID, NotificationType: gtsmodel.NotificationMention, TargetAccountID: m.TargetAccountID, + TargetAccount: m.TargetAccount, OriginAccountID: status.AccountID, + OriginAccount: status.Account, StatusID: status.ID, + Status: status, } if err := p.db.Put(notif); err != nil { @@ -103,7 +102,7 @@ func (p *processor) notifyStatus(status *gtsmodel.Status) error { return fmt.Errorf("notifyStatus: error converting notification to masto representation: %s", err) } - if err := p.streamingProcessor.StreamNotificationToAccount(mastoNotif, m.GTSAccount); err != nil { + if err := p.streamingProcessor.StreamNotificationToAccount(mastoNotif, m.TargetAccount); err != nil { return fmt.Errorf("notifyStatus: error streaming notification to account: %s", err) } } @@ -146,9 +145,9 @@ func (p *processor) notifyFollowRequest(followRequest *gtsmodel.FollowRequest, r return nil } -func (p *processor) notifyFollow(follow *gtsmodel.Follow, receivingAccount *gtsmodel.Account) error { +func (p *processor) notifyFollow(follow *gtsmodel.Follow, targetAccount *gtsmodel.Account) error { // return if this isn't a local account - if receivingAccount.Domain != "" { + if targetAccount.Domain != "" { return nil } @@ -171,7 +170,9 @@ func (p *processor) notifyFollow(follow *gtsmodel.Follow, receivingAccount *gtsm ID: notifID, NotificationType: gtsmodel.NotificationFollow, TargetAccountID: follow.TargetAccountID, + TargetAccount: follow.TargetAccount, OriginAccountID: follow.AccountID, + OriginAccount: follow.Account, } if err := p.db.Put(notif); err != nil { return fmt.Errorf("notifyFollow: error putting notification in database: %s", err) @@ -183,16 +184,16 @@ func (p *processor) notifyFollow(follow *gtsmodel.Follow, receivingAccount *gtsm return fmt.Errorf("notifyStatus: error converting notification to masto representation: %s", err) } - if err := p.streamingProcessor.StreamNotificationToAccount(mastoNotif, receivingAccount); err != nil { + if err := p.streamingProcessor.StreamNotificationToAccount(mastoNotif, targetAccount); err != nil { return fmt.Errorf("notifyStatus: error streaming notification to account: %s", err) } return nil } -func (p *processor) notifyFave(fave *gtsmodel.StatusFave, receivingAccount *gtsmodel.Account) error { +func (p *processor) notifyFave(fave *gtsmodel.StatusFave, targetAccount *gtsmodel.Account) error { // return if this isn't a local account - if receivingAccount.Domain != "" { + if targetAccount.Domain != "" { return nil } @@ -205,8 +206,11 @@ func (p *processor) notifyFave(fave *gtsmodel.StatusFave, receivingAccount *gtsm ID: notifID, NotificationType: gtsmodel.NotificationFave, TargetAccountID: fave.TargetAccountID, + TargetAccount: fave.TargetAccount, OriginAccountID: fave.AccountID, + OriginAccount: fave.Account, StatusID: fave.StatusID, + Status: fave.Status, } if err := p.db.Put(notif); err != nil { @@ -219,7 +223,7 @@ func (p *processor) notifyFave(fave *gtsmodel.StatusFave, receivingAccount *gtsm return fmt.Errorf("notifyStatus: error converting notification to masto representation: %s", err) } - if err := p.streamingProcessor.StreamNotificationToAccount(mastoNotif, receivingAccount); err != nil { + if err := p.streamingProcessor.StreamNotificationToAccount(mastoNotif, targetAccount); err != nil { return fmt.Errorf("notifyStatus: error streaming notification to account: %s", err) } @@ -232,22 +236,29 @@ func (p *processor) notifyAnnounce(status *gtsmodel.Status) error { return nil } - boostedStatus := >smodel.Status{} - if err := p.db.GetByID(status.BoostOfID, boostedStatus); err != nil { - return fmt.Errorf("notifyAnnounce: error getting status with id %s: %s", status.BoostOfID, err) + if status.BoostOf == nil { + boostedStatus, err := p.db.GetStatusByID(status.BoostOfID) + if err != nil { + return fmt.Errorf("notifyAnnounce: error getting status with id %s: %s", status.BoostOfID, err) + } + status.BoostOf = boostedStatus } - boostedAcct := >smodel.Account{} - if err := p.db.GetByID(boostedStatus.AccountID, boostedAcct); err != nil { - return fmt.Errorf("notifyAnnounce: error getting account with id %s: %s", boostedStatus.AccountID, err) + if status.BoostOfAccount == nil { + boostedAcct, err := p.db.GetAccountByID(status.BoostOfAccountID) + if err != nil { + return fmt.Errorf("notifyAnnounce: error getting account with id %s: %s", status.BoostOfAccountID, err) + } + status.BoostOf.Account = boostedAcct + status.BoostOfAccount = boostedAcct } - if boostedAcct.Domain != "" { + if status.BoostOfAccount.Domain == "" { // remote account, nothing to do return nil } - if boostedStatus.AccountID == status.AccountID { + if status.BoostOfAccountID == status.AccountID { // it's a self boost, nothing to do return nil } @@ -255,7 +266,7 @@ func (p *processor) notifyAnnounce(status *gtsmodel.Status) error { // make sure a notif doesn't already exist for this announce err := p.db.GetWhere([]db.Where{ {Key: "notification_type", Value: gtsmodel.NotificationReblog}, - {Key: "target_account_id", Value: boostedAcct.ID}, + {Key: "target_account_id", Value: status.BoostOfAccountID}, {Key: "origin_account_id", Value: status.AccountID}, {Key: "status_id", Value: status.ID}, }, >smodel.Notification{}) @@ -273,9 +284,12 @@ func (p *processor) notifyAnnounce(status *gtsmodel.Status) error { notif := >smodel.Notification{ ID: notifID, NotificationType: gtsmodel.NotificationReblog, - TargetAccountID: boostedAcct.ID, + TargetAccountID: status.BoostOfAccountID, + TargetAccount: status.BoostOfAccount, OriginAccountID: status.AccountID, + OriginAccount: status.Account, StatusID: status.ID, + Status: status, } if err := p.db.Put(notif); err != nil { @@ -288,7 +302,7 @@ func (p *processor) notifyAnnounce(status *gtsmodel.Status) error { return fmt.Errorf("notifyStatus: error converting notification to masto representation: %s", err) } - if err := p.streamingProcessor.StreamNotificationToAccount(mastoNotif, boostedAcct); err != nil { + if err := p.streamingProcessor.StreamNotificationToAccount(mastoNotif, status.BoostOfAccount); err != nil { return fmt.Errorf("notifyStatus: error streaming notification to account: %s", err) } @@ -297,32 +311,33 @@ func (p *processor) notifyAnnounce(status *gtsmodel.Status) error { func (p *processor) timelineStatus(status *gtsmodel.Status) error { // make sure the author account is pinned onto the status - if status.GTSAuthorAccount == nil { - a := >smodel.Account{} - if err := p.db.GetByID(status.AccountID, a); err != nil { + if status.Account == nil { + a, err := p.db.GetAccountByID(status.AccountID) + if err != nil { return fmt.Errorf("timelineStatus: error getting author account with id %s: %s", status.AccountID, err) } - status.GTSAuthorAccount = a + status.Account = a } // get local followers of the account that posted the status - followers := []gtsmodel.Follow{} - if err := p.db.GetFollowersByAccountID(status.AccountID, &followers, true); err != nil { + follows, err := p.db.GetAccountFollowedBy(status.AccountID, true) + if err != nil { return fmt.Errorf("timelineStatus: error getting followers for account id %s: %s", status.AccountID, err) } // if the poster is local, add a fake entry for them to the followers list so they can see their own status in their timeline - if status.GTSAuthorAccount.Domain == "" { - followers = append(followers, gtsmodel.Follow{ + if status.Account.Domain == "" { + follows = append(follows, >smodel.Follow{ AccountID: status.AccountID, + Account: status.Account, }) } wg := sync.WaitGroup{} - wg.Add(len(followers)) - errors := make(chan error, len(followers)) + wg.Add(len(follows)) + errors := make(chan error, len(follows)) - for _, f := range followers { + for _, f := range follows { go p.timelineStatusForAccount(status, f.AccountID, errors, &wg) } @@ -354,8 +369,8 @@ func (p *processor) timelineStatusForAccount(status *gtsmodel.Status, accountID defer wg.Done() // get the timeline owner account - timelineAccount := >smodel.Account{} - if err := p.db.GetByID(accountID, timelineAccount); err != nil { + timelineAccount, err := p.db.GetAccountByID(accountID) + if err != nil { errors <- fmt.Errorf("timelineStatusForAccount: error getting account for timeline with id %s: %s", accountID, err) return } diff --git a/internal/processing/fromfederator.go b/internal/processing/fromfederator.go index 949a734c7..c95c27778 100644 --- a/internal/processing/fromfederator.go +++ b/internal/processing/fromfederator.go @@ -100,8 +100,8 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er } incomingAnnounce.ID = incomingAnnounceID - if err := p.db.Put(incomingAnnounce); err != nil { - if _, ok := err.(db.ErrAlreadyExists); !ok { + if err := p.db.PutStatus(incomingAnnounce); err != nil { + if err != db.ErrNoEntries { return fmt.Errorf("error adding dereferenced announce to the db: %s", err) } } @@ -164,14 +164,14 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er } // delete all attachments for this status - for _, a := range statusToDelete.Attachments { + for _, a := range statusToDelete.AttachmentIDs { if err := p.mediaProcessor.Delete(a); err != nil { return err } } // delete all mentions for this status - for _, m := range statusToDelete.Mentions { + for _, m := range statusToDelete.MentionIDs { if err := p.db.DeleteByID(m, >smodel.Mention{}); err != nil { return err } diff --git a/internal/processing/instance.go b/internal/processing/instance.go index 89f60f5d4..b151744ef 100644 --- a/internal/processing/instance.go +++ b/internal/processing/instance.go @@ -51,8 +51,8 @@ func (p *processor) InstancePatch(form *apimodel.InstanceSettingsUpdateRequest) } // fetch the instance account from the db for processing - ia := >smodel.Account{} - if err := p.db.GetLocalAccountByUsername(p.config.Host, ia); err != nil { + ia, err := p.db.GetInstanceAccount("") + if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error fetching instance account %s: %s", p.config.Host, err)) } @@ -67,8 +67,8 @@ func (p *processor) InstancePatch(form *apimodel.InstanceSettingsUpdateRequest) // validate & update site contact account if it's set on the form if form.ContactUsername != nil { // make sure the account with the given username exists in the db - contactAccount := >smodel.Account{} - if err := p.db.GetLocalAccountByUsername(*form.ContactUsername, contactAccount); err != nil { + contactAccount, err := p.db.GetLocalAccountByUsername(*form.ContactUsername) + if err != nil { return nil, gtserror.NewErrorBadRequest(err, fmt.Sprintf("account with username %s not retrievable", *form.ContactUsername)) } // make sure it has a user associated with it diff --git a/internal/processing/media/delete.go b/internal/processing/media/delete.go index 694d78ac3..b5ea8c806 100644 --- a/internal/processing/media/delete.go +++ b/internal/processing/media/delete.go @@ -12,7 +12,7 @@ import ( func (p *processor) Delete(mediaAttachmentID string) gtserror.WithCode { a := >smodel.MediaAttachment{} if err := p.db.GetByID(mediaAttachmentID, a); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + if err == db.ErrNoEntries { // attachment already gone return nil } @@ -38,7 +38,7 @@ func (p *processor) Delete(mediaAttachmentID string) gtserror.WithCode { // delete the attachment if err := p.db.DeleteByID(mediaAttachmentID, a); err != nil { - if _, ok := err.(db.ErrNoEntries); !ok { + if err != db.ErrNoEntries { errs = append(errs, fmt.Sprintf("remove attachment: %s", err)) } } diff --git a/internal/processing/media/getfile.go b/internal/processing/media/getfile.go index 1664306b8..01288c56d 100644 --- a/internal/processing/media/getfile.go +++ b/internal/processing/media/getfile.go @@ -57,7 +57,7 @@ func (p *processor) GetFile(account *gtsmodel.Account, form *apimodel.GetContent // make sure the requesting account and the media account don't block each other if account != nil { - blocked, err := p.db.Blocked(account.ID, form.AccountID) + blocked, err := p.db.IsBlocked(account.ID, form.AccountID, true) if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("block status could not be established between accounts %s and %s: %s", form.AccountID, account.ID, err)) } diff --git a/internal/processing/media/getmedia.go b/internal/processing/media/getmedia.go index c36370225..380a54cc2 100644 --- a/internal/processing/media/getmedia.go +++ b/internal/processing/media/getmedia.go @@ -31,7 +31,7 @@ import ( func (p *processor) GetMedia(account *gtsmodel.Account, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) { attachment := >smodel.MediaAttachment{} if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + if err == db.ErrNoEntries { // attachment doesn't exist return nil, gtserror.NewErrorNotFound(errors.New("attachment doesn't exist in the db")) } diff --git a/internal/processing/media/update.go b/internal/processing/media/update.go index 28f3a26f6..89ed08ac1 100644 --- a/internal/processing/media/update.go +++ b/internal/processing/media/update.go @@ -32,7 +32,7 @@ import ( func (p *processor) Update(account *gtsmodel.Account, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) { attachment := >smodel.MediaAttachment{} if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + if err == db.ErrNoEntries { // attachment doesn't exist return nil, gtserror.NewErrorNotFound(errors.New("attachment doesn't exist in the db")) } diff --git a/internal/processing/notification.go b/internal/processing/notification.go index 6ad974126..7af74b04f 100644 --- a/internal/processing/notification.go +++ b/internal/processing/notification.go @@ -27,7 +27,7 @@ import ( func (p *processor) NotificationsGet(authed *oauth.Auth, limit int, maxID string, sinceID string) ([]*apimodel.Notification, gtserror.WithCode) { l := p.log.WithField("func", "NotificationsGet") - notifs, err := p.db.GetNotificationsForAccount(authed.Account.ID, limit, maxID, sinceID) + notifs, err := p.db.GetNotifications(authed.Account.ID, limit, maxID, sinceID) if err != nil { return nil, gtserror.NewErrorInternalError(err) } diff --git a/internal/processing/search.go b/internal/processing/search.go index 737ad8f71..f2ae721ae 100644 --- a/internal/processing/search.go +++ b/internal/processing/search.go @@ -90,7 +90,7 @@ func (p *processor) SearchGet(authed *oauth.Auth, searchQuery *apimodel.SearchQu */ 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 { + if blocked, err := p.db.IsBlocked(authed.Account.ID, foundAccount.ID, true); 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) @@ -99,11 +99,6 @@ func (p *processor) SearchGet(authed *oauth.Auth, searchQuery *apimodel.SearchQu } for _, foundStatus := range foundStatuses { - statusOwner := >smodel.Account{} - if err := p.db.GetByID(foundStatus.AccountID, statusOwner); err != nil { - continue - } - if visible, err := p.filter.StatusVisible(foundStatus, authed.Account); !visible || err != nil { continue } @@ -126,12 +121,9 @@ func (p *processor) searchStatusByURI(authed *oauth.Auth, uri *url.URL, resolve "resolve": resolve, }) - 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 + if maybeStatus, err := p.db.GetStatusByURI(uri.String()); err == nil { 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 + } else if maybeStatus, err := p.db.GetStatusByURL(uri.String()); err == nil { return maybeStatus, nil } @@ -150,14 +142,12 @@ func (p *processor) searchStatusByURI(authed *oauth.Auth, uri *url.URL, resolve } 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 + if maybeAccount, err := p.db.GetAccountByURI(uri.String()); err == nil { 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 + } else if maybeAccount, err := p.db.GetAccountByURL(uri.String()); err == nil { return maybeAccount, nil } + if resolve { // we don't have it locally so try and dereference it account, _, err := p.federator.GetRemoteAccount(authed.Account.Username, uri, true) @@ -179,7 +169,8 @@ func (p *processor) searchAccountByMention(authed *oauth.Auth, mention string, r // 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 { + maybeAcct, err = p.db.GetLocalAccountByUsername(username) + if err != nil { return nil, fmt.Errorf("searchAccountByMention: error getting local account by username: %s", err) } return maybeAcct, nil @@ -196,7 +187,7 @@ func (p *processor) searchAccountByMention(authed *oauth.Auth, mention string, r return maybeAcct, nil } - if _, ok := err.(db.ErrNoEntries); !ok { + if err != db.ErrNoEntries { // 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) } diff --git a/internal/processing/status/boost.go b/internal/processing/status/boost.go index 93d0f19de..d7a62beb1 100644 --- a/internal/processing/status/boost.go +++ b/internal/processing/status/boost.go @@ -9,31 +9,22 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) -func (p *processor) Boost(account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { - 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 { +func (p *processor) Boost(requestingAccount *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { + targetStatus, err := p.db.GetStatusByID(targetStatusID) + if err != nil { return nil, gtserror.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, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)) + if targetStatus.Account == nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID)) } - l.Trace("going to see if status is visible") - visible, err := p.filter.StatusVisible(targetStatus, account) + visible, err := p.filter.StatusVisible(targetStatus, requestingAccount) if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) } - if !visible { return nil, gtserror.NewErrorNotFound(errors.New("status is not visible")) } - if targetStatus.VisibilityAdvanced != nil { if !targetStatus.VisibilityAdvanced.Boostable { return nil, gtserror.NewErrorForbidden(errors.New("status is not boostable")) @@ -41,16 +32,16 @@ func (p *processor) Boost(account *gtsmodel.Account, application *gtsmodel.Appli } // it's visible! it's boostable! so let's boost the FUCK out of it - boostWrapperStatus, err := p.tc.StatusToBoost(targetStatus, account) + boostWrapperStatus, err := p.tc.StatusToBoost(targetStatus, requestingAccount) if err != nil { return nil, gtserror.NewErrorInternalError(err) } boostWrapperStatus.CreatedWithApplicationID = application.ID - boostWrapperStatus.GTSBoostedAccount = targetAccount + boostWrapperStatus.BoostOfAccount = targetStatus.Account // put the boost in the database - if err := p.db.Put(boostWrapperStatus); err != nil { + if err := p.db.PutStatus(boostWrapperStatus); err != nil { return nil, gtserror.NewErrorInternalError(err) } @@ -59,12 +50,12 @@ func (p *processor) Boost(account *gtsmodel.Account, application *gtsmodel.Appli APObjectType: gtsmodel.ActivityStreamsAnnounce, APActivityType: gtsmodel.ActivityStreamsCreate, GTSModel: boostWrapperStatus, - OriginAccount: account, - TargetAccount: targetAccount, + OriginAccount: requestingAccount, + TargetAccount: targetStatus.Account, } // return the frontend representation of the new status to the submitter - mastoStatus, err := p.tc.StatusToMasto(boostWrapperStatus, account) + mastoStatus, err := p.tc.StatusToMasto(boostWrapperStatus, requestingAccount) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)) } diff --git a/internal/processing/status/boostedby.go b/internal/processing/status/boostedby.go index b352178e3..1bde6b5ae 100644 --- a/internal/processing/status/boostedby.go +++ b/internal/processing/status/boostedby.go @@ -9,46 +9,37 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) -func (p *processor) BoostedBy(account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) { - l := p.log.WithField("func", "StatusBoostedBy") - - l.Tracef("going to search for target status %s", targetStatusID) - targetStatus := >smodel.Status{} - if err := p.db.GetByID(targetStatusID, targetStatus); err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: 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, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error fetching target account %s: %s", targetStatus.AccountID, err)) - } - - l.Trace("going to see if status is visible") - visible, err := p.filter.StatusVisible(targetStatus, account) +func (p *processor) BoostedBy(requestingAccount *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) { + targetStatus, err := p.db.GetStatusByID(targetStatusID) if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error seeing if status %s is visible: %s", targetStatus.ID, err)) + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err)) + } + if targetStatus.Account == nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID)) } + visible, err := p.filter.StatusVisible(targetStatus, requestingAccount) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) + } if !visible { - return nil, gtserror.NewErrorNotFound(errors.New("StatusBoostedBy: status is not visible")) + return nil, gtserror.NewErrorNotFound(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.WhoBoostedStatus(targetStatus) + statusReblogs, err := p.db.GetStatusReblogs(targetStatus) if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error seeing who boosted 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(account.ID, acc.ID) + for _, s := range statusReblogs { + blocked, err := p.db.IsBlocked(requestingAccount.ID, s.AccountID, true) if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error checking blocks: %s", err)) } if !blocked { - filteredAccounts = append(filteredAccounts, acc) + filteredAccounts = append(filteredAccounts, s.Account) } } diff --git a/internal/processing/status/context.go b/internal/processing/status/context.go index 32c528296..43002545e 100644 --- a/internal/processing/status/context.go +++ b/internal/processing/status/context.go @@ -1,46 +1,45 @@ package status import ( + "errors" "fmt" "sort" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) -func (p *processor) Context(account *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode) { +func (p *processor) Context(requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode) { + targetStatus, err := p.db.GetStatusByID(targetStatusID) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err)) + } + if targetStatus.Account == nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID)) + } + + visible, err := p.filter.StatusVisible(targetStatus, requestingAccount) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) + } + if !visible { + return nil, gtserror.NewErrorNotFound(errors.New("status is not visible")) + } context := &apimodel.Context{ Ancestors: []apimodel.Status{}, Descendants: []apimodel.Status{}, } - targetStatus := >smodel.Status{} - if err := p.db.GetByID(targetStatusID, targetStatus); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { - return nil, gtserror.NewErrorNotFound(err) - } - return nil, gtserror.NewErrorInternalError(err) - } - - visible, err := p.filter.StatusVisible(targetStatus, account) - if err != nil { - return nil, gtserror.NewErrorNotFound(err) - } - if !visible { - return nil, gtserror.NewErrorForbidden(fmt.Errorf("account with id %s does not have permission to view status %s", account.ID, targetStatusID)) - } - - parents, err := p.db.StatusParents(targetStatus, false) + parents, err := p.db.GetStatusParents(targetStatus, false) if err != nil { return nil, gtserror.NewErrorInternalError(err) } for _, status := range parents { - if v, err := p.filter.StatusVisible(status, account); err == nil && v { - mastoStatus, err := p.tc.StatusToMasto(status, account) + if v, err := p.filter.StatusVisible(status, requestingAccount); err == nil && v { + mastoStatus, err := p.tc.StatusToMasto(status, requestingAccount) if err == nil { context.Ancestors = append(context.Ancestors, *mastoStatus) } @@ -51,14 +50,14 @@ func (p *processor) Context(account *gtsmodel.Account, targetStatusID string) (* return context.Ancestors[i].ID < context.Ancestors[j].ID }) - children, err := p.db.StatusChildren(targetStatus, false, "") + children, err := p.db.GetStatusChildren(targetStatus, false, "") if err != nil { return nil, gtserror.NewErrorInternalError(err) } for _, status := range children { - if v, err := p.filter.StatusVisible(status, account); err == nil && v { - mastoStatus, err := p.tc.StatusToMasto(status, account) + if v, err := p.filter.StatusVisible(status, requestingAccount); err == nil && v { + mastoStatus, err := p.tc.StatusToMasto(status, requestingAccount) if err == nil { context.Descendants = append(context.Descendants, *mastoStatus) } diff --git a/internal/processing/status/create.go b/internal/processing/status/create.go index 0e99b5f4a..fc112ed8b 100644 --- a/internal/processing/status/create.go +++ b/internal/processing/status/create.go @@ -38,27 +38,22 @@ func (p *processor) Create(account *gtsmodel.Account, application *gtsmodel.Appl Text: form.Status, } - // check if replyToID is ok if err := p.ProcessReplyToID(form, account.ID, newStatus); err != nil { return nil, gtserror.NewErrorInternalError(err) } - // check if mediaIDs are ok if err := p.ProcessMediaIDs(form, account.ID, newStatus); err != nil { return nil, gtserror.NewErrorInternalError(err) } - // check if visibility settings are ok if err := p.ProcessVisibility(form, account.Privacy, newStatus); err != nil { return nil, gtserror.NewErrorInternalError(err) } - // handle language settings if err := p.ProcessLanguage(form, account.Language, newStatus); err != nil { return nil, gtserror.NewErrorInternalError(err) } - // handle mentions if err := p.ProcessMentions(form, account.ID, newStatus); err != nil { return nil, gtserror.NewErrorInternalError(err) } @@ -75,20 +70,11 @@ func (p *processor) Create(account *gtsmodel.Account, application *gtsmodel.Appl return nil, gtserror.NewErrorInternalError(err) } - // put the new status in the database, generating an ID for it in the process - if err := p.db.Put(newStatus); err != nil { + // put the new status in the database + if err := p.db.PutStatus(newStatus); err != nil { return nil, gtserror.NewErrorInternalError(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, gtserror.NewErrorInternalError(err) - } - } - // send it back to the processor for async processing p.fromClientAPI <- gtsmodel.FromClientAPI{ APObjectType: gtsmodel.ActivityStreamsNote, diff --git a/internal/processing/status/delete.go b/internal/processing/status/delete.go index 259038dee..4c5dfd744 100644 --- a/internal/processing/status/delete.go +++ b/internal/processing/status/delete.go @@ -5,36 +5,24 @@ import ( "fmt" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) -func (p *processor) Delete(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { - 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 { - if _, ok := err.(db.ErrNoEntries); !ok { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err)) - } - // status is already gone - return nil, nil +func (p *processor) Delete(requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { + targetStatus, err := p.db.GetStatusByID(targetStatusID) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err)) + } + if targetStatus.Account == nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID)) } - if targetStatus.AccountID != account.ID { + if targetStatus.AccountID != requestingAccount.ID { return nil, gtserror.NewErrorForbidden(errors.New("status doesn't belong to requesting account")) } - var boostOfStatus *gtsmodel.Status - if targetStatus.BoostOfID != "" { - boostOfStatus = >smodel.Status{} - if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)) - } - } - - mastoStatus, err := p.tc.StatusToMasto(targetStatus, account) + mastoStatus, err := p.tc.StatusToMasto(targetStatus, requestingAccount) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)) } @@ -48,8 +36,8 @@ func (p *processor) Delete(account *gtsmodel.Account, targetStatusID string) (*a APObjectType: gtsmodel.ActivityStreamsNote, APActivityType: gtsmodel.ActivityStreamsDelete, GTSModel: targetStatus, - OriginAccount: account, - TargetAccount: account, + OriginAccount: requestingAccount, + TargetAccount: requestingAccount, } return mastoStatus, nil diff --git a/internal/processing/status/fave.go b/internal/processing/status/fave.go index 0dfee6233..7ba8c8fe8 100644 --- a/internal/processing/status/fave.go +++ b/internal/processing/status/fave.go @@ -12,39 +12,22 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/util" ) -func (p *processor) Fave(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { - 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 { +func (p *processor) Fave(requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { + targetStatus, err := p.db.GetStatusByID(targetStatusID) + if err != nil { return nil, gtserror.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, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)) + if targetStatus.Account == nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID)) } - var boostOfStatus *gtsmodel.Status - if targetStatus.BoostOfID != "" { - boostOfStatus = >smodel.Status{} - if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)) - } - } - - l.Trace("going to see if status is visible") - visible, err := p.filter.StatusVisible(targetStatus, account) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that + visible, err := p.filter.StatusVisible(targetStatus, requestingAccount) if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) } - if !visible { return nil, gtserror.NewErrorNotFound(errors.New("status is not visible")) } - - // is the status faveable? if targetStatus.VisibilityAdvanced != nil { if !targetStatus.VisibilityAdvanced.Likeable { return nil, gtserror.NewErrorForbidden(errors.New("status is not faveable")) @@ -54,7 +37,7 @@ func (p *processor) Fave(account *gtsmodel.Account, targetStatusID string) (*api // first check if the status is already faved, if so we don't need to do anything newFave := true gtsFave := >smodel.StatusFave{} - if err := p.db.GetWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: account.ID}}, gtsFave); err == nil { + if err := p.db.GetWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsFave); err == nil { // we already have a fave for this status newFave = false } @@ -67,14 +50,14 @@ func (p *processor) Fave(account *gtsmodel.Account, targetStatusID string) (*api // we need to create a new fave in the database gtsFave := >smodel.StatusFave{ - ID: thisFaveID, - AccountID: account.ID, - TargetAccountID: targetAccount.ID, - StatusID: targetStatus.ID, - URI: util.GenerateURIForLike(account.Username, p.config.Protocol, p.config.Host, thisFaveID), - GTSStatus: targetStatus, - GTSTargetAccount: targetAccount, - GTSFavingAccount: account, + ID: thisFaveID, + AccountID: requestingAccount.ID, + Account: requestingAccount, + TargetAccountID: targetStatus.AccountID, + TargetAccount: targetStatus.Account, + StatusID: targetStatus.ID, + Status: targetStatus, + URI: util.GenerateURIForLike(requestingAccount.Username, p.config.Protocol, p.config.Host, thisFaveID), } if err := p.db.Put(gtsFave); err != nil { @@ -86,13 +69,13 @@ func (p *processor) Fave(account *gtsmodel.Account, targetStatusID string) (*api APObjectType: gtsmodel.ActivityStreamsLike, APActivityType: gtsmodel.ActivityStreamsCreate, GTSModel: gtsFave, - OriginAccount: account, - TargetAccount: targetAccount, + OriginAccount: requestingAccount, + TargetAccount: targetStatus.Account, } } // return the mastodon representation of the target status - mastoStatus, err := p.tc.StatusToMasto(targetStatus, account) + mastoStatus, err := p.tc.StatusToMasto(targetStatus, requestingAccount) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)) } diff --git a/internal/processing/status/favedby.go b/internal/processing/status/favedby.go index 5194cc258..dffe6bba9 100644 --- a/internal/processing/status/favedby.go +++ b/internal/processing/status/favedby.go @@ -9,51 +9,40 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) -func (p *processor) FavedBy(account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) { - 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 { +func (p *processor) FavedBy(requestingAccount *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) { + targetStatus, err := p.db.GetStatusByID(targetStatusID) + if err != nil { return nil, gtserror.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, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)) + if targetStatus.Account == nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID)) } - l.Trace("going to see if status is visible") - visible, err := p.filter.StatusVisible(targetStatus, account) + visible, err := p.filter.StatusVisible(targetStatus, requestingAccount) if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) } - if !visible { return nil, gtserror.NewErrorNotFound(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) + statusFaves, err := p.db.GetStatusFaves(targetStatus) if err != nil { return nil, gtserror.NewErrorNotFound(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(account.ID, acc.ID) + for _, fave := range statusFaves { + blocked, err := p.db.IsBlocked(requestingAccount.ID, fave.AccountID, true) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking blocks: %s", err)) } if !blocked { - filteredAccounts = append(filteredAccounts, acc) + filteredAccounts = append(filteredAccounts, fave.Account) } } - // 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 { diff --git a/internal/processing/status/get.go b/internal/processing/status/get.go index 9a70185b0..9d403b901 100644 --- a/internal/processing/status/get.go +++ b/internal/processing/status/get.go @@ -9,44 +9,27 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) -func (p *processor) Get(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { - 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 { +func (p *processor) Get(requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { + targetStatus, err := p.db.GetStatusByID(targetStatusID) + if err != nil { return nil, gtserror.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, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)) + if targetStatus.Account == nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID)) } - l.Trace("going to see if status is visible") - visible, err := p.filter.StatusVisible(targetStatus, account) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that + visible, err := p.filter.StatusVisible(targetStatus, requestingAccount) if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) } - if !visible { return nil, gtserror.NewErrorNotFound(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, gtserror.NewErrorInternalError(fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)) - } - } - - mastoStatus, err := p.tc.StatusToMasto(targetStatus, account) + mastoStatus, err := p.tc.StatusToMasto(targetStatus, requestingAccount) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)) } return mastoStatus, nil - } diff --git a/internal/processing/status/unboost.go b/internal/processing/status/unboost.go index 2a1394695..254cfe11f 100644 --- a/internal/processing/status/unboost.go +++ b/internal/processing/status/unboost.go @@ -10,27 +10,19 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) -func (p *processor) Unboost(account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { - l := p.log.WithField("func", "Unboost") - - l.Tracef("going to search for target status %s", targetStatusID) - targetStatus := >smodel.Status{} - if err := p.db.GetByID(targetStatusID, targetStatus); err != nil { +func (p *processor) Unboost(requestingAccount *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { + targetStatus, err := p.db.GetStatusByID(targetStatusID) + if err != nil { return nil, gtserror.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, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)) + if targetStatus.Account == nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID)) } - l.Trace("going to see if status is visible") - visible, err := p.filter.StatusVisible(targetStatus, account) + visible, err := p.filter.StatusVisible(targetStatus, requestingAccount) if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) } - if !visible { return nil, gtserror.NewErrorNotFound(errors.New("status is not visible")) } @@ -46,7 +38,7 @@ func (p *processor) Unboost(account *gtsmodel.Account, application *gtsmodel.App }, { Key: "account_id", - Value: account.ID, + Value: requestingAccount.ID, }, } err = p.db.GetWhere(where, gtsBoost) @@ -57,7 +49,7 @@ func (p *processor) Unboost(account *gtsmodel.Account, application *gtsmodel.App if err != nil { // something went wrong in the db finding the boost - if _, ok := err.(db.ErrNoEntries); !ok { + if err != db.ErrNoEntries { return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing boost from database: %s", err)) } // we just don't have a boost @@ -71,22 +63,23 @@ func (p *processor) Unboost(account *gtsmodel.Account, application *gtsmodel.App } // pin some stuff onto the boost while we have it out of the db - gtsBoost.GTSBoostedStatus = targetStatus - gtsBoost.GTSBoostedStatus.GTSAuthorAccount = targetAccount - gtsBoost.GTSBoostedAccount = targetAccount - gtsBoost.GTSAuthorAccount = account + gtsBoost.Account = requestingAccount + + gtsBoost.BoostOf = targetStatus + gtsBoost.BoostOfAccount = targetStatus.Account + gtsBoost.BoostOf.Account = targetStatus.Account // send it back to the processor for async processing p.fromClientAPI <- gtsmodel.FromClientAPI{ APObjectType: gtsmodel.ActivityStreamsAnnounce, APActivityType: gtsmodel.ActivityStreamsUndo, GTSModel: gtsBoost, - OriginAccount: account, - TargetAccount: targetAccount, + OriginAccount: requestingAccount, + TargetAccount: targetStatus.Account, } } - mastoStatus, err := p.tc.StatusToMasto(targetStatus, account) + mastoStatus, err := p.tc.StatusToMasto(targetStatus, requestingAccount) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)) } diff --git a/internal/processing/status/unfave.go b/internal/processing/status/unfave.go index b51daacb9..d6e5320db 100644 --- a/internal/processing/status/unfave.go +++ b/internal/processing/status/unfave.go @@ -10,26 +10,19 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) -func (p *processor) Unfave(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { - 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 { +func (p *processor) Unfave(requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { + targetStatus, err := p.db.GetStatusByID(targetStatusID) + if err != nil { return nil, gtserror.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, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)) + if targetStatus.Account == nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID)) } - l.Trace("going to see if status is visible") - visible, err := p.filter.StatusVisible(targetStatus, account) + visible, err := p.filter.StatusVisible(targetStatus, requestingAccount) if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) } - if !visible { return nil, gtserror.NewErrorNotFound(errors.New("status is not visible")) } @@ -38,14 +31,14 @@ func (p *processor) Unfave(account *gtsmodel.Account, targetStatusID string) (*a var toUnfave bool gtsFave := >smodel.StatusFave{} - err = p.db.GetWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: account.ID}}, gtsFave) + err = p.db.GetWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsFave) if err == nil { // we have a fave toUnfave = true } if err != nil { // something went wrong in the db finding the fave - if _, ok := err.(db.ErrNoEntries); !ok { + if err != db.ErrNoEntries { return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing fave from database: %s", err)) } // we just don't have a fave @@ -54,7 +47,7 @@ func (p *processor) Unfave(account *gtsmodel.Account, targetStatusID string) (*a if toUnfave { // we had a fave, so take some action to get rid of it - if err := p.db.DeleteWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: account.ID}}, gtsFave); err != nil { + if err := p.db.DeleteWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsFave); err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unfaveing status: %s", err)) } @@ -63,12 +56,12 @@ func (p *processor) Unfave(account *gtsmodel.Account, targetStatusID string) (*a APObjectType: gtsmodel.ActivityStreamsLike, APActivityType: gtsmodel.ActivityStreamsUndo, GTSModel: gtsFave, - OriginAccount: account, - TargetAccount: targetAccount, + OriginAccount: requestingAccount, + TargetAccount: targetStatus.Account, } } - mastoStatus, err := p.tc.StatusToMasto(targetStatus, account) + mastoStatus, err := p.tc.StatusToMasto(targetStatus, requestingAccount) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)) } diff --git a/internal/processing/status/util.go b/internal/processing/status/util.go index 3be53591b..025607f4a 100644 --- a/internal/processing/status/util.go +++ b/internal/processing/status/util.go @@ -99,7 +99,7 @@ func (p *processor) ProcessReplyToID(form *apimodel.AdvancedStatusCreateForm, th 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 { + if err == db.ErrNoEntries { 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) @@ -113,14 +113,14 @@ func (p *processor) ProcessReplyToID(form *apimodel.AdvancedStatusCreateForm, th // check replied account is known to us if err := p.db.GetByID(repliedStatus.AccountID, repliedAccount); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + if err == db.ErrNoEntries { 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 { + if blocked, err := p.db.IsBlocked(thisAccountID, repliedAccount.ID, true); err != nil { + if err != db.ErrNoEntries { return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err) } } else if blocked { @@ -156,8 +156,8 @@ func (p *processor) ProcessMediaIDs(form *apimodel.AdvancedStatusCreateForm, thi gtsMediaAttachments = append(gtsMediaAttachments, a) attachments = append(attachments, a.ID) } - status.GTSMediaAttachments = gtsMediaAttachments - status.Attachments = attachments + status.Attachments = gtsMediaAttachments + status.AttachmentIDs = attachments return nil } @@ -192,9 +192,9 @@ func (p *processor) ProcessMentions(form *apimodel.AdvancedStatusCreateForm, acc menchies = append(menchies, menchie.ID) } // add full populated gts menchies to the status for passing them around conveniently - status.GTSMentions = gtsMenchies + status.Mentions = gtsMenchies // add just the ids of the mentioned accounts to the status for putting in the db - status.Mentions = menchies + status.MentionIDs = menchies return nil } @@ -211,9 +211,9 @@ func (p *processor) ProcessTags(form *apimodel.AdvancedStatusCreateForm, account tags = append(tags, tag.ID) } // add full populated gts tags to the status for passing them around conveniently - status.GTSTags = gtsTags + status.Tags = gtsTags // add just the ids of the used tags to the status for putting in the db - status.Tags = tags + status.TagIDs = tags return nil } @@ -227,9 +227,9 @@ func (p *processor) ProcessEmojis(form *apimodel.AdvancedStatusCreateForm, accou emojis = append(emojis, e.ID) } // add full populated gts emojis to the status for passing them around conveniently - status.GTSEmojis = gtsEmojis + status.Emojis = gtsEmojis // add just the ids of the used emojis to the status for putting in the db - status.Emojis = emojis + status.EmojiIDs = emojis return nil } @@ -252,9 +252,9 @@ func (p *processor) ProcessContent(form *apimodel.AdvancedStatusCreateForm, acco var formatted string switch form.Format { case apimodel.StatusFormatPlain: - formatted = p.formatter.FromPlain(content, status.GTSMentions, status.GTSTags) + formatted = p.formatter.FromPlain(content, status.Mentions, status.Tags) case apimodel.StatusFormatMarkdown: - formatted = p.formatter.FromMarkdown(content, status.GTSMentions, status.GTSTags) + formatted = p.formatter.FromMarkdown(content, status.Mentions, status.Tags) default: return fmt.Errorf("format %s not recognised as a valid status format", form.Format) } diff --git a/internal/processing/status/util_test.go b/internal/processing/status/util_test.go index 4bf508848..9c282eb52 100644 --- a/internal/processing/status/util_test.go +++ b/internal/processing/status/util_test.go @@ -91,19 +91,19 @@ func (suite *UtilTestSuite) TestProcessMentions1() { err := suite.status.ProcessMentions(form, creatingAccount.ID, status) assert.NoError(suite.T(), err) - assert.Len(suite.T(), status.GTSMentions, 1) - newMention := status.GTSMentions[0] + assert.Len(suite.T(), status.Mentions, 1) + newMention := status.Mentions[0] assert.Equal(suite.T(), mentionedAccount.ID, newMention.TargetAccountID) assert.Equal(suite.T(), creatingAccount.ID, newMention.OriginAccountID) assert.Equal(suite.T(), creatingAccount.URI, newMention.OriginAccountURI) assert.Equal(suite.T(), status.ID, newMention.StatusID) assert.Equal(suite.T(), fmt.Sprintf("@%s@%s", mentionedAccount.Username, mentionedAccount.Domain), newMention.NameString) - assert.Equal(suite.T(), mentionedAccount.URI, newMention.MentionedAccountURI) - assert.Equal(suite.T(), mentionedAccount.URL, newMention.MentionedAccountURL) - assert.NotNil(suite.T(), newMention.GTSAccount) + assert.Equal(suite.T(), mentionedAccount.URI, newMention.TargetAccountURI) + assert.Equal(suite.T(), mentionedAccount.URL, newMention.TargetAccountURL) + assert.NotNil(suite.T(), newMention.OriginAccount) - assert.Len(suite.T(), status.Mentions, 1) - assert.Equal(suite.T(), newMention.ID, status.Mentions[0]) + assert.Len(suite.T(), status.MentionIDs, 1) + assert.Equal(suite.T(), newMention.ID, status.MentionIDs[0]) } func (suite *UtilTestSuite) TestProcessContentFull1() { @@ -232,19 +232,19 @@ func (suite *UtilTestSuite) TestProcessMentions2() { err := suite.status.ProcessMentions(form, creatingAccount.ID, status) assert.NoError(suite.T(), err) - assert.Len(suite.T(), status.GTSMentions, 1) - newMention := status.GTSMentions[0] + assert.Len(suite.T(), status.Mentions, 1) + newMention := status.Mentions[0] assert.Equal(suite.T(), mentionedAccount.ID, newMention.TargetAccountID) assert.Equal(suite.T(), creatingAccount.ID, newMention.OriginAccountID) assert.Equal(suite.T(), creatingAccount.URI, newMention.OriginAccountURI) assert.Equal(suite.T(), status.ID, newMention.StatusID) assert.Equal(suite.T(), fmt.Sprintf("@%s@%s", mentionedAccount.Username, mentionedAccount.Domain), newMention.NameString) - assert.Equal(suite.T(), mentionedAccount.URI, newMention.MentionedAccountURI) - assert.Equal(suite.T(), mentionedAccount.URL, newMention.MentionedAccountURL) - assert.NotNil(suite.T(), newMention.GTSAccount) + assert.Equal(suite.T(), mentionedAccount.URI, newMention.TargetAccountURI) + assert.Equal(suite.T(), mentionedAccount.URL, newMention.TargetAccountURL) + assert.NotNil(suite.T(), newMention.OriginAccount) - assert.Len(suite.T(), status.Mentions, 1) - assert.Equal(suite.T(), newMention.ID, status.Mentions[0]) + assert.Len(suite.T(), status.MentionIDs, 1) + assert.Equal(suite.T(), newMention.ID, status.MentionIDs[0]) } func (suite *UtilTestSuite) TestProcessContentFull2() { diff --git a/internal/processing/streaming.go b/internal/processing/streaming.go index 1e566da81..457db0576 100644 --- a/internal/processing/streaming.go +++ b/internal/processing/streaming.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package processing import ( diff --git a/internal/processing/timeline.go b/internal/processing/timeline.go index 18d0a6ac7..afddd3e6c 100644 --- a/internal/processing/timeline.go +++ b/internal/processing/timeline.go @@ -74,9 +74,9 @@ func (p *processor) HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID st } func (p *processor) PublicTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.StatusTimelineResponse, gtserror.WithCode) { - statuses, err := p.db.GetPublicTimelineForAccount(authed.Account.ID, maxID, sinceID, minID, limit, local) + statuses, err := p.db.GetPublicTimeline(authed.Account.ID, maxID, sinceID, minID, limit, local) if err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + if err == db.ErrNoEntries { // there are just no entries left return &apimodel.StatusTimelineResponse{ Statuses: []*apimodel.Status{}, @@ -95,9 +95,9 @@ func (p *processor) PublicTimelineGet(authed *oauth.Auth, maxID string, sinceID } func (p *processor) FavedTimelineGet(authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.StatusTimelineResponse, gtserror.WithCode) { - statuses, nextMaxID, prevMinID, err := p.db.GetFavedTimelineForAccount(authed.Account.ID, maxID, minID, limit) + statuses, nextMaxID, prevMinID, err := p.db.GetFavedTimeline(authed.Account.ID, maxID, minID, limit) if err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + if err == db.ErrNoEntries { // there are just no entries left return &apimodel.StatusTimelineResponse{ Statuses: []*apimodel.Status{}, @@ -122,7 +122,7 @@ func (p *processor) filterPublicStatuses(authed *oauth.Auth, statuses []*gtsmode for _, s := range statuses { targetAccount := >smodel.Account{} if err := p.db.GetByID(s.AccountID, targetAccount); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + if err == db.ErrNoEntries { l.Debugf("filterPublicStatuses: skipping status %s because account %s can't be found in the db", s.ID, s.AccountID) continue } @@ -157,7 +157,7 @@ func (p *processor) filterFavedStatuses(authed *oauth.Auth, statuses []*gtsmodel for _, s := range statuses { targetAccount := >smodel.Account{} if err := p.db.GetByID(s.AccountID, targetAccount); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + if err == db.ErrNoEntries { l.Debugf("filterFavedStatuses: skipping status %s because account %s can't be found in the db", s.ID, s.AccountID) continue } diff --git a/internal/router/session.go b/internal/router/session.go index 2b9be2f56..38810572f 100644 --- a/internal/router/session.go +++ b/internal/router/session.go @@ -49,7 +49,7 @@ func useSession(cfg *config.Config, dbService db.DB, engine *gin.Engine) error { // check if we have a saved router session already routerSessions := []*gtsmodel.RouterSession{} if err := dbService.GetAll(&routerSessions); err != nil { - if _, ok := err.(db.ErrNoEntries); !ok { + if err != db.ErrNoEntries { // proper error occurred return err } diff --git a/internal/text/common.go b/internal/text/common.go index f6a5ca5f5..af77521dd 100644 --- a/internal/text/common.go +++ b/internal/text/common.go @@ -93,9 +93,9 @@ func (f *formatter) ReplaceMentions(in string, mentions []*gtsmodel.Mention) str // make sure we have a target account, either by getting one pinned on the mention, // or by pulling it from the database var targetAccount *gtsmodel.Account - if menchie.GTSAccount != nil { + if menchie.OriginAccount != nil { // got it from the mention - targetAccount = menchie.GTSAccount + targetAccount = menchie.OriginAccount } else { a := >smodel.Account{} if err := f.db.GetByID(menchie.TargetAccountID, a); err == nil { diff --git a/internal/timeline/index.go b/internal/timeline/index.go index 1e1a9d7bb..7cffe7ab9 100644 --- a/internal/timeline/index.go +++ b/internal/timeline/index.go @@ -50,9 +50,9 @@ func (t *timeline) IndexBefore(statusID string, include bool, amount int) error i := 0 grabloop: for ; len(filtered) < amount && i < 5; i = i + 1 { // try the grabloop 5 times only - statuses, err := t.db.GetHomeTimelineForAccount(t.accountID, "", "", offsetStatus, amount, false) + statuses, err := t.db.GetHomeTimeline(t.accountID, "", "", offsetStatus, amount, false) if err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + if err == db.ErrNoEntries { break grabloop // we just don't have enough statuses left in the db so index what we've got and then bail } return fmt.Errorf("IndexBefore: error getting statuses from db: %s", err) @@ -130,9 +130,9 @@ positionLoop: grabloop: for ; len(filtered) < amount && i < 5; i = i + 1 { // try the grabloop 5 times only l.Tracef("entering grabloop; i is %d; len(filtered) is %d", i, len(filtered)) - statuses, err := t.db.GetHomeTimelineForAccount(t.accountID, offsetStatus, "", "", amount, false) + statuses, err := t.db.GetHomeTimeline(t.accountID, offsetStatus, "", "", amount, false) if err != nil { - if _, ok := err.(db.ErrNoEntries); ok { + if err == db.ErrNoEntries { break grabloop // we just don't have enough statuses left in the db so index what we've got and then bail } return fmt.Errorf("IndexBehind: error getting statuses from db: %s", err) diff --git a/internal/timeline/index_test.go b/internal/timeline/index_test.go index f48b2691c..4201a27dd 100644 --- a/internal/timeline/index_test.go +++ b/internal/timeline/index_test.go @@ -67,9 +67,8 @@ func (suite *IndexTestSuite) TestIndexBeforeLowID() { suite.NoError(err) suite.Equal("01F8MHAAY43M6RJ473VQFCVH37", postID) - // indexLength should only be 9 because that's all this user has hometimelineable indexLength := suite.timeline.PostIndexLength() - suite.Equal(9, indexLength) + suite.Equal(10, indexLength) } func (suite *IndexTestSuite) TestIndexBeforeHighID() { @@ -97,9 +96,9 @@ func (suite *IndexTestSuite) TestIndexBehindHighID() { suite.NoError(err) suite.Equal("01FCTA44PW9H1TB328S9AQXKDS", postID) - // indexLength should only be 11 because that's all this user has hometimelineable + // indexLength should be 10 because that's all this user has hometimelineable indexLength := suite.timeline.PostIndexLength() - suite.Equal(11, indexLength) + suite.Equal(10, indexLength) } func (suite *IndexTestSuite) TestIndexBehindLowID() { diff --git a/internal/timeline/manager_test.go b/internal/timeline/manager_test.go index 9b975a5ce..00c6dcb4a 100644 --- a/internal/timeline/manager_test.go +++ b/internal/timeline/manager_test.go @@ -66,9 +66,9 @@ func (suite *ManagerTestSuite) TestManagerIntegration() { err = suite.manager.PrepareXFromTop(testAccount.ID, 20) suite.NoError(err) - // local_account_1 can see 11 statuses out of the testrig statuses in its home timeline + // local_account_1 can see 12 statuses out of the testrig statuses in its home timeline indexedLen = suite.manager.GetIndexedLength(testAccount.ID) - suite.Equal(11, indexedLen) + suite.Equal(12, indexedLen) // oldest should now be set oldestIndexed, err = suite.manager.GetOldestIndexedID(testAccount.ID) @@ -78,7 +78,7 @@ func (suite *ManagerTestSuite) TestManagerIntegration() { // get hometimeline statuses, err := suite.manager.HomeTimeline(testAccount.ID, "", "", "", 20, false) suite.NoError(err) - suite.Len(statuses, 11) + suite.Len(statuses, 12) // now wipe the last status from all timelines, as though it had been deleted by the owner err = suite.manager.WipeStatusFromAllTimelines("01F8MH75CBF9JFX4ZAD54N0W0R") @@ -86,7 +86,7 @@ func (suite *ManagerTestSuite) TestManagerIntegration() { // timeline should be shorter indexedLen = suite.manager.GetIndexedLength(testAccount.ID) - suite.Equal(10, indexedLen) + suite.Equal(11, indexedLen) // oldest should now be different oldestIndexed, err = suite.manager.GetOldestIndexedID(testAccount.ID) @@ -100,7 +100,7 @@ func (suite *ManagerTestSuite) TestManagerIntegration() { // timeline should be shorter indexedLen = suite.manager.GetIndexedLength(testAccount.ID) - suite.Equal(9, indexedLen) + suite.Equal(10, indexedLen) // oldest should now be different oldestIndexed, err = suite.manager.GetOldestIndexedID(testAccount.ID) diff --git a/internal/timeline/prepare.go b/internal/timeline/prepare.go index 51846c816..20000b4e9 100644 --- a/internal/timeline/prepare.go +++ b/internal/timeline/prepare.go @@ -95,7 +95,7 @@ prepareloop: if preparing { if err := t.prepare(entry.statusID); err != nil { // there's been an error - if _, ok := err.(db.ErrNoEntries); !ok { + if err != db.ErrNoEntries { // it's a real error return fmt.Errorf("PrepareBehind: error preparing status with id %s: %s", entry.statusID, err) } @@ -150,7 +150,7 @@ prepareloop: if preparing { if err := t.prepare(entry.statusID); err != nil { // there's been an error - if _, ok := err.(db.ErrNoEntries); !ok { + if err != db.ErrNoEntries { // it's a real error return fmt.Errorf("PrepareBefore: error preparing status with id %s: %s", entry.statusID, err) } @@ -205,7 +205,7 @@ prepareloop: if err := t.prepare(entry.statusID); err != nil { // there's been an error - if _, ok := err.(db.ErrNoEntries); !ok { + if err != db.ErrNoEntries { // it's a real error return fmt.Errorf("PrepareFromTop: error preparing status with id %s: %s", entry.statusID, err) } diff --git a/internal/transport/controller.go b/internal/transport/controller.go index 33eab2a3a..4eb6b5658 100644 --- a/internal/transport/controller.go +++ b/internal/transport/controller.go @@ -28,7 +28,6 @@ import ( "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) // Controller generates transports for use in making federation requests to other servers. @@ -95,14 +94,15 @@ func (c *controller) NewTransportForUsername(username string) (Transport, error) // We need an account to use to create a transport for dereferecing something. // If a username has been given, we can fetch the account with that username and use it. // Otherwise, we can take the instance account and use those credentials to make the request. - ourAccount := >smodel.Account{} var u string if username == "" { u = c.config.Host } else { u = username } - if err := c.db.GetLocalAccountByUsername(u, ourAccount); err != nil { + + ourAccount, err := c.db.GetLocalAccountByUsername(u) + if err != nil { return nil, fmt.Errorf("error getting account %s from db: %s", username, err) } diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index f754d282a..a16318df8 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -37,21 +37,20 @@ func (c *converter) ASRepresentationToAccount(accountable ap.Accountable, update } uri := uriProp.GetIRI() - acct := >smodel.Account{} if !update { - err := c.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, acct) + acct, err := c.db.GetAccountByURI(uri.String()) if err == nil { // we already know this account so we can skip generating it return acct, nil } - if _, ok := err.(db.ErrNoEntries); !ok { + if err != db.ErrNoEntries { // we don't know the account and there's been a real error return nil, fmt.Errorf("error getting account with uri %s from the database: %s", uri.String(), err) } } // we don't know the account, or we're being told to update it, so we need to generate it from the person -- at least we already have the URI! - acct = >smodel.Account{} + acct := >smodel.Account{} acct.URI = uri.String() // Username aka preferredUsername @@ -188,22 +187,22 @@ func (c *converter) ASStatusToStatus(statusable ap.Statusable) (*gtsmodel.Status // attachments to dereference and fetch later on (we don't do that here) if attachments, err := ap.ExtractAttachments(statusable); err == nil { - status.GTSMediaAttachments = attachments + status.Attachments = attachments } // hashtags to dereference later on if hashtags, err := ap.ExtractHashtags(statusable); err == nil { - status.GTSTags = hashtags + status.Tags = hashtags } // emojis to dereference and fetch later on if emojis, err := ap.ExtractEmojis(statusable); err == nil { - status.GTSEmojis = emojis + status.Emojis = emojis } // mentions to dereference later on if mentions, err := ap.ExtractMentions(statusable); err == nil { - status.GTSMentions = mentions + status.Mentions = mentions } // cw string for this status @@ -225,13 +224,13 @@ func (c *converter) ASStatusToStatus(statusable ap.Statusable) (*gtsmodel.Status } status.AccountURI = attributedTo.String() - statusOwner := >smodel.Account{} - if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: attributedTo.String(), CaseInsensitive: true}}, statusOwner); err != nil { + statusOwner, err := c.db.GetAccountByURI(attributedTo.String()) + if err != nil { return nil, fmt.Errorf("couldn't get status owner from db: %s", err) } status.AccountID = statusOwner.ID status.AccountURI = statusOwner.URI - status.GTSAuthorAccount = statusOwner + status.Account = statusOwner // check if there's a post that this is a reply to inReplyToURI := ap.ExtractInReplyToURI(statusable) @@ -241,18 +240,16 @@ func (c *converter) ASStatusToStatus(statusable ap.Statusable) (*gtsmodel.Status status.InReplyToURI = inReplyToURI.String() // now we can check if we have the replied-to status in our db already - inReplyToStatus := >smodel.Status{} - if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: inReplyToURI.String()}}, inReplyToStatus); err == nil { + if inReplyToStatus, err := c.db.GetStatusByURI(inReplyToURI.String()); err == nil { // we have the status in our database already - // so we can set these fields here and then... + // so we can set these fields here and now... status.InReplyToID = inReplyToStatus.ID status.InReplyToAccountID = inReplyToStatus.AccountID - status.GTSReplyToStatus = inReplyToStatus - - // ... check if we've seen the account already - inReplyToAccount := >smodel.Account{} - if err := c.db.GetByID(inReplyToStatus.AccountID, inReplyToAccount); err == nil { - status.GTSReplyToAccount = inReplyToAccount + status.InReplyTo = inReplyToStatus + if status.InReplyToAccount == nil { + if inReplyToAccount, err := c.db.GetAccountByID(inReplyToStatus.AccountID); err == nil { + status.InReplyToAccount = inReplyToAccount + } } } } @@ -328,8 +325,8 @@ func (c *converter) ASFollowToFollowRequest(followable ap.Followable) (*gtsmodel if err != nil { return nil, errors.New("error extracting actor property from follow") } - originAccount := >smodel.Account{} - if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: origin.String()}}, originAccount); err != nil { + originAccount, err := c.db.GetAccountByURI(origin.String()) + if err != nil { return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) } @@ -337,8 +334,8 @@ func (c *converter) ASFollowToFollowRequest(followable ap.Followable) (*gtsmodel if err != nil { return nil, errors.New("error extracting object property from follow") } - targetAccount := >smodel.Account{} - if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: target.String()}}, targetAccount); err != nil { + targetAccount, err := c.db.GetAccountByURI(target.String()) + if err != nil { return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) } @@ -362,8 +359,8 @@ func (c *converter) ASFollowToFollow(followable ap.Followable) (*gtsmodel.Follow if err != nil { return nil, errors.New("error extracting actor property from follow") } - originAccount := >smodel.Account{} - if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: origin.String()}}, originAccount); err != nil { + originAccount, err := c.db.GetAccountByURI(origin.String()) + if err != nil { return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) } @@ -371,8 +368,8 @@ func (c *converter) ASFollowToFollow(followable ap.Followable) (*gtsmodel.Follow if err != nil { return nil, errors.New("error extracting object property from follow") } - targetAccount := >smodel.Account{} - if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: target.String()}}, targetAccount); err != nil { + targetAccount, err := c.db.GetAccountByURI(target.String()) + if err != nil { return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) } @@ -396,8 +393,8 @@ func (c *converter) ASLikeToFave(likeable ap.Likeable) (*gtsmodel.StatusFave, er if err != nil { return nil, errors.New("error extracting actor property from like") } - originAccount := >smodel.Account{} - if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: origin.String()}}, originAccount); err != nil { + originAccount, err := c.db.GetAccountByURI(origin.String()) + if err != nil { return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) } @@ -406,24 +403,30 @@ func (c *converter) ASLikeToFave(likeable ap.Likeable) (*gtsmodel.StatusFave, er return nil, errors.New("error extracting object property from like") } - targetStatus := >smodel.Status{} - if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: target.String()}}, targetStatus); err != nil { + targetStatus, err := c.db.GetStatusByURI(target.String()) + if err != nil { return nil, fmt.Errorf("error extracting status with uri %s from the database: %s", target.String(), err) } - targetAccount := >smodel.Account{} - if err := c.db.GetByID(targetStatus.AccountID, targetAccount); err != nil { - return nil, fmt.Errorf("error extracting account with id %s from the database: %s", targetStatus.AccountID, err) + var targetAccount *gtsmodel.Account + if targetStatus.Account != nil { + targetAccount = targetStatus.Account + } else { + a, err := c.db.GetAccountByID(targetStatus.AccountID) + if err != nil { + return nil, fmt.Errorf("error extracting account with id %s from the database: %s", targetStatus.AccountID, err) + } + targetAccount = a } return >smodel.StatusFave{ - TargetAccountID: targetAccount.ID, - StatusID: targetStatus.ID, - AccountID: originAccount.ID, - URI: uri, - GTSStatus: targetStatus, - GTSTargetAccount: targetAccount, - GTSFavingAccount: originAccount, + AccountID: originAccount.ID, + Account: originAccount, + TargetAccountID: targetAccount.ID, + TargetAccount: targetAccount, + StatusID: targetStatus.ID, + Status: targetStatus, + URI: uri, }, nil } @@ -438,9 +441,9 @@ func (c *converter) ASBlockToBlock(blockable ap.Blockable) (*gtsmodel.Block, err if err != nil { return nil, errors.New("ASBlockToBlock: error extracting actor property from block") } - originAccount := >smodel.Account{} - if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: origin.String()}}, originAccount); err != nil { - return nil, fmt.Errorf("ASBlockToBlock: error extracting account with uri %s from the database: %s", origin.String(), err) + originAccount, err := c.db.GetAccountByURI(origin.String()) + if err != nil { + return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) } target, err := ap.ExtractObject(blockable) @@ -448,9 +451,9 @@ func (c *converter) ASBlockToBlock(blockable ap.Blockable) (*gtsmodel.Block, err return nil, errors.New("ASBlockToBlock: error extracting object property from block") } - targetAccount := >smodel.Account{} - if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: target.String(), CaseInsensitive: true}}, targetAccount); err != nil { - return nil, fmt.Errorf("ASBlockToBlock: error extracting account with uri %s from the database: %s", target.String(), err) + targetAccount, err := c.db.GetAccountByURI(target.String()) + if err != nil { + return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) } return >smodel.Block{ @@ -473,7 +476,7 @@ func (c *converter) ASAnnounceToStatus(announceable ap.Announceable) (*gtsmodel. } uri := idProp.GetIRI().String() - if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: uri}}, status); err == nil { + if status, err := c.db.GetStatusByURI(uri); err == nil { // we already have it, great, just return it as-is :) isNew = false return status, isNew, nil @@ -487,7 +490,7 @@ func (c *converter) ASAnnounceToStatus(announceable ap.Announceable) (*gtsmodel. } // set the URI on the new status for dereferencing later - status.GTSBoostedStatus = >smodel.Status{ + status.BoostOf = >smodel.Status{ URI: boostedStatusURI.String(), } @@ -507,18 +510,19 @@ func (c *converter) ASAnnounceToStatus(announceable ap.Announceable) (*gtsmodel. // get the boosting account based on the URI // this should have been dereferenced already before we hit this point so we can confidently error out if we don't have it - boostingAccount := >smodel.Account{} - if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: actor.String()}}, boostingAccount); err != nil { + boostingAccount, err := c.db.GetAccountByURI(actor.String()) + if err != nil { return nil, isNew, fmt.Errorf("ASAnnounceToStatus: error in db fetching account with uri %s: %s", actor.String(), err) } status.AccountID = boostingAccount.ID status.AccountURI = boostingAccount.URI + status.Account = boostingAccount // these will all be wrapped in the boosted status so set them empty here - status.Attachments = []string{} - status.Tags = []string{} - status.Mentions = []string{} - status.Emojis = []string{} + status.AttachmentIDs = []string{} + status.TagIDs = []string{} + status.MentionIDs = []string{} + status.EmojiIDs = []string{} // parse the visibility from the To and CC entries var visibility gtsmodel.Visibility @@ -552,7 +556,6 @@ func (c *converter) ASAnnounceToStatus(announceable ap.Announceable) (*gtsmodel. status.Visibility = visibility // the rest of the fields will be taken from the target status, but it's not our job to do the dereferencing here - return status, isNew, nil } diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go index 10d9a0f18..e477a6135 100644 --- a/internal/typeutils/converter.go +++ b/internal/typeutils/converter.go @@ -24,6 +24,7 @@ import ( "github.com/go-fed/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/cache" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" @@ -175,14 +176,18 @@ type TypeConverter interface { } type converter struct { - config *config.Config - db db.DB + config *config.Config + db db.DB + frontendCache cache.Cache + asCache cache.Cache } // NewConverter returns a new Converter func NewConverter(config *config.Config, db db.DB) TypeConverter { return &converter{ - config: config, - db: db, + config: config, + db: db, + frontendCache: cache.New(), + asCache: cache.New(), } } diff --git a/internal/typeutils/internal.go b/internal/typeutils/internal.go index a46ad7fbd..ad15ecbee 100644 --- a/internal/typeutils/internal.go +++ b/internal/typeutils/internal.go @@ -54,10 +54,10 @@ func (c *converter) StatusToBoost(s *gtsmodel.Status, boostingAccount *gtsmodel. InReplyToAccountID: "", // these will all be wrapped in the boosted status so set them empty here - Attachments: []string{}, - Tags: []string{}, - Mentions: []string{}, - Emojis: []string{}, + AttachmentIDs: []string{}, + TagIDs: []string{}, + MentionIDs: []string{}, + EmojiIDs: []string{}, // the below fields will be taken from the target status Content: s.Content, @@ -74,7 +74,7 @@ func (c *converter) StatusToBoost(s *gtsmodel.Status, boostingAccount *gtsmodel. // attach these here for convenience -- the boosted status/account won't go in the DB // but they're needed in the processor and for the frontend. Since we have them, we can // attach them so we don't need to fetch them again later (save some DB calls) - GTSBoostedStatus: s, + BoostOf: s, } return boostWrapperStatus, nil diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go index 333f131d4..11ace9dfa 100644 --- a/internal/typeutils/internaltoas.go +++ b/internal/typeutils/internaltoas.go @@ -34,6 +34,14 @@ import ( // Converts a gts model account into an Activity Streams person type, following // the spec laid out for mastodon here: https://docs.joinmastodon.org/spec/activitypub/ func (c *converter) AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error) { + // first check if we have this person in our asCache already + if personI, err := c.asCache.Fetch(a.ID); err == nil { + if person, ok := personI.(vocab.ActivityStreamsPerson); ok { + // we have it, so just return it as-is + return person, nil + } + } + person := streams.NewActivityStreamsPerson() // id should be the activitypub URI of this user @@ -256,6 +264,11 @@ func (c *converter) AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerso person.SetActivityStreamsImage(headerProperty) } + // put the person in our cache in case we need it again soon + if err := c.asCache.Store(a.ID, person); err != nil { + return nil, err + } + return person, nil } @@ -326,16 +339,24 @@ func (c *converter) AccountToASMinimal(a *gtsmodel.Account) (vocab.ActivityStrea } func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, error) { + // first check if we have this note in our asCache already + if noteI, err := c.asCache.Fetch(s.ID); err == nil { + if note, ok := noteI.(vocab.ActivityStreamsNote); ok { + // we have it, so just return it as-is + return note, nil + } + } + // ensure prerequisites here before we get stuck in // check if author account is already attached to status and attach it if not // if we can't retrieve this, bail here already because we can't attribute the status to anyone - if s.GTSAuthorAccount == nil { - a := >smodel.Account{} - if err := c.db.GetByID(s.AccountID, a); err != nil { + if s.Account == nil { + a, err := c.db.GetAccountByID(s.AccountID) + if err != nil { return nil, fmt.Errorf("StatusToAS: error retrieving author account from db: %s", err) } - s.GTSAuthorAccount = a + s.Account = a } // create the Note! @@ -361,16 +382,16 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e // inReplyTo if s.InReplyToID != "" { // fetch the replied status if we don't have it on hand already - if s.GTSReplyToStatus == nil { + if s.InReplyTo == nil { rs := >smodel.Status{} if err := c.db.GetByID(s.InReplyToID, rs); err != nil { return nil, fmt.Errorf("StatusToAS: error retrieving replied-to status from db: %s", err) } - s.GTSReplyToStatus = rs + s.InReplyTo = rs } - rURI, err := url.Parse(s.GTSReplyToStatus.URI) + rURI, err := url.Parse(s.InReplyTo.URI) if err != nil { - return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.GTSReplyToStatus.URI, err) + return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.InReplyTo.URI, err) } inReplyToProp := streams.NewActivityStreamsInReplyToProperty() @@ -396,9 +417,9 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e } // attributedTo - authorAccountURI, err := url.Parse(s.GTSAuthorAccount.URI) + authorAccountURI, err := url.Parse(s.Account.URI) if err != nil { - return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.GTSAuthorAccount.URI, err) + return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.Account.URI, err) } attributedToProp := streams.NewActivityStreamsAttributedToProperty() attributedToProp.AppendIRI(authorAccountURI) @@ -408,7 +429,7 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e tagProp := streams.NewActivityStreamsTagProperty() // tag -- mentions - for _, m := range s.GTSMentions { + for _, m := range s.Mentions { asMention, err := c.MentionToAS(m) if err != nil { return nil, fmt.Errorf("StatusToAS: error converting mention to AS mention: %s", err) @@ -425,9 +446,9 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e status.SetActivityStreamsTag(tagProp) // parse out some URIs we need here - authorFollowersURI, err := url.Parse(s.GTSAuthorAccount.FollowersURI) + authorFollowersURI, err := url.Parse(s.Account.FollowersURI) if err != nil { - return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.GTSAuthorAccount.FollowersURI, err) + return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.Account.FollowersURI, err) } publicURI, err := url.Parse(asPublicURI) @@ -441,10 +462,10 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e switch s.Visibility { case gtsmodel.VisibilityDirect: // if DIRECT, then only mentioned users should be added to TO, and nothing to CC - for _, m := range s.GTSMentions { - iri, err := url.Parse(m.GTSAccount.URI) + for _, m := range s.Mentions { + iri, err := url.Parse(m.OriginAccount.URI) if err != nil { - return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.GTSAccount.URI, err) + return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.OriginAccount.URI, err) } toProp.AppendIRI(iri) } @@ -453,10 +474,10 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e case gtsmodel.VisibilityFollowersOnly: // if FOLLOWERS ONLY then we want to add followers to TO, and mentions to CC toProp.AppendIRI(authorFollowersURI) - for _, m := range s.GTSMentions { - iri, err := url.Parse(m.GTSAccount.URI) + for _, m := range s.Mentions { + iri, err := url.Parse(m.OriginAccount.URI) if err != nil { - return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.GTSAccount.URI, err) + return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.OriginAccount.URI, err) } ccProp.AppendIRI(iri) } @@ -464,10 +485,10 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e // if UNLOCKED, we want to add followers to TO, and public and mentions to CC toProp.AppendIRI(authorFollowersURI) ccProp.AppendIRI(publicURI) - for _, m := range s.GTSMentions { - iri, err := url.Parse(m.GTSAccount.URI) + for _, m := range s.Mentions { + iri, err := url.Parse(m.OriginAccount.URI) if err != nil { - return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.GTSAccount.URI, err) + return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.OriginAccount.URI, err) } ccProp.AppendIRI(iri) } @@ -475,10 +496,10 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e // if PUBLIC, we want to add public to TO, and followers and mentions to CC toProp.AppendIRI(publicURI) ccProp.AppendIRI(authorFollowersURI) - for _, m := range s.GTSMentions { - iri, err := url.Parse(m.GTSAccount.URI) + for _, m := range s.Mentions { + iri, err := url.Parse(m.OriginAccount.URI) if err != nil { - return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.GTSAccount.URI, err) + return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.OriginAccount.URI, err) } ccProp.AppendIRI(iri) } @@ -496,7 +517,7 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e // attachment attachmentProp := streams.NewActivityStreamsAttachmentProperty() - for _, a := range s.GTSMediaAttachments { + for _, a := range s.Attachments { doc, err := c.AttachmentToAS(a) if err != nil { return nil, fmt.Errorf("StatusToAS: error converting attachment: %s", err) @@ -515,6 +536,11 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e repliesProp.SetActivityStreamsCollection(repliesCollection) status.SetActivityStreamsReplies(repliesProp) + // put the note in our cache in case we need it again soon + if err := c.asCache.Store(s.ID, status); err != nil { + return nil, err + } + return status, nil } @@ -565,12 +591,12 @@ func (c *converter) FollowToAS(f *gtsmodel.Follow, originAccount *gtsmodel.Accou } func (c *converter) MentionToAS(m *gtsmodel.Mention) (vocab.ActivityStreamsMention, error) { - if m.GTSAccount == nil { + if m.OriginAccount == nil { a := >smodel.Account{} if err := c.db.GetWhere([]db.Where{{Key: "target_account_id", Value: m.TargetAccountID}}, a); err != nil { return nil, fmt.Errorf("MentionToAS: error getting target account from db: %s", err) } - m.GTSAccount = a + m.OriginAccount = a } // create the mention @@ -578,21 +604,21 @@ func (c *converter) MentionToAS(m *gtsmodel.Mention) (vocab.ActivityStreamsMenti // href -- this should be the URI of the mentioned user hrefProp := streams.NewActivityStreamsHrefProperty() - hrefURI, err := url.Parse(m.GTSAccount.URI) + hrefURI, err := url.Parse(m.OriginAccount.URI) if err != nil { - return nil, fmt.Errorf("MentionToAS: error parsing uri %s: %s", m.GTSAccount.URI, err) + return nil, fmt.Errorf("MentionToAS: error parsing uri %s: %s", m.OriginAccount.URI, err) } hrefProp.SetIRI(hrefURI) mention.SetActivityStreamsHref(hrefProp) // name -- this should be the namestring of the mentioned user, something like @whatever@example.org var domain string - if m.GTSAccount.Domain == "" { + if m.OriginAccount.Domain == "" { domain = c.config.AccountDomain } else { - domain = m.GTSAccount.Domain + domain = m.OriginAccount.Domain } - username := m.GTSAccount.Username + username := m.OriginAccount.Username nameString := fmt.Sprintf("@%s@%s", username, domain) nameProp := streams.NewActivityStreamsNameProperty() nameProp.AppendXMLSchemaString(nameString) @@ -648,30 +674,30 @@ func (c *converter) AttachmentToAS(a *gtsmodel.MediaAttachment) (vocab.ActivityS */ func (c *converter) FaveToAS(f *gtsmodel.StatusFave) (vocab.ActivityStreamsLike, error) { // check if targetStatus is already pinned to this fave, and fetch it if not - if f.GTSStatus == nil { + if f.Status == nil { s := >smodel.Status{} if err := c.db.GetByID(f.StatusID, s); err != nil { return nil, fmt.Errorf("FaveToAS: error fetching target status from database: %s", err) } - f.GTSStatus = s + f.Status = s } // check if the targetAccount is already pinned to this fave, and fetch it if not - if f.GTSTargetAccount == nil { + if f.TargetAccount == nil { a := >smodel.Account{} if err := c.db.GetByID(f.TargetAccountID, a); err != nil { return nil, fmt.Errorf("FaveToAS: error fetching target account from database: %s", err) } - f.GTSTargetAccount = a + f.TargetAccount = a } // check if the faving account is already pinned to this fave, and fetch it if not - if f.GTSFavingAccount == nil { + if f.Account == nil { a := >smodel.Account{} if err := c.db.GetByID(f.AccountID, a); err != nil { return nil, fmt.Errorf("FaveToAS: error fetching faving account from database: %s", err) } - f.GTSFavingAccount = a + f.Account = a } // create the like @@ -679,9 +705,9 @@ func (c *converter) FaveToAS(f *gtsmodel.StatusFave) (vocab.ActivityStreamsLike, // set the actor property to the fave-ing account's URI actorProp := streams.NewActivityStreamsActorProperty() - actorIRI, err := url.Parse(f.GTSFavingAccount.URI) + actorIRI, err := url.Parse(f.Account.URI) if err != nil { - return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.GTSFavingAccount.URI, err) + return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.Account.URI, err) } actorProp.AppendIRI(actorIRI) like.SetActivityStreamsActor(actorProp) @@ -697,18 +723,18 @@ func (c *converter) FaveToAS(f *gtsmodel.StatusFave) (vocab.ActivityStreamsLike, // set the object property to the target status's URI objectProp := streams.NewActivityStreamsObjectProperty() - statusIRI, err := url.Parse(f.GTSStatus.URI) + statusIRI, err := url.Parse(f.Status.URI) if err != nil { - return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.GTSStatus.URI, err) + return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.Status.URI, err) } objectProp.AppendIRI(statusIRI) like.SetActivityStreamsObject(objectProp) // set the TO property to the target account's IRI toProp := streams.NewActivityStreamsToProperty() - toIRI, err := url.Parse(f.GTSTargetAccount.URI) + toIRI, err := url.Parse(f.TargetAccount.URI) if err != nil { - return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.GTSTargetAccount.URI, err) + return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.TargetAccount.URI, err) } toProp.AppendIRI(toIRI) like.SetActivityStreamsTo(toProp) @@ -718,12 +744,12 @@ func (c *converter) FaveToAS(f *gtsmodel.StatusFave) (vocab.ActivityStreamsLike, func (c *converter) BoostToAS(boostWrapperStatus *gtsmodel.Status, boostingAccount *gtsmodel.Account, boostedAccount *gtsmodel.Account) (vocab.ActivityStreamsAnnounce, error) { // the boosted status is probably pinned to the boostWrapperStatus but double check to make sure - if boostWrapperStatus.GTSBoostedStatus == nil { + if boostWrapperStatus.BoostOf == nil { b := >smodel.Status{} if err := c.db.GetByID(boostWrapperStatus.BoostOfID, b); err != nil { return nil, fmt.Errorf("BoostToAS: error getting status with ID %s from the db: %s", boostWrapperStatus.BoostOfID, err) } - boostWrapperStatus.GTSBoostedStatus = b + boostWrapperStatus.BoostOf = b } // create the announce @@ -748,9 +774,9 @@ func (c *converter) BoostToAS(boostWrapperStatus *gtsmodel.Status, boostingAccou announce.SetJSONLDId(idProp) // set the object - boostedStatusURI, err := url.Parse(boostWrapperStatus.GTSBoostedStatus.URI) + boostedStatusURI, err := url.Parse(boostWrapperStatus.BoostOf.URI) if err != nil { - return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", boostWrapperStatus.GTSBoostedStatus.URI, err) + return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", boostWrapperStatus.BoostOf.URI, err) } objectProp := streams.NewActivityStreamsObjectProperty() objectProp.AppendIRI(boostedStatusURI) diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 1283e718a..caa14e211 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -38,15 +38,15 @@ func (c *converter) AccountToMastoSensitive(a *gtsmodel.Account) (*model.Account // then adding the Source object to it... // check pending follow requests aimed at this account - fr := []gtsmodel.FollowRequest{} - if err := c.db.GetFollowRequestsForAccountID(a.ID, &fr); err != nil { - if _, ok := err.(db.ErrNoEntries); !ok { + frs, err := c.db.GetAccountFollowRequests(a.ID) + if err != nil { + if err != db.ErrNoEntries { return nil, fmt.Errorf("error getting follow requests: %s", err) } } var frc int - if fr != nil { - frc = len(fr) + if frs != nil { + frc = len(frs) } mastoAccount.Source = &model.Source{ @@ -62,68 +62,69 @@ func (c *converter) AccountToMastoSensitive(a *gtsmodel.Account) (*model.Account } func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*model.Account, error) { - // count followers - followers := []gtsmodel.Follow{} - if err := c.db.GetFollowersByAccountID(a.ID, &followers, false); err != nil { - if _, ok := err.(db.ErrNoEntries); !ok { - return nil, fmt.Errorf("error getting followers: %s", err) + // first check if we have this account in our frontEnd cache + if accountI, err := c.frontendCache.Fetch(a.ID); err == nil { + if account, ok := accountI.(*model.Account); ok { + // we have it, so just return it as-is + return account, nil } } - var followersCount int - if followers != nil { - followersCount = len(followers) + + // count followers + followersCount, err := c.db.CountAccountFollowedBy(a.ID, false) + if err != nil { + return nil, fmt.Errorf("error counting followers: %s", err) } // count following - following := []gtsmodel.Follow{} - if err := c.db.GetFollowingByAccountID(a.ID, &following); err != nil { - if _, ok := err.(db.ErrNoEntries); !ok { - return nil, fmt.Errorf("error getting following: %s", err) - } - } - var followingCount int - if following != nil { - followingCount = len(following) + followingCount, err := c.db.CountAccountFollows(a.ID, false) + if err != nil { + return nil, fmt.Errorf("error counting following: %s", err) } // count statuses - statusesCount, err := c.db.CountStatusesByAccountID(a.ID) + statusesCount, err := c.db.CountAccountStatuses(a.ID) if err != nil { - if _, ok := err.(db.ErrNoEntries); !ok { - return nil, fmt.Errorf("error getting last statuses: %s", err) - } + return nil, fmt.Errorf("error counting statuses: %s", err) } // check when the last status was - lastStatus := >smodel.Status{} - if err := c.db.GetLastStatusForAccountID(a.ID, lastStatus); err != nil { - if _, ok := err.(db.ErrNoEntries); !ok { - return nil, fmt.Errorf("error getting last status: %s", err) - } - } var lastStatusAt string - if lastStatus != nil { - lastStatusAt = lastStatus.CreatedAt.Format(time.RFC3339) + lastPosted, err := c.db.GetAccountLastPosted(a.ID) + if err == nil && !lastPosted.IsZero() { + lastStatusAt = lastPosted.Format(time.RFC3339) } // build the avatar and header URLs - avi := >smodel.MediaAttachment{} - if err := c.db.GetAvatarForAccountID(avi, a.ID); err != nil { - if _, ok := err.(db.ErrNoEntries); !ok { - return nil, fmt.Errorf("error getting avatar: %s", err) + var aviURL string + var aviURLStatic string + if a.AvatarMediaAttachmentID != "" { + // make sure avi is pinned to this account + if a.AvatarMediaAttachment == nil { + avi, err := c.db.GetAttachmentByID(a.AvatarMediaAttachmentID) + if err != nil { + return nil, fmt.Errorf("error retrieving avatar: %s", err) + } + a.AvatarMediaAttachment = avi } + aviURL = a.AvatarMediaAttachment.URL + aviURLStatic = a.AvatarMediaAttachment.Thumbnail.URL } - aviURL := avi.URL - aviURLStatic := avi.Thumbnail.URL - header := >smodel.MediaAttachment{} - if err := c.db.GetHeaderForAccountID(header, a.ID); err != nil { - if _, ok := err.(db.ErrNoEntries); !ok { - return nil, fmt.Errorf("error getting header: %s", err) + var headerURL string + var headerURLStatic string + if a.HeaderMediaAttachmentID != "" { + // make sure header is pinned to this account + if a.HeaderMediaAttachment == nil { + avi, err := c.db.GetAttachmentByID(a.HeaderMediaAttachmentID) + if err != nil { + return nil, fmt.Errorf("error retrieving avatar: %s", err) + } + a.HeaderMediaAttachment = avi } + headerURL = a.HeaderMediaAttachment.URL + headerURLStatic = a.HeaderMediaAttachment.Thumbnail.URL } - headerURL := header.URL - headerURLStatic := header.Thumbnail.URL // get the fields set on this account fields := []model.Field{} @@ -155,7 +156,7 @@ func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*model.Account, e suspended = true } - return &model.Account{ + accountFrontend := &model.Account{ ID: a.ID, Username: a.Username, Acct: acct, @@ -176,7 +177,14 @@ func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*model.Account, e Emojis: emojis, // TODO: implement this Fields: fields, Suspended: suspended, - }, nil + } + + // put the account in our cache in case we need it again soon + if err := c.frontendCache.Store(a.ID, accountFrontend); err != nil { + return nil, err + } + + return accountFrontend, nil } func (c *converter) AccountToMastoBlocked(a *gtsmodel.Account) (*model.Account, error) { @@ -302,17 +310,17 @@ func (c *converter) TagToMasto(t *gtsmodel.Tag) (model.Tag, error) { } func (c *converter) StatusToMasto(s *gtsmodel.Status, requestingAccount *gtsmodel.Account) (*model.Status, error) { - repliesCount, err := c.db.GetReplyCountForStatus(s) + repliesCount, err := c.db.CountStatusReplies(s) if err != nil { return nil, fmt.Errorf("error counting replies: %s", err) } - reblogsCount, err := c.db.GetReblogCountForStatus(s) + reblogsCount, err := c.db.CountStatusReblogs(s) if err != nil { return nil, fmt.Errorf("error counting reblogs: %s", err) } - favesCount, err := c.db.GetFaveCountForStatus(s) + favesCount, err := c.db.CountStatusFaves(s) if err != nil { return nil, fmt.Errorf("error counting faves: %s", err) } @@ -320,27 +328,27 @@ func (c *converter) StatusToMasto(s *gtsmodel.Status, requestingAccount *gtsmode var mastoRebloggedStatus *model.Status if s.BoostOfID != "" { // the boosted status might have been set on this struct already so check first before doing db calls - if s.GTSBoostedStatus == nil { + if s.BoostOf == nil { // it's not set so fetch it from the db bs := >smodel.Status{} if err := c.db.GetByID(s.BoostOfID, bs); err != nil { return nil, fmt.Errorf("error getting boosted status with id %s: %s", s.BoostOfID, err) } - s.GTSBoostedStatus = bs + s.BoostOf = bs } // the boosted account might have been set on this struct already or passed as a param so check first before doing db calls - if s.GTSBoostedAccount == nil { + if s.BoostOfAccount == nil { // it's not set so fetch it from the db ba := >smodel.Account{} - if err := c.db.GetByID(s.GTSBoostedStatus.AccountID, ba); err != nil { - return nil, fmt.Errorf("error getting boosted account %s from status with id %s: %s", s.GTSBoostedStatus.AccountID, s.BoostOfID, err) + if err := c.db.GetByID(s.BoostOf.AccountID, ba); err != nil { + return nil, fmt.Errorf("error getting boosted account %s from status with id %s: %s", s.BoostOf.AccountID, s.BoostOfID, err) } - s.GTSBoostedAccount = ba - s.GTSBoostedStatus.GTSAuthorAccount = ba + s.BoostOfAccount = ba + s.BoostOf.Account = ba } - mastoRebloggedStatus, err = c.StatusToMasto(s.GTSBoostedStatus, requestingAccount) + mastoRebloggedStatus, err = c.StatusToMasto(s.BoostOf, requestingAccount) if err != nil { return nil, fmt.Errorf("error converting boosted status to mastotype: %s", err) } @@ -358,15 +366,15 @@ func (c *converter) StatusToMasto(s *gtsmodel.Status, requestingAccount *gtsmode } } - if s.GTSAuthorAccount == nil { + if s.Account == nil { a := >smodel.Account{} if err := c.db.GetByID(s.AccountID, a); err != nil { return nil, fmt.Errorf("error getting status author: %s", err) } - s.GTSAuthorAccount = a + s.Account = a } - mastoAuthorAccount, err := c.AccountToMastoPublic(s.GTSAuthorAccount) + mastoAuthorAccount, err := c.AccountToMastoPublic(s.Account) if err != nil { return nil, fmt.Errorf("error parsing account of status author: %s", err) } @@ -374,8 +382,8 @@ func (c *converter) StatusToMasto(s *gtsmodel.Status, requestingAccount *gtsmode mastoAttachments := []model.Attachment{} // the status might already have some gts attachments on it if it's not been pulled directly from the database // if so, we can directly convert the gts attachments into masto ones - if s.GTSMediaAttachments != nil { - for _, gtsAttachment := range s.GTSMediaAttachments { + if s.Attachments != nil { + for _, gtsAttachment := range s.Attachments { mastoAttachment, err := c.AttachmentToMasto(gtsAttachment) if err != nil { return nil, fmt.Errorf("error converting attachment with id %s: %s", gtsAttachment.ID, err) @@ -385,7 +393,7 @@ func (c *converter) StatusToMasto(s *gtsmodel.Status, requestingAccount *gtsmode // the status doesn't have gts attachments on it, but it does have attachment IDs // in this case, we need to pull the gts attachments from the db to convert them into masto ones } else { - for _, a := range s.Attachments { + for _, a := range s.AttachmentIDs { gtsAttachment := >smodel.MediaAttachment{} if err := c.db.GetByID(a, gtsAttachment); err != nil { return nil, fmt.Errorf("error getting attachment with id %s: %s", a, err) @@ -401,8 +409,8 @@ func (c *converter) StatusToMasto(s *gtsmodel.Status, requestingAccount *gtsmode mastoMentions := []model.Mention{} // the status might already have some gts mentions on it if it's not been pulled directly from the database // if so, we can directly convert the gts mentions into masto ones - if s.GTSMentions != nil { - for _, gtsMention := range s.GTSMentions { + if s.Mentions != nil { + for _, gtsMention := range s.Mentions { mastoMention, err := c.MentionToMasto(gtsMention) if err != nil { return nil, fmt.Errorf("error converting mention with id %s: %s", gtsMention.ID, err) @@ -412,7 +420,7 @@ func (c *converter) StatusToMasto(s *gtsmodel.Status, requestingAccount *gtsmode // the status doesn't have gts mentions on it, but it does have mention IDs // in this case, we need to pull the gts mentions from the db to convert them into masto ones } else { - for _, m := range s.Mentions { + for _, m := range s.MentionIDs { gtsMention := >smodel.Mention{} if err := c.db.GetByID(m, gtsMention); err != nil { return nil, fmt.Errorf("error getting mention with id %s: %s", m, err) @@ -428,8 +436,8 @@ func (c *converter) StatusToMasto(s *gtsmodel.Status, requestingAccount *gtsmode mastoTags := []model.Tag{} // the status might already have some gts tags on it if it's not been pulled directly from the database // if so, we can directly convert the gts tags into masto ones - if s.GTSTags != nil { - for _, gtsTag := range s.GTSTags { + if s.Tags != nil { + for _, gtsTag := range s.Tags { mastoTag, err := c.TagToMasto(gtsTag) if err != nil { return nil, fmt.Errorf("error converting tag with id %s: %s", gtsTag.ID, err) @@ -439,7 +447,7 @@ func (c *converter) StatusToMasto(s *gtsmodel.Status, requestingAccount *gtsmode // the status doesn't have gts tags on it, but it does have tag IDs // in this case, we need to pull the gts tags from the db to convert them into masto ones } else { - for _, t := range s.Tags { + for _, t := range s.TagIDs { gtsTag := >smodel.Tag{} if err := c.db.GetByID(t, gtsTag); err != nil { return nil, fmt.Errorf("error getting tag with id %s: %s", t, err) @@ -455,8 +463,8 @@ func (c *converter) StatusToMasto(s *gtsmodel.Status, requestingAccount *gtsmode mastoEmojis := []model.Emoji{} // the status might already have some gts emojis on it if it's not been pulled directly from the database // if so, we can directly convert the gts emojis into masto ones - if s.GTSEmojis != nil { - for _, gtsEmoji := range s.GTSEmojis { + if s.Emojis != nil { + for _, gtsEmoji := range s.Emojis { mastoEmoji, err := c.EmojiToMasto(gtsEmoji) if err != nil { return nil, fmt.Errorf("error converting emoji with id %s: %s", gtsEmoji.ID, err) @@ -466,7 +474,7 @@ func (c *converter) StatusToMasto(s *gtsmodel.Status, requestingAccount *gtsmode // the status doesn't have gts emojis on it, but it does have emoji IDs // in this case, we need to pull the gts emojis from the db to convert them into masto ones } else { - for _, e := range s.Emojis { + for _, e := range s.EmojiIDs { gtsEmoji := >smodel.Emoji{} if err := c.db.GetByID(e, gtsEmoji); err != nil { return nil, fmt.Errorf("error getting emoji with id %s: %s", e, err) @@ -559,17 +567,17 @@ func (c *converter) InstanceToMasto(i *gtsmodel.Instance) (*model.Instance, erro statusCountKey := "status_count" domainCountKey := "domain_count" - userCount, err := c.db.GetUserCountForInstance(c.config.Host) + userCount, err := c.db.CountInstanceUsers(c.config.Host) if err == nil { mi.Stats[userCountKey] = userCount } - statusCount, err := c.db.GetStatusCountForInstance(c.config.Host) + statusCount, err := c.db.CountInstanceStatuses(c.config.Host) if err == nil { mi.Stats[statusCountKey] = statusCount } - domainCount, err := c.db.GetDomainCountForInstance(c.config.Host) + domainCount, err := c.db.CountInstanceDomains(c.config.Host) if err == nil { mi.Stats[domainCountKey] = domainCount } @@ -585,13 +593,10 @@ func (c *converter) InstanceToMasto(i *gtsmodel.Instance) (*model.Instance, erro } // get the instance account if it exists and just skip if it doesn't - ia := >smodel.Account{} - if err := c.db.GetWhere([]db.Where{{Key: "username", Value: i.Domain}}, ia); err == nil { - // instance account exists, get the header for the account if it exists - attachment := >smodel.MediaAttachment{} - if err := c.db.GetHeaderForAccountID(attachment, ia.ID); err == nil { - // header exists, set it on the api model - mi.Thumbnail = attachment.URL + ia, err := c.db.GetInstanceAccount("") + if err == nil { + if ia.HeaderMediaAttachment != nil { + mi.Thumbnail = ia.HeaderMediaAttachment.URL } } @@ -628,47 +633,47 @@ func (c *converter) RelationshipToMasto(r *gtsmodel.Relationship) (*model.Relati } func (c *converter) NotificationToMasto(n *gtsmodel.Notification) (*model.Notification, error) { - - if n.GTSTargetAccount == nil { - tAccount := >smodel.Account{} - if err := c.db.GetByID(n.TargetAccountID, tAccount); err != nil { + if n.TargetAccount == nil { + tAccount, err := c.db.GetAccountByID(n.TargetAccountID) + if err != nil { return nil, fmt.Errorf("NotificationToMasto: error getting target account with id %s from the db: %s", n.TargetAccountID, err) } - n.GTSTargetAccount = tAccount + n.TargetAccount = tAccount } - if n.GTSOriginAccount == nil { - ogAccount := >smodel.Account{} - if err := c.db.GetByID(n.OriginAccountID, ogAccount); err != nil { + if n.OriginAccount == nil { + ogAccount, err := c.db.GetAccountByID(n.OriginAccountID) + if err != nil { return nil, fmt.Errorf("NotificationToMasto: error getting origin account with id %s from the db: %s", n.OriginAccountID, err) } - n.GTSOriginAccount = ogAccount + n.OriginAccount = ogAccount } - mastoAccount, err := c.AccountToMastoPublic(n.GTSOriginAccount) + + mastoAccount, err := c.AccountToMastoPublic(n.OriginAccount) if err != nil { return nil, fmt.Errorf("NotificationToMasto: error converting account to masto: %s", err) } var mastoStatus *model.Status if n.StatusID != "" { - if n.GTSStatus == nil { - status := >smodel.Status{} - if err := c.db.GetByID(n.StatusID, status); err != nil { + if n.Status == nil { + status, err := c.db.GetStatusByID(n.StatusID) + if err != nil { return nil, fmt.Errorf("NotificationToMasto: error getting status with id %s from the db: %s", n.StatusID, err) } - n.GTSStatus = status + n.Status = status } - if n.GTSStatus.GTSAuthorAccount == nil { - if n.GTSStatus.AccountID == n.GTSTargetAccount.ID { - n.GTSStatus.GTSAuthorAccount = n.GTSTargetAccount - } else if n.GTSStatus.AccountID == n.GTSOriginAccount.ID { - n.GTSStatus.GTSAuthorAccount = n.GTSOriginAccount + if n.Status.Account == nil { + if n.Status.AccountID == n.TargetAccount.ID { + n.Status.Account = n.TargetAccount + } else if n.Status.AccountID == n.OriginAccount.ID { + n.Status.Account = n.OriginAccount } } var err error - mastoStatus, err = c.StatusToMasto(n.GTSStatus, nil) + mastoStatus, err = c.StatusToMasto(n.Status, nil) if err != nil { return nil, fmt.Errorf("NotificationToMasto: error converting status to masto: %s", err) } diff --git a/internal/typeutils/util.go b/internal/typeutils/util.go index 1e13f0713..5751fbc84 100644 --- a/internal/typeutils/util.go +++ b/internal/typeutils/util.go @@ -10,25 +10,25 @@ func (c *converter) interactionsWithStatusForAccount(s *gtsmodel.Status, request si := &statusInteractions{} if requestingAccount != nil { - faved, err := c.db.StatusFavedBy(s, requestingAccount.ID) + faved, err := c.db.IsStatusFavedBy(s, requestingAccount.ID) if err != nil { return nil, fmt.Errorf("error checking if requesting account has faved status: %s", err) } si.Faved = faved - reblogged, err := c.db.StatusRebloggedBy(s, requestingAccount.ID) + reblogged, err := c.db.IsStatusRebloggedBy(s, requestingAccount.ID) if err != nil { return nil, fmt.Errorf("error checking if requesting account has reblogged status: %s", err) } si.Reblogged = reblogged - muted, err := c.db.StatusMutedBy(s, requestingAccount.ID) + muted, err := c.db.IsStatusMutedBy(s, requestingAccount.ID) if err != nil { return nil, fmt.Errorf("error checking if requesting account has muted status: %s", err) } si.Muted = muted - bookmarked, err := c.db.StatusBookmarkedBy(s, requestingAccount.ID) + bookmarked, err := c.db.IsStatusBookmarkedBy(s, requestingAccount.ID) if err != nil { return nil, fmt.Errorf("error checking if requesting account has bookmarked status: %s", err) } diff --git a/internal/util/statustools.go b/internal/util/statustools.go index ce5860c6d..4a89e60f6 100644 --- a/internal/util/statustools.go +++ b/internal/util/statustools.go @@ -34,7 +34,7 @@ func DeriveMentionsFromStatus(status string) []string { for _, m := range mentionFinderRegex.FindAllStringSubmatch(status, -1) { mentionedAccounts = append(mentionedAccounts, m[1]) } - return unique(mentionedAccounts) + return UniqueStrings(mentionedAccounts) } // DeriveHashtagsFromStatus takes a plaintext (ie., not html-formatted) status, @@ -46,7 +46,7 @@ func DeriveHashtagsFromStatus(status string) []string { for _, m := range HashtagFinderRegex.FindAllStringSubmatch(status, -1) { tags = append(tags, strings.TrimPrefix(m[1], "#")) } - return unique(tags) + return UniqueStrings(tags) } // DeriveEmojisFromStatus takes a plaintext (ie., not html-formatted) status, @@ -57,7 +57,7 @@ func DeriveEmojisFromStatus(status string) []string { for _, m := range emojiFinderRegex.FindAllStringSubmatch(status, -1) { emojis = append(emojis, m[1]) } - return unique(emojis) + return UniqueStrings(emojis) } // ExtractMentionParts extracts the username test_user and the domain example.org @@ -79,16 +79,3 @@ func ExtractMentionParts(mention string) (username, domain string, err error) { func IsMention(mention string) bool { return mentionNameRegex.MatchString(strings.ToLower(mention)) } - -// unique returns a deduplicated version of a given string slice. -func unique(s []string) []string { - keys := make(map[string]bool) - list := []string{} - for _, entry := range s { - if _, value := keys[entry]; !value { - keys[entry] = true - list = append(list, entry) - } - } - return list -} diff --git a/internal/util/unique.go b/internal/util/unique.go new file mode 100644 index 000000000..d679515d0 --- /dev/null +++ b/internal/util/unique.go @@ -0,0 +1,32 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package util + +// UniqueStrings returns a deduplicated version of a given string slice. +func UniqueStrings(s []string) []string { + keys := make(map[string]bool) + list := []string{} + for _, entry := range s { + if _, value := keys[entry]; !value { + keys[entry] = true + list = append(list, entry) + } + } + return list +} diff --git a/internal/visibility/filter.go b/internal/visibility/filter.go index 181eb8ee7..2c43fa4ee 100644 --- a/internal/visibility/filter.go +++ b/internal/visibility/filter.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package visibility import ( diff --git a/internal/visibility/relevantaccounts.go b/internal/visibility/relevantaccounts.go new file mode 100644 index 000000000..5957d3111 --- /dev/null +++ b/internal/visibility/relevantaccounts.go @@ -0,0 +1,229 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package visibility + +import ( + "errors" + "fmt" + + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +// relevantAccounts denotes accounts that are replied to, boosted by, or mentioned in a status. +type relevantAccounts struct { + // Who wrote the status + Account *gtsmodel.Account + // Who is the status replying to + InReplyToAccount *gtsmodel.Account + // Which accounts are mentioned (tagged) in the status + MentionedAccounts []*gtsmodel.Account + // Who authed the boosted status + BoostedAccount *gtsmodel.Account + // If the boosted status replies to another account, who does it reply to? + BoostedInReplyToAccount *gtsmodel.Account + // Who is mentioned (tagged) in the boosted status + BoostedMentionedAccounts []*gtsmodel.Account +} + +func (f *filter) relevantAccounts(status *gtsmodel.Status, getBoosted bool) (*relevantAccounts, error) { + relAccts := &relevantAccounts{ + MentionedAccounts: []*gtsmodel.Account{}, + BoostedMentionedAccounts: []*gtsmodel.Account{}, + } + + /* + Here's what we need to try and extract from the status: + + // 1. Who wrote the status + Account *gtsmodel.Account + + // 2. Who is the status replying to + InReplyToAccount *gtsmodel.Account + + // 3. Which accounts are mentioned (tagged) in the status + MentionedAccounts []*gtsmodel.Account + + if getBoosted: + // 4. Who wrote the boosted status + BoostedAccount *gtsmodel.Account + + // 5. If the boosted status replies to another account, who does it reply to? + BoostedInReplyToAccount *gtsmodel.Account + + // 6. Who is mentioned (tagged) in the boosted status + BoostedMentionedAccounts []*gtsmodel.Account + */ + + // 1. Account. + // Account might be set on the status already + if status.Account != nil { + // it was set + relAccts.Account = status.Account + } else { + // it wasn't set, so get it from the db + account, err := f.db.GetAccountByID(status.AccountID) + if err != nil { + return nil, fmt.Errorf("relevantAccounts: error getting account with id %s: %s", status.AccountID, err) + } + // set it on the status in case we need it further along + status.Account = account + // set it on relevant accounts + relAccts.Account = account + } + + // 2. InReplyToAccount + // only get this if InReplyToAccountID is set + if status.InReplyToAccountID != "" { + // InReplyToAccount might be set on the status already + if status.InReplyToAccount != nil { + // it was set + relAccts.InReplyToAccount = status.InReplyToAccount + } else { + // it wasn't set, so get it from the db + inReplyToAccount, err := f.db.GetAccountByID(status.InReplyToAccountID) + if err != nil { + return nil, fmt.Errorf("relevantAccounts: error getting inReplyToAccount with id %s: %s", status.InReplyToAccountID, err) + } + // set it on the status in case we need it further along + status.InReplyToAccount = inReplyToAccount + // set it on relevant accounts + relAccts.InReplyToAccount = inReplyToAccount + } + } + + // 3. MentionedAccounts + // First check if status.Mentions is populated with all mentions that correspond to status.MentionIDs + for _, mID := range status.MentionIDs { + if mID == "" { + continue + } + if !idIn(mID, status.Mentions) { + // mention with ID isn't in status.Mentions + mention, err := f.db.GetMention(mID) + if err != nil { + return nil, fmt.Errorf("relevantAccounts: error getting mention with id %s: %s", mID, err) + } + if mention == nil { + return nil, fmt.Errorf("relevantAccounts: mention with id %s was nil", mID) + } + status.Mentions = append(status.Mentions, mention) + } + } + // now filter mentions to make sure we only have mentions with a corresponding ID + nm := []*gtsmodel.Mention{} + for _, m := range status.Mentions { + if m == nil { + continue + } + if mentionIn(m, status.MentionIDs) { + nm = append(nm, m) + } + } + status.Mentions = nm + + if len(status.Mentions) != len(status.MentionIDs) { + return nil, errors.New("relevantAccounts: mentions length did not correspond with mentionIDs length") + } + + // if getBoosted is set, we should check the same properties on the boosted account as well + if getBoosted { + // 4, 5, 6. Boosted status items + // get the boosted status if it's not set on the status already + if status.BoostOfID != "" && status.BoostOf == nil { + boostedStatus, err := f.db.GetStatusByID(status.BoostOfID) + if err != nil { + return nil, fmt.Errorf("relevantAccounts: error getting boosted status with id %s: %s", status.BoostOfID, err) + } + status.BoostOf = boostedStatus + } + + if status.BoostOf != nil { + // return relevant accounts for the boosted status + boostedRelAccts, err := f.relevantAccounts(status.BoostOf, false) // false because we don't want to recurse + if err != nil { + return nil, fmt.Errorf("relevantAccounts: error getting relevant accounts of boosted status %s: %s", status.BoostOf.ID, err) + } + relAccts.BoostedAccount = boostedRelAccts.Account + relAccts.BoostedInReplyToAccount = boostedRelAccts.InReplyToAccount + relAccts.BoostedMentionedAccounts = boostedRelAccts.MentionedAccounts + } + } + + return relAccts, nil +} + +// domainBlockedRelevant checks through all relevant accounts attached to a status +// to make sure none of them are domain blocked by this instance. +func (f *filter) domainBlockedRelevant(r *relevantAccounts) (bool, error) { + domains := []string{} + + if r.Account != nil { + domains = append(domains, r.Account.Domain) + } + + if r.InReplyToAccount != nil { + domains = append(domains, r.InReplyToAccount.Domain) + } + + for _, a := range r.MentionedAccounts { + if a != nil { + domains = append(domains, a.Domain) + } + } + + if r.BoostedAccount != nil { + domains = append(domains, r.BoostedAccount.Domain) + } + + if r.BoostedInReplyToAccount != nil { + domains = append(domains, r.BoostedInReplyToAccount.Domain) + } + + for _, a := range r.BoostedMentionedAccounts { + if a != nil { + domains = append(domains, a.Domain) + } + } + + return f.db.AreDomainsBlocked(domains) +} + +func idIn(id string, mentions []*gtsmodel.Mention) bool { + for _, m := range mentions { + if m == nil { + continue + } + if m.ID == id { + return true + } + } + return false +} + +func mentionIn(mention *gtsmodel.Mention, ids []string) bool { + if mention == nil { + return false + } + for _, i := range ids { + if mention.ID == i { + return true + } + } + return false +} diff --git a/internal/visibility/statushometimelineable.go b/internal/visibility/statushometimelineable.go index bc5f7bcb8..a3ca62fb3 100644 --- a/internal/visibility/statushometimelineable.go +++ b/internal/visibility/statushometimelineable.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package visibility import ( @@ -28,6 +46,13 @@ func (f *filter) StatusHometimelineable(targetStatus *gtsmodel.Status, timelineO return false, nil } + for _, m := range targetStatus.Mentions { + if m.TargetAccountID == timelineOwnerAccount.ID { + // if we're mentioned we should be able to see the post + return true, nil + } + } + // Don't timeline a status whose parent hasn't been dereferenced yet or can't be dereferenced. // If we have the reply to URI but don't have an ID for the replied-to account or the replied-to status in our database, we haven't dereferenced it yet. if targetStatus.InReplyToURI != "" && (targetStatus.InReplyToID == "" || targetStatus.InReplyToAccountID == "") { @@ -37,21 +62,21 @@ func (f *filter) StatusHometimelineable(targetStatus *gtsmodel.Status, timelineO // if a status replies to an ID we know in the database, we need to make sure we also follow the replied-to status owner account if targetStatus.InReplyToID != "" { // pin the reply to status on to this status if it hasn't been done already - if targetStatus.GTSReplyToStatus == nil { - rs := >smodel.Status{} - if err := f.db.GetByID(targetStatus.InReplyToID, rs); err != nil { + if targetStatus.InReplyTo == nil { + rs, err := f.db.GetStatusByID(targetStatus.InReplyToID) + if err != nil { return false, fmt.Errorf("StatusHometimelineable: error getting replied to status with id %s: %s", targetStatus.InReplyToID, err) } - targetStatus.GTSReplyToStatus = rs + targetStatus.InReplyTo = rs } // pin the reply to account on to this status if it hasn't been done already - if targetStatus.GTSReplyToAccount == nil { - ra := >smodel.Account{} - if err := f.db.GetByID(targetStatus.InReplyToAccountID, ra); err != nil { + if targetStatus.InReplyToAccount == nil { + ra, err := f.db.GetAccountByID(targetStatus.InReplyToAccountID) + if err != nil { return false, fmt.Errorf("StatusHometimelineable: error getting replied to account with id %s: %s", targetStatus.InReplyToAccountID, err) } - targetStatus.GTSReplyToAccount = ra + targetStatus.InReplyToAccount = ra } // if it's a reply to the timelineOwnerAccount, we don't need to check if the timelineOwnerAccount follows itself, just return true, they can see it @@ -60,7 +85,7 @@ func (f *filter) StatusHometimelineable(targetStatus *gtsmodel.Status, timelineO } // the replied-to account != timelineOwnerAccount, so make sure the timelineOwnerAccount follows the replied-to account - follows, err := f.db.Follows(timelineOwnerAccount, targetStatus.GTSReplyToAccount) + follows, err := f.db.IsFollowing(timelineOwnerAccount, targetStatus.InReplyToAccount) if err != nil { return false, fmt.Errorf("StatusHometimelineable: error checking follow from account %s to account %s: %s", timelineOwnerAccount.ID, targetStatus.InReplyToAccountID, err) } diff --git a/internal/visibility/statuspublictimelineable.go b/internal/visibility/statuspublictimelineable.go index d7f68faee..f07e06aae 100644 --- a/internal/visibility/statuspublictimelineable.go +++ b/internal/visibility/statuspublictimelineable.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package visibility import ( diff --git a/internal/visibility/statusvisible.go b/internal/visibility/statusvisible.go index dc6b74702..15e545881 100644 --- a/internal/visibility/statusvisible.go +++ b/internal/visibility/statusvisible.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package visibility import ( @@ -16,10 +34,11 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount "statusID": targetStatus.ID, }) - relevantAccounts, err := f.pullRelevantAccountsFromStatus(targetStatus) + getBoosted := true + relevantAccounts, err := f.relevantAccounts(targetStatus, getBoosted) if err != nil { l.Debugf("error pulling relevant accounts for status %s: %s", targetStatus.ID, err) - return false, fmt.Errorf("error pulling relevant accounts for status %s: %s", targetStatus.ID, err) + return false, fmt.Errorf("StatusVisible: error pulling relevant accounts for status %s: %s", targetStatus.ID, err) } domainBlocked, err := f.domainBlockedRelevant(relevantAccounts) @@ -32,7 +51,12 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount return false, nil } - targetAccount := relevantAccounts.StatusAuthor + targetAccount := relevantAccounts.Account + if targetAccount == nil { + l.Trace("target account is not set") + return false, nil + } + // if target account is suspended then don't show the status if !targetAccount.SuspendedAt.IsZero() { l.Trace("target account suspended at is not zero") @@ -45,7 +69,7 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount targetUser := >smodel.User{} if err := f.db.GetWhere([]db.Where{{Key: "account_id", Value: targetAccount.ID}}, targetUser); err != nil { l.Debug("target user could not be selected") - if _, ok := err.(db.ErrNoEntries); ok { + if err == db.ErrNoEntries { return false, nil } return false, fmt.Errorf("StatusVisible: db error selecting user for local target account %s: %s", targetAccount.ID, err) @@ -76,7 +100,7 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount if err := f.db.GetWhere([]db.Where{{Key: "account_id", Value: requestingAccount.ID}}, requestingUser); err != nil { // if the requesting account is local but doesn't have a corresponding user in the db this is a problem l.Debug("requesting user could not be selected") - if _, ok := err.(db.ErrNoEntries); ok { + if err == db.ErrNoEntries { return false, nil } return false, fmt.Errorf("StatusVisible: db error selecting user for local requesting account %s: %s", requestingAccount.ID, err) @@ -102,7 +126,7 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount // At this point we have a populated targetAccount, targetStatus, and requestingAccount, so we can check for blocks and whathaveyou // First check if a block exists directly between the target account (which authored the status) and the requesting account. - if blocked, err := f.db.Blocked(targetAccount.ID, requestingAccount.ID); err != nil { + if blocked, err := f.db.IsBlocked(targetAccount.ID, requestingAccount.ID, true); err != nil { l.Debugf("something went wrong figuring out if the accounts have a block: %s", err) return false, err } else if blocked { @@ -112,8 +136,8 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount } // status replies to account id - if relevantAccounts.ReplyToAccount != nil && relevantAccounts.ReplyToAccount.ID != requestingAccount.ID { - if blocked, err := f.db.Blocked(relevantAccounts.ReplyToAccount.ID, requestingAccount.ID); err != nil { + if relevantAccounts.InReplyToAccount != nil && relevantAccounts.InReplyToAccount.ID != requestingAccount.ID { + if blocked, err := f.db.IsBlocked(relevantAccounts.InReplyToAccount.ID, requestingAccount.ID, true); err != nil { return false, err } else if blocked { l.Trace("a block exists between requesting account and reply to account") @@ -122,7 +146,7 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount // check reply to ID if targetStatus.InReplyToID != "" && (targetStatus.Visibility == gtsmodel.VisibilityFollowersOnly || targetStatus.Visibility == gtsmodel.VisibilityDirect) { - followsRepliedAccount, err := f.db.Follows(requestingAccount, relevantAccounts.ReplyToAccount) + followsRepliedAccount, err := f.db.IsFollowing(requestingAccount, relevantAccounts.InReplyToAccount) if err != nil { return false, err } @@ -134,8 +158,8 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount } // status boosts accounts id - if relevantAccounts.BoostedStatusAuthor != nil { - if blocked, err := f.db.Blocked(relevantAccounts.BoostedStatusAuthor.ID, requestingAccount.ID); err != nil { + if relevantAccounts.BoostedAccount != nil { + if blocked, err := f.db.IsBlocked(relevantAccounts.BoostedAccount.ID, requestingAccount.ID, true); err != nil { return false, err } else if blocked { l.Trace("a block exists between requesting account and boosted account") @@ -144,8 +168,8 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount } // status boosts a reply to account id - if relevantAccounts.BoostedReplyToAccount != nil { - if blocked, err := f.db.Blocked(relevantAccounts.BoostedReplyToAccount.ID, requestingAccount.ID); err != nil { + if relevantAccounts.BoostedInReplyToAccount != nil { + if blocked, err := f.db.IsBlocked(relevantAccounts.BoostedInReplyToAccount.ID, requestingAccount.ID, true); err != nil { return false, err } else if blocked { l.Trace("a block exists between requesting account and boosted reply to account") @@ -155,7 +179,10 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount // status mentions accounts for _, a := range relevantAccounts.MentionedAccounts { - if blocked, err := f.db.Blocked(a.ID, requestingAccount.ID); err != nil { + if a == nil { + continue + } + if blocked, err := f.db.IsBlocked(a.ID, requestingAccount.ID, true); err != nil { return false, err } else if blocked { l.Trace("a block exists between requesting account and a mentioned account") @@ -165,7 +192,10 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount // boost mentions accounts for _, a := range relevantAccounts.BoostedMentionedAccounts { - if blocked, err := f.db.Blocked(a.ID, requestingAccount.ID); err != nil { + if a == nil { + continue + } + if blocked, err := f.db.IsBlocked(a.ID, requestingAccount.ID, true); err != nil { return false, err } else if blocked { l.Trace("a block exists between requesting account and a boosted mentioned account") @@ -175,6 +205,9 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount // if the requesting account is mentioned in the status it should always be visible for _, acct := range relevantAccounts.MentionedAccounts { + if acct == nil { + continue + } if acct.ID == requestingAccount.ID { return true, nil // yep it's mentioned! } @@ -188,7 +221,7 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount return true, nil case gtsmodel.VisibilityFollowersOnly: // check one-way follow - follows, err := f.db.Follows(requestingAccount, targetAccount) + follows, err := f.db.IsFollowing(requestingAccount, targetAccount) if err != nil { return false, err } @@ -199,7 +232,7 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount return true, nil case gtsmodel.VisibilityMutualsOnly: // check mutual follow - mutuals, err := f.db.Mutuals(requestingAccount, targetAccount) + mutuals, err := f.db.IsMutualFollowing(requestingAccount, targetAccount) if err != nil { return false, err } diff --git a/internal/visibility/util.go b/internal/visibility/util.go deleted file mode 100644 index a12dd555f..000000000 --- a/internal/visibility/util.go +++ /dev/null @@ -1,191 +0,0 @@ -package visibility - -import ( - "fmt" - - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func (f *filter) pullRelevantAccountsFromStatus(targetStatus *gtsmodel.Status) (*relevantAccounts, error) { - accounts := &relevantAccounts{ - MentionedAccounts: []*gtsmodel.Account{}, - BoostedMentionedAccounts: []*gtsmodel.Account{}, - } - - // get the author account - if targetStatus.GTSAuthorAccount == nil { - statusAuthor := >smodel.Account{} - if err := f.db.GetByID(targetStatus.AccountID, statusAuthor); err != nil { - return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting statusAuthor with id %s: %s", targetStatus.AccountID, err) - } - targetStatus.GTSAuthorAccount = statusAuthor - } - accounts.StatusAuthor = targetStatus.GTSAuthorAccount - - // get the replied to account from the status and add it to the pile - if targetStatus.InReplyToAccountID != "" { - repliedToAccount := >smodel.Account{} - if err := f.db.GetByID(targetStatus.InReplyToAccountID, repliedToAccount); err != nil { - return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting repliedToAcount with id %s: %s", targetStatus.InReplyToAccountID, err) - } - accounts.ReplyToAccount = repliedToAccount - } - - // now get all accounts with IDs that are mentioned in the status - for _, mentionID := range targetStatus.Mentions { - - mention := >smodel.Mention{} - if err := f.db.GetByID(mentionID, mention); err != nil { - return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting mention with id %s: %s", mentionID, err) - } - - mentionedAccount := >smodel.Account{} - if err := f.db.GetByID(mention.TargetAccountID, mentionedAccount); err != nil { - return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting mentioned account: %s", err) - } - accounts.MentionedAccounts = append(accounts.MentionedAccounts, mentionedAccount) - } - - // get the boosted account from the status and add it to the pile - if targetStatus.BoostOfID != "" { - // retrieve the boosted status first - boostedStatus := >smodel.Status{} - if err := f.db.GetByID(targetStatus.BoostOfID, boostedStatus); err != nil { - return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boostedStatus with id %s: %s", targetStatus.BoostOfID, err) - } - boostedAccount := >smodel.Account{} - if err := f.db.GetByID(boostedStatus.AccountID, boostedAccount); err != nil { - return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boostedAccount with id %s: %s", boostedStatus.AccountID, err) - } - accounts.BoostedStatusAuthor = boostedAccount - - // the boosted status might be a reply to another account so we should get that too - if boostedStatus.InReplyToAccountID != "" { - boostedStatusRepliedToAccount := >smodel.Account{} - if err := f.db.GetByID(boostedStatus.InReplyToAccountID, boostedStatusRepliedToAccount); err != nil { - return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boostedStatusRepliedToAccount with id %s: %s", boostedStatus.InReplyToAccountID, err) - } - accounts.BoostedReplyToAccount = boostedStatusRepliedToAccount - } - - // now get all accounts with IDs that are mentioned in the status - for _, mentionID := range boostedStatus.Mentions { - mention := >smodel.Mention{} - if err := f.db.GetByID(mentionID, mention); err != nil { - return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boosted mention with id %s: %s", mentionID, err) - } - - mentionedAccount := >smodel.Account{} - if err := f.db.GetByID(mention.TargetAccountID, mentionedAccount); err != nil { - return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boosted mentioned account: %s", err) - } - accounts.BoostedMentionedAccounts = append(accounts.BoostedMentionedAccounts, mentionedAccount) - } - } - - return accounts, nil -} - -// relevantAccounts denotes accounts that are replied to, boosted by, or mentioned in a status. -type relevantAccounts struct { - // Who wrote the status - StatusAuthor *gtsmodel.Account - // Who is the status replying to - ReplyToAccount *gtsmodel.Account - // Which accounts are mentioned (tagged) in the status - MentionedAccounts []*gtsmodel.Account - // Who authed the boosted status - BoostedStatusAuthor *gtsmodel.Account - // If the boosted status replies to another account, who does it reply to? - BoostedReplyToAccount *gtsmodel.Account - // Who is mentioned (tagged) in the boosted status - BoostedMentionedAccounts []*gtsmodel.Account -} - -// blockedDomain checks whether the given domain is blocked by us or not -func (f *filter) blockedDomain(host string) (bool, error) { - b := >smodel.DomainBlock{} - err := f.db.GetWhere([]db.Where{{Key: "domain", Value: host, CaseInsensitive: true}}, b) - if err == nil { - // block exists - return true, nil - } - - if _, ok := err.(db.ErrNoEntries); ok { - // there are no entries so there's no block - return false, nil - } - - // there's an actual error - return false, err -} - -// domainBlockedRelevant checks through all relevant accounts attached to a status -// to make sure none of them are domain blocked by this instance. -// -// Will return true+nil if there's a block, false+nil if there's no block, or -// an error if something goes wrong. -func (f *filter) domainBlockedRelevant(r *relevantAccounts) (bool, error) { - if r.StatusAuthor != nil { - b, err := f.blockedDomain(r.StatusAuthor.Domain) - if err != nil { - return false, err - } - if b { - return true, nil - } - } - - if r.ReplyToAccount != nil { - b, err := f.blockedDomain(r.ReplyToAccount.Domain) - if err != nil { - return false, err - } - if b { - return true, nil - } - } - - for _, a := range r.MentionedAccounts { - b, err := f.blockedDomain(a.Domain) - if err != nil { - return false, err - } - if b { - return true, nil - } - } - - if r.BoostedStatusAuthor != nil { - b, err := f.blockedDomain(r.BoostedStatusAuthor.Domain) - if err != nil { - return false, err - } - if b { - return true, nil - } - } - - if r.BoostedReplyToAccount != nil { - b, err := f.blockedDomain(r.BoostedReplyToAccount.Domain) - if err != nil { - return false, err - } - if b { - return true, nil - } - } - - for _, a := range r.BoostedMentionedAccounts { - b, err := f.blockedDomain(a.Domain) - if err != nil { - return false, err - } - if b { - return true, nil - } - } - - return false, nil -} diff --git a/testrig/db.go b/testrig/db.go index 659a74ca2..c670103f1 100644 --- a/testrig/db.go +++ b/testrig/db.go @@ -40,6 +40,8 @@ var testModels []interface{} = []interface{}{ >smodel.MediaAttachment{}, >smodel.Mention{}, >smodel.Status{}, + >smodel.StatusToEmoji{}, + >smodel.StatusToTag{}, >smodel.StatusFave{}, >smodel.StatusBookmark{}, >smodel.StatusMute{}, @@ -133,7 +135,7 @@ func StandardDBSetup(db db.DB, accounts map[string]*gtsmodel.Account) { } for _, v := range NewTestStatuses() { - if err := db.Put(v); err != nil { + if err := db.PutStatus(v); err != nil { panic(err) } } diff --git a/testrig/testmodels.go b/testrig/testmodels.go index 77274474c..220a3d5ac 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -793,10 +793,10 @@ func NewTestStatuses() map[string]*gtsmodel.Status { URI: "http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R", URL: "http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R", Content: "hello world! #welcome ! first post on the instance :rainbow: !", - Attachments: []string{"01F8MH6NEM8D7527KZAECTCR76"}, - Tags: []string{"01F8MHA1A2NF9MJ3WCCQ3K8BSZ"}, - Mentions: []string{}, - Emojis: []string{"01F8MH9H8E4VG3KDYJR9EGPXCQ"}, + AttachmentIDs: []string{"01F8MH6NEM8D7527KZAECTCR76"}, + TagIDs: []string{"01F8MHA1A2NF9MJ3WCCQ3K8BSZ"}, + MentionIDs: []string{}, + EmojiIDs: []string{"01F8MH9H8E4VG3KDYJR9EGPXCQ"}, CreatedAt: time.Now().Add(-71 * time.Hour), UpdatedAt: time.Now().Add(-71 * time.Hour), Local: true, @@ -917,7 +917,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { URI: "http://localhost:8080/users/the_mighty_zork/statuses/01F8MH82FYRXD2RC6108DAJ5HB", URL: "http://localhost:8080/@the_mighty_zork/statuses/01F8MH82FYRXD2RC6108DAJ5HB", Content: "here's a little gif of trent", - Attachments: []string{"01F8MH7TDVANYKWVE8VVKFPJTJ"}, + AttachmentIDs: []string{"01F8MH7TDVANYKWVE8VVKFPJTJ"}, CreatedAt: time.Now().Add(-1 * time.Hour), UpdatedAt: time.Now().Add(-1 * time.Hour), Local: true, @@ -942,7 +942,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { URI: "http://localhost:8080/users/the_mighty_zork/statuses/01FCTA44PW9H1TB328S9AQXKDS", URL: "http://localhost:8080/@the_mighty_zork/statuses/01FCTA44PW9H1TB328S9AQXKDS", Content: "hi!", - Attachments: []string{}, + AttachmentIDs: []string{}, CreatedAt: time.Now().Add(-1 * time.Minute), UpdatedAt: time.Now().Add(-1 * time.Minute), Local: true, @@ -1062,10 +1062,11 @@ func NewTestStatuses() map[string]*gtsmodel.Status { ID: "01FCQSQ667XHJ9AV9T27SJJSX5", URI: "http://localhost:8080/users/1happyturtle/statuses/01FCQSQ667XHJ9AV9T27SJJSX5", URL: "http://localhost:8080/@1happyturtle/statuses/01FCQSQ667XHJ9AV9T27SJJSX5", - Content: "🐢 hi zork! 🐢", + Content: "🐢 @the_mighty_zork hi zork! 🐢", CreatedAt: time.Now().Add(-1 * time.Minute), UpdatedAt: time.Now().Add(-1 * time.Minute), Local: true, + MentionIDs: []string{"01FDF2HM2NF6FSRZCDEDV451CN"}, AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF", InReplyToID: "01F8MHAMCHF6Y650WCRSCP4WMY", InReplyToAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF", @@ -1119,16 +1120,28 @@ func NewTestTags() map[string]*gtsmodel.Tag { func NewTestMentions() map[string]*gtsmodel.Mention { return map[string]*gtsmodel.Mention{ "zork_mention_foss_satan": { - ID: "01FCTA2Y6FGHXQA4ZE6N5NMNEX", - StatusID: "01FCTA44PW9H1TB328S9AQXKDS", - CreatedAt: time.Now().Add(-1 * time.Minute), - UpdatedAt: time.Now().Add(-1 * time.Minute), - OriginAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF", - OriginAccountURI: "http://localhost:8080/users/the_mighty_zork", - TargetAccountID: "01F8MH5ZK5VRH73AKHQM6Y9VNX", - NameString: "@foss_satan@fossbros-anonymous.io", - MentionedAccountURI: "http://fossbros-anonymous.io/users/foss_satan", - MentionedAccountURL: "http://fossbros-anonymous.io/@foss_satan", + ID: "01FCTA2Y6FGHXQA4ZE6N5NMNEX", + StatusID: "01FCTA44PW9H1TB328S9AQXKDS", + CreatedAt: time.Now().Add(-1 * time.Minute), + UpdatedAt: time.Now().Add(-1 * time.Minute), + OriginAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF", + OriginAccountURI: "http://localhost:8080/users/the_mighty_zork", + TargetAccountID: "01F8MH5ZK5VRH73AKHQM6Y9VNX", + NameString: "@foss_satan@fossbros-anonymous.io", + TargetAccountURI: "http://fossbros-anonymous.io/users/foss_satan", + TargetAccountURL: "http://fossbros-anonymous.io/@foss_satan", + }, + "local_user_2_mention_zork": { + ID: "01FDF2HM2NF6FSRZCDEDV451CN", + StatusID: "01FCQSQ667XHJ9AV9T27SJJSX5", + CreatedAt: time.Now().Add(-1 * time.Minute), + UpdatedAt: time.Now().Add(-1 * time.Minute), + OriginAccountID: "01F8MH5NBDF2MV7CTC4Q5128HF", + OriginAccountURI: "http://localhost:8080/users/1happyturtle", + TargetAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF", + NameString: "@the_mighty_zork", + TargetAccountURI: "http://localhost:8080/users/the_mighty_zork", + TargetAccountURL: "http://localhost:8080/@the_mighty_zork", }, } } diff --git a/vendor/github.com/ReneKroon/ttlcache/.travis.yml b/vendor/github.com/ReneKroon/ttlcache/.travis.yml new file mode 100644 index 000000000..095be4ff3 --- /dev/null +++ b/vendor/github.com/ReneKroon/ttlcache/.travis.yml @@ -0,0 +1,18 @@ +language: go + +go: + - "1.14" + - "1.13" +git: + depth: 1 + +install: + - go install -race std + - go install golang.org/x/tools/cmd/cover + - go install golang.org/x/lint/golint + - export PATH=$HOME/gopath/bin:$PATH + +script: + - golint . + - go test -cover -race -count=1 -timeout=30s -run . + - cd bench; go test -run=Bench.* -bench=. -benchmem \ No newline at end of file diff --git a/vendor/github.com/ReneKroon/ttlcache/LICENSE b/vendor/github.com/ReneKroon/ttlcache/LICENSE new file mode 100644 index 000000000..b3b587dce --- /dev/null +++ b/vendor/github.com/ReneKroon/ttlcache/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Rene Kroon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/ReneKroon/ttlcache/Readme.md b/vendor/github.com/ReneKroon/ttlcache/Readme.md new file mode 100644 index 000000000..9c537fbdc --- /dev/null +++ b/vendor/github.com/ReneKroon/ttlcache/Readme.md @@ -0,0 +1,71 @@ +## TTLCache - an in-memory cache with expiration + +TTLCache is a simple key/value cache in golang with the following functions: + +1. Thread-safe +2. Individual expiring time or global expiring time, you can choose +3. Auto-Extending expiration on `Get` -or- DNS style TTL, see `SkipTtlExtensionOnHit(bool)` +4. Fast and memory efficient +5. Can trigger callback on key expiration +6. Cleanup resources by calling `Close()` at end of lifecycle. + +Note (issue #25): by default, due to historic reasons, the TTL will be reset on each cache hit and you need to explicitly configure the cache to use a TTL that will not get extended. + +[![Build Status](https://travis-ci.org/ReneKroon/ttlcache.svg?branch=master)](https://travis-ci.org/ReneKroon/ttlcache) + +#### Usage +```go +import ( + "time" + "fmt" + + "github.com/ReneKroon/ttlcache" +) + +func main () { + newItemCallback := func(key string, value interface{}) { + fmt.Printf("New key(%s) added\n", key) + } + checkExpirationCallback := func(key string, value interface{}) bool { + if key == "key1" { + // if the key equals "key1", the value + // will not be allowed to expire + return false + } + // all other values are allowed to expire + return true + } + expirationCallback := func(key string, value interface{}) { + fmt.Printf("This key(%s) has expired\n", key) + } + + cache := ttlcache.NewCache() + defer cache.Close() + cache.SetTTL(time.Duration(10 * time.Second)) + cache.SetExpirationCallback(expirationCallback) + + cache.Set("key", "value") + cache.SetWithTTL("keyWithTTL", "value", 10 * time.Second) + + value, exists := cache.Get("key") + count := cache.Count() + result := cache.Remove("key") +} +``` + +#### TTLCache - Some design considerations + +1. The complexity of the current cache is already quite high. Therefore i will not add 'convenience' features like an interface to supply a function to get missing keys. +2. The locking should be done only in the functions of the Cache struct. Else data races can occur or recursive locks are needed, which are both unwanted. +3. I prefer correct functionality over fast tests. It's ok for new tests to take seconds to proof something. + +#### Original Project + +TTLCache was forked from [wunderlist/ttlcache](https://github.com/wunderlist/ttlcache) to add extra functions not avaiable in the original scope. +The main differences are: + +1. A item can store any kind of object, previously, only strings could be saved +2. Optionally, you can add callbacks too: check if a value should expire, be notified if a value expires, and be notified when new values are added to the cache +3. The expiration can be either global or per item +4. Can exist items without expiration time +5. Expirations and callbacks are realtime. Don't have a pooling time to check anymore, now it's done with a heap. diff --git a/vendor/github.com/ReneKroon/ttlcache/cache.go b/vendor/github.com/ReneKroon/ttlcache/cache.go new file mode 100644 index 000000000..f772d0c7c --- /dev/null +++ b/vendor/github.com/ReneKroon/ttlcache/cache.go @@ -0,0 +1,307 @@ +package ttlcache + +import ( + "sync" + "time" +) + +// CheckExpireCallback is used as a callback for an external check on item expiration +type checkExpireCallback func(key string, value interface{}) bool + +// ExpireCallback is used as a callback on item expiration or when notifying of an item new to the cache +type expireCallback func(key string, value interface{}) + +// Cache is a synchronized map of items that can auto-expire once stale +type Cache struct { + mutex sync.Mutex + ttl time.Duration + items map[string]*item + expireCallback expireCallback + checkExpireCallback checkExpireCallback + newItemCallback expireCallback + priorityQueue *priorityQueue + expirationNotification chan bool + expirationTime time.Time + skipTTLExtension bool + shutdownSignal chan (chan struct{}) + isShutDown bool +} + +func (cache *Cache) getItem(key string) (*item, bool, bool) { + item, exists := cache.items[key] + if !exists || item.expired() { + return nil, false, false + } + + if item.ttl >= 0 && (item.ttl > 0 || cache.ttl > 0) { + if cache.ttl > 0 && item.ttl == 0 { + item.ttl = cache.ttl + } + + if !cache.skipTTLExtension { + item.touch() + } + cache.priorityQueue.update(item) + } + + expirationNotification := false + if cache.expirationTime.After(time.Now().Add(item.ttl)) { + expirationNotification = true + } + return item, exists, expirationNotification +} + +func (cache *Cache) startExpirationProcessing() { + timer := time.NewTimer(time.Hour) + for { + var sleepTime time.Duration + cache.mutex.Lock() + if cache.priorityQueue.Len() > 0 { + sleepTime = time.Until(cache.priorityQueue.items[0].expireAt) + if sleepTime < 0 && cache.priorityQueue.items[0].expireAt.IsZero() { + sleepTime = time.Hour + } else if sleepTime < 0 { + sleepTime = time.Microsecond + } + if cache.ttl > 0 { + sleepTime = min(sleepTime, cache.ttl) + } + + } else if cache.ttl > 0 { + sleepTime = cache.ttl + } else { + sleepTime = time.Hour + } + + cache.expirationTime = time.Now().Add(sleepTime) + cache.mutex.Unlock() + + timer.Reset(sleepTime) + select { + case shutdownFeedback := <-cache.shutdownSignal: + timer.Stop() + cache.mutex.Lock() + if cache.priorityQueue.Len() > 0 { + cache.evictjob() + } + cache.mutex.Unlock() + shutdownFeedback <- struct{}{} + return + case <-timer.C: + timer.Stop() + cache.mutex.Lock() + if cache.priorityQueue.Len() == 0 { + cache.mutex.Unlock() + continue + } + + cache.cleanjob() + cache.mutex.Unlock() + + case <-cache.expirationNotification: + timer.Stop() + continue + } + } +} + +func (cache *Cache) evictjob() { + // index will only be advanced if the current entry will not be evicted + i := 0 + for item := cache.priorityQueue.items[i]; ; item = cache.priorityQueue.items[i] { + + cache.priorityQueue.remove(item) + delete(cache.items, item.key) + if cache.expireCallback != nil { + go cache.expireCallback(item.key, item.data) + } + if cache.priorityQueue.Len() == 0 { + return + } + } +} + +func (cache *Cache) cleanjob() { + // index will only be advanced if the current entry will not be evicted + i := 0 + for item := cache.priorityQueue.items[i]; item.expired(); item = cache.priorityQueue.items[i] { + + if cache.checkExpireCallback != nil { + if !cache.checkExpireCallback(item.key, item.data) { + item.touch() + cache.priorityQueue.update(item) + i++ + if i == cache.priorityQueue.Len() { + break + } + continue + } + } + + cache.priorityQueue.remove(item) + delete(cache.items, item.key) + if cache.expireCallback != nil { + go cache.expireCallback(item.key, item.data) + } + if cache.priorityQueue.Len() == 0 { + return + } + } +} + +// Close calls Purge, and then stops the goroutine that does ttl checking, for a clean shutdown. +// The cache is no longer cleaning up after the first call to Close, repeated calls are safe though. +func (cache *Cache) Close() { + + cache.mutex.Lock() + if !cache.isShutDown { + cache.isShutDown = true + cache.mutex.Unlock() + feedback := make(chan struct{}) + cache.shutdownSignal <- feedback + <-feedback + close(cache.shutdownSignal) + } else { + cache.mutex.Unlock() + } + cache.Purge() +} + +// Set is a thread-safe way to add new items to the map +func (cache *Cache) Set(key string, data interface{}) { + cache.SetWithTTL(key, data, ItemExpireWithGlobalTTL) +} + +// SetWithTTL is a thread-safe way to add new items to the map with individual ttl +func (cache *Cache) SetWithTTL(key string, data interface{}, ttl time.Duration) { + cache.mutex.Lock() + item, exists, _ := cache.getItem(key) + + if exists { + item.data = data + item.ttl = ttl + } else { + item = newItem(key, data, ttl) + cache.items[key] = item + } + + if item.ttl >= 0 && (item.ttl > 0 || cache.ttl > 0) { + if cache.ttl > 0 && item.ttl == 0 { + item.ttl = cache.ttl + } + item.touch() + } + + if exists { + cache.priorityQueue.update(item) + } else { + cache.priorityQueue.push(item) + } + + cache.mutex.Unlock() + if !exists && cache.newItemCallback != nil { + cache.newItemCallback(key, data) + } + cache.expirationNotification <- true +} + +// Get is a thread-safe way to lookup items +// Every lookup, also touches the item, hence extending it's life +func (cache *Cache) Get(key string) (interface{}, bool) { + cache.mutex.Lock() + item, exists, triggerExpirationNotification := cache.getItem(key) + + var dataToReturn interface{} + if exists { + dataToReturn = item.data + } + cache.mutex.Unlock() + if triggerExpirationNotification { + cache.expirationNotification <- true + } + return dataToReturn, exists +} + +func (cache *Cache) Remove(key string) bool { + cache.mutex.Lock() + object, exists := cache.items[key] + if !exists { + cache.mutex.Unlock() + return false + } + delete(cache.items, object.key) + cache.priorityQueue.remove(object) + cache.mutex.Unlock() + + return true +} + +// Count returns the number of items in the cache +func (cache *Cache) Count() int { + cache.mutex.Lock() + length := len(cache.items) + cache.mutex.Unlock() + return length +} + +func (cache *Cache) SetTTL(ttl time.Duration) { + cache.mutex.Lock() + cache.ttl = ttl + cache.mutex.Unlock() + cache.expirationNotification <- true +} + +// SetExpirationCallback sets a callback that will be called when an item expires +func (cache *Cache) SetExpirationCallback(callback expireCallback) { + cache.expireCallback = callback +} + +// SetCheckExpirationCallback sets a callback that will be called when an item is about to expire +// in order to allow external code to decide whether the item expires or remains for another TTL cycle +func (cache *Cache) SetCheckExpirationCallback(callback checkExpireCallback) { + cache.checkExpireCallback = callback +} + +// SetNewItemCallback sets a callback that will be called when a new item is added to the cache +func (cache *Cache) SetNewItemCallback(callback expireCallback) { + cache.newItemCallback = callback +} + +// SkipTtlExtensionOnHit allows the user to change the cache behaviour. When this flag is set to true it will +// no longer extend TTL of items when they are retrieved using Get, or when their expiration condition is evaluated +// using SetCheckExpirationCallback. +func (cache *Cache) SkipTtlExtensionOnHit(value bool) { + cache.skipTTLExtension = value +} + +// Purge will remove all entries +func (cache *Cache) Purge() { + cache.mutex.Lock() + cache.items = make(map[string]*item) + cache.priorityQueue = newPriorityQueue() + cache.mutex.Unlock() +} + +// NewCache is a helper to create instance of the Cache struct +func NewCache() *Cache { + + shutdownChan := make(chan chan struct{}) + + cache := &Cache{ + items: make(map[string]*item), + priorityQueue: newPriorityQueue(), + expirationNotification: make(chan bool), + expirationTime: time.Now(), + shutdownSignal: shutdownChan, + isShutDown: false, + } + go cache.startExpirationProcessing() + return cache +} + +func min(duration time.Duration, second time.Duration) time.Duration { + if duration < second { + return duration + } + return second +} diff --git a/vendor/github.com/ReneKroon/ttlcache/go.mod b/vendor/github.com/ReneKroon/ttlcache/go.mod new file mode 100644 index 000000000..6806b2859 --- /dev/null +++ b/vendor/github.com/ReneKroon/ttlcache/go.mod @@ -0,0 +1,9 @@ +module github.com/ReneKroon/ttlcache + +go 1.14 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/stretchr/testify v1.3.0 + go.uber.org/goleak v0.10.0 +) diff --git a/vendor/github.com/ReneKroon/ttlcache/go.sum b/vendor/github.com/ReneKroon/ttlcache/go.sum new file mode 100644 index 000000000..5701e60f9 --- /dev/null +++ b/vendor/github.com/ReneKroon/ttlcache/go.sum @@ -0,0 +1,11 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +go.uber.org/goleak v0.10.0 h1:G3eWbSNIskeRqtsN/1uI5B+eP73y3JUuBsv9AZjehb4= +go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI= diff --git a/vendor/github.com/ReneKroon/ttlcache/item.go b/vendor/github.com/ReneKroon/ttlcache/item.go new file mode 100644 index 000000000..2f78f49cc --- /dev/null +++ b/vendor/github.com/ReneKroon/ttlcache/item.go @@ -0,0 +1,46 @@ +package ttlcache + +import ( + "time" +) + +const ( + // ItemNotExpire Will avoid the item being expired by TTL, but can still be exired by callback etc. + ItemNotExpire time.Duration = -1 + // ItemExpireWithGlobalTTL will use the global TTL when set. + ItemExpireWithGlobalTTL time.Duration = 0 +) + +func newItem(key string, data interface{}, ttl time.Duration) *item { + item := &item{ + data: data, + ttl: ttl, + key: key, + } + // since nobody is aware yet of this item, it's safe to touch without lock here + item.touch() + return item +} + +type item struct { + key string + data interface{} + ttl time.Duration + expireAt time.Time + queueIndex int +} + +// Reset the item expiration time +func (item *item) touch() { + if item.ttl > 0 { + item.expireAt = time.Now().Add(item.ttl) + } +} + +// Verify if the item is expired +func (item *item) expired() bool { + if item.ttl <= 0 { + return false + } + return item.expireAt.Before(time.Now()) +} diff --git a/vendor/github.com/ReneKroon/ttlcache/priority_queue.go b/vendor/github.com/ReneKroon/ttlcache/priority_queue.go new file mode 100644 index 000000000..11b9c3140 --- /dev/null +++ b/vendor/github.com/ReneKroon/ttlcache/priority_queue.go @@ -0,0 +1,71 @@ +package ttlcache + +import ( + "container/heap" +) + +func newPriorityQueue() *priorityQueue { + queue := &priorityQueue{} + heap.Init(queue) + return queue +} + +type priorityQueue struct { + items []*item +} + +func (pq *priorityQueue) update(item *item) { + heap.Fix(pq, item.queueIndex) +} + +func (pq *priorityQueue) push(item *item) { + heap.Push(pq, item) +} + +func (pq *priorityQueue) pop() *item { + if pq.Len() == 0 { + return nil + } + return heap.Pop(pq).(*item) +} + +func (pq *priorityQueue) remove(item *item) { + heap.Remove(pq, item.queueIndex) +} + +func (pq priorityQueue) Len() int { + length := len(pq.items) + return length +} + +// Less will consider items with time.Time default value (epoch start) as more than set items. +func (pq priorityQueue) Less(i, j int) bool { + if pq.items[i].expireAt.IsZero() { + return false + } + if pq.items[j].expireAt.IsZero() { + return true + } + return pq.items[i].expireAt.Before(pq.items[j].expireAt) +} + +func (pq priorityQueue) Swap(i, j int) { + pq.items[i], pq.items[j] = pq.items[j], pq.items[i] + pq.items[i].queueIndex = i + pq.items[j].queueIndex = j +} + +func (pq *priorityQueue) Push(x interface{}) { + item := x.(*item) + item.queueIndex = len(pq.items) + pq.items = append(pq.items, item) +} + +func (pq *priorityQueue) Pop() interface{} { + old := pq.items + n := len(old) + item := old[n-1] + item.queueIndex = -1 + pq.items = old[0 : n-1] + return item +} diff --git a/vendor/modules.txt b/vendor/modules.txt index dc9739bd9..043a62463 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,3 +1,6 @@ +# github.com/ReneKroon/ttlcache v1.7.0 +## explicit +github.com/ReneKroon/ttlcache # github.com/aymerick/douceur v0.2.0 github.com/aymerick/douceur/css github.com/aymerick/douceur/parser