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