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/internal/api/security/signaturecheck.go b/internal/api/security/signaturecheck.go
index a3a04180d..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 err == db.ErrNoEntries {
- // 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 3f797beb6..5a570cad4 100644
--- a/internal/cache/cache.go
+++ b/internal/cache/cache.go
@@ -35,11 +35,11 @@ type cache struct {
// New returns a new in-memory cache.
func New() Cache {
- cache := &cache{
+ cache := &cache{
stored: &sync.Map{},
}
- go cache.sweep()
- return cache
+ go cache.sweep()
+ return cache
}
type cacheEntry struct {
diff --git a/internal/cache/error.go b/internal/cache/error.go
index df7cd8710..3f32aa7ce 100644
--- a/internal/cache/error.go
+++ b/internal/cache/error.go
@@ -20,8 +20,8 @@ package cache
import "errors"
-// CacheError models an error returned by the in-memory cache.
-type CacheError error
+// 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/account.go b/internal/db/account.go
index 29194b551..15c6839f8 100644
--- a/internal/db/account.go
+++ b/internal/db/account.go
@@ -24,68 +24,69 @@ import (
"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, DBError)
+ 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, DBError)
+ 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, DBError)
+ GetAccountByURL(uri string) (*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) (*gtsmodel.Account, DBError)
+ GetLocalAccountByUsername(username string) (*gtsmodel.Account, Error)
// GetAccountFollowRequests 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
- GetAccountFollowRequests(accountID string, followRequests *[]gtsmodel.FollowRequest) DBError
+ GetAccountFollowRequests(accountID string, followRequests *[]gtsmodel.FollowRequest) Error
// GetAccountFollowing 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
- GetAccountFollowing(accountID string, following *[]gtsmodel.Follow) DBError
+ GetAccountFollowing(accountID string, following *[]gtsmodel.Follow) Error
- CountAccountFollowing(accountID string, localOnly bool) (int, DBError)
+ CountAccountFollowing(accountID string, localOnly bool) (int, Error)
// GetAccountFollowers 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.
- GetAccountFollowers(accountID string, followers *[]gtsmodel.Follow, localOnly bool) DBError
+ GetAccountFollowers(accountID string, followers *[]gtsmodel.Follow, localOnly bool) Error
- CountAccountFollowers(accountID string, localOnly bool) (int, DBError)
+ CountAccountFollowers(accountID string, localOnly bool) (int, Error)
// GetAccountFaves 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
- GetAccountFaves(accountID string, faves *[]gtsmodel.StatusFave) DBError
+ GetAccountFaves(accountID string, faves *[]gtsmodel.StatusFave) Error
// GetAccountStatusesCount is a shortcut for the common action of counting statuses produced by accountID.
- CountAccountStatuses(accountID string) (int, DBError)
+ 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, DBError)
+ 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, DBError)
+ 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, DBError)
+ 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) DBError
+ 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, DBError)
+ GetInstanceAccount(domain string) (*gtsmodel.Account, Error)
}
diff --git a/internal/db/admin.go b/internal/db/admin.go
index c9cc96117..aa2b22f47 100644
--- a/internal/db/admin.go
+++ b/internal/db/admin.go
@@ -24,29 +24,30 @@ import (
"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) DBError
+ 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) DBError
+ 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, DBError)
+ 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() DBError
+ 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() DBError
+ CreateInstanceInstance() Error
}
diff --git a/internal/db/basic.go b/internal/db/basic.go
index 558bf8d91..729920bba 100644
--- a/internal/db/basic.go
+++ b/internal/db/basic.go
@@ -20,67 +20,68 @@ 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{}) DBError
+ 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{}) DBError
+ 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{}) DBError
+ 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) DBError
+ Stop(ctx context.Context) Error
// IsHealthy should return nil if the database connection is healthy, or an error if not.
- IsHealthy(ctx context.Context) DBError
+ 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{}) DBError
+ 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{}) DBError
+ 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{}) DBError
+ 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{}) DBError
+ 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) DBError
+ 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{}) DBError
+ 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{}) DBError
+ 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{}) DBError
+ 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{}) DBError
+ 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{}) DBError
+ DeleteWhere(where []Where, i interface{}) Error
}
diff --git a/internal/db/db.go b/internal/db/db.go
index d74eb27ed..d6ac883e4 100644
--- a/internal/db/db.go
+++ b/internal/db/db.go
@@ -27,13 +27,12 @@ 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 {
Account
Admin
Basic
+ Domain
Instance
Media
Mention
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 9ccc37b3e..c13bd78dd 100644
--- a/internal/db/error.go
+++ b/internal/db/error.go
@@ -20,11 +20,16 @@ package db
import "fmt"
-type DBError error
+// Error denotes a database error.
+type Error error
var (
- ErrNoEntries DBError = fmt.Errorf("no entries")
- ErrMultipleEntries DBError = fmt.Errorf("multiple entries")
- ErrAlreadyExists DBError = fmt.Errorf("already exists")
- ErrUnknown DBError = fmt.Errorf("unknown error")
+ // 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
index 3268b2085..afac8266c 100644
--- a/internal/db/instance.go
+++ b/internal/db/instance.go
@@ -20,16 +20,17 @@ package db
import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+// Instance contains functions for instance-level actions (counting instance users etc.).
type Instance interface {
// GetUserCountForInstance returns the number of known accounts registered with the given domain.
- GetUserCountForInstance(domain string) (int, DBError)
+ GetUserCountForInstance(domain string) (int, Error)
// GetStatusCountForInstance returns the number of known statuses posted from the given domain.
- GetStatusCountForInstance(domain string) (int, DBError)
+ GetStatusCountForInstance(domain string) (int, Error)
// GetDomainCountForInstance returns the number of known instances known that the given domain federates with.
- GetDomainCountForInstance(domain string) (int, DBError)
+ 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, DBError)
+ GetAccountsForInstance(domain string, maxID string, limit int) ([]*gtsmodel.Account, Error)
}
diff --git a/internal/db/media.go b/internal/db/media.go
index a677ad019..db4db3411 100644
--- a/internal/db/media.go
+++ b/internal/db/media.go
@@ -20,7 +20,8 @@ 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, DBError)
+ GetAttachmentByID(id string) (*gtsmodel.MediaAttachment, Error)
}
diff --git a/internal/db/mention.go b/internal/db/mention.go
index 0e6216508..cb1c56dc1 100644
--- a/internal/db/mention.go
+++ b/internal/db/mention.go
@@ -20,10 +20,11 @@ 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, DBError)
+ GetMention(id string) (*gtsmodel.Mention, Error)
// GetMentions gets multiple mentions.
- GetMentions(ids []string) ([]*gtsmodel.Mention, DBError)
+ GetMentions(ids []string) ([]*gtsmodel.Mention, Error)
}
diff --git a/internal/db/notification.go b/internal/db/notification.go
index 26bcc0a79..b8e2829e7 100644
--- a/internal/db/notification.go
+++ b/internal/db/notification.go
@@ -20,7 +20,8 @@ package db
import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+// Notification contains functions for creating and getting notifications.
type Notification interface {
// GetNotificationsForAccount returns a list of notifications that pertain to the given accountID.
- GetNotificationsForAccount(accountID string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, DBError)
+ GetNotificationsForAccount(accountID string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, Error)
}
diff --git a/internal/db/pg/account.go b/internal/db/pg/account.go
index 4d17488d8..485c66e4f 100644
--- a/internal/db/pg/account.go
+++ b/internal/db/pg/account.go
@@ -45,7 +45,7 @@ func (a *accountDB) newAccountQ(account *gtsmodel.Account) *orm.Query {
Relation("HeaderMediaAttachment")
}
-func (a *accountDB) GetAccountByID(id string) (*gtsmodel.Account, db.DBError) {
+func (a *accountDB) GetAccountByID(id string) (*gtsmodel.Account, db.Error) {
account := >smodel.Account{}
q := a.newAccountQ(account).
@@ -56,7 +56,7 @@ func (a *accountDB) GetAccountByID(id string) (*gtsmodel.Account, db.DBError) {
return account, err
}
-func (a *accountDB) GetAccountByURI(uri string) (*gtsmodel.Account, db.DBError) {
+func (a *accountDB) GetAccountByURI(uri string) (*gtsmodel.Account, db.Error) {
account := >smodel.Account{}
q := a.newAccountQ(account).
@@ -67,7 +67,7 @@ func (a *accountDB) GetAccountByURI(uri string) (*gtsmodel.Account, db.DBError)
return account, err
}
-func (a *accountDB) GetAccountByURL(uri string) (*gtsmodel.Account, db.DBError) {
+func (a *accountDB) GetAccountByURL(uri string) (*gtsmodel.Account, db.Error) {
account := >smodel.Account{}
q := a.newAccountQ(account).
@@ -78,7 +78,7 @@ func (a *accountDB) GetAccountByURL(uri string) (*gtsmodel.Account, db.DBError)
return account, err
}
-func (a *accountDB) GetInstanceAccount(domain string) (*gtsmodel.Account, db.DBError) {
+func (a *accountDB) GetInstanceAccount(domain string) (*gtsmodel.Account, db.Error) {
account := >smodel.Account{}
q := a.newAccountQ(account)
@@ -98,7 +98,7 @@ func (a *accountDB) GetInstanceAccount(domain string) (*gtsmodel.Account, db.DBE
return account, err
}
-func (a *accountDB) GetAccountLastPosted(accountID string) (time.Time, db.DBError) {
+func (a *accountDB) GetAccountLastPosted(accountID string) (time.Time, db.Error) {
status := >smodel.Status{}
q := a.conn.Model(status).
@@ -112,7 +112,7 @@ func (a *accountDB) GetAccountLastPosted(accountID string) (time.Time, db.DBErro
return status.CreatedAt, err
}
-func (a *accountDB) SetAccountHeaderOrAvatar(mediaAttachment *gtsmodel.MediaAttachment, accountID string) db.DBError {
+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")
}
@@ -137,7 +137,7 @@ func (a *accountDB) SetAccountHeaderOrAvatar(mediaAttachment *gtsmodel.MediaAtta
return nil
}
-func (a *accountDB) GetLocalAccountByUsername(username string) (*gtsmodel.Account, db.DBError) {
+func (a *accountDB) GetLocalAccountByUsername(username string) (*gtsmodel.Account, db.Error) {
account := >smodel.Account{}
q := a.newAccountQ(account).
@@ -149,7 +149,7 @@ func (a *accountDB) GetLocalAccountByUsername(username string) (*gtsmodel.Accoun
return account, err
}
-func (a *accountDB) GetAccountFollowRequests(accountID string, followRequests *[]gtsmodel.FollowRequest) db.DBError {
+func (a *accountDB) GetAccountFollowRequests(accountID string, followRequests *[]gtsmodel.FollowRequest) db.Error {
if err := a.conn.Model(followRequests).Where("target_account_id = ?", accountID).Select(); err != nil {
if err == pg.ErrNoRows {
return nil
@@ -159,7 +159,7 @@ func (a *accountDB) GetAccountFollowRequests(accountID string, followRequests *[
return nil
}
-func (a *accountDB) GetAccountFollowing(accountID string, following *[]gtsmodel.Follow) db.DBError {
+func (a *accountDB) GetAccountFollowing(accountID string, following *[]gtsmodel.Follow) db.Error {
if err := a.conn.Model(following).Where("account_id = ?", accountID).Select(); err != nil {
if err == pg.ErrNoRows {
return nil
@@ -169,11 +169,11 @@ func (a *accountDB) GetAccountFollowing(accountID string, following *[]gtsmodel.
return nil
}
-func (a *accountDB) CountAccountFollowing(accountID string, localOnly bool) (int, db.DBError) {
+func (a *accountDB) CountAccountFollowing(accountID string, localOnly bool) (int, db.Error) {
return a.conn.Model(&[]*gtsmodel.Follow{}).Where("account_id = ?", accountID).Count()
}
-func (a *accountDB) GetAccountFollowers(accountID string, followers *[]gtsmodel.Follow, localOnly bool) db.DBError {
+func (a *accountDB) GetAccountFollowers(accountID string, followers *[]gtsmodel.Follow, localOnly bool) db.Error {
q := a.conn.Model(followers)
@@ -203,11 +203,11 @@ func (a *accountDB) GetAccountFollowers(accountID string, followers *[]gtsmodel.
return nil
}
-func (a *accountDB) CountAccountFollowers(accountID string, localOnly bool) (int, db.DBError) {
+func (a *accountDB) CountAccountFollowers(accountID string, localOnly bool) (int, db.Error) {
return a.conn.Model(&[]*gtsmodel.Follow{}).Where("target_account_id = ?", accountID).Count()
}
-func (a *accountDB) GetAccountFaves(accountID string, faves *[]gtsmodel.StatusFave) db.DBError {
+func (a *accountDB) GetAccountFaves(accountID string, faves *[]gtsmodel.StatusFave) db.Error {
if err := a.conn.Model(faves).Where("account_id = ?", accountID).Select(); err != nil {
if err == pg.ErrNoRows {
return nil
@@ -217,11 +217,11 @@ func (a *accountDB) GetAccountFaves(accountID string, faves *[]gtsmodel.StatusFa
return nil
}
-func (a *accountDB) CountAccountStatuses(accountID string) (int, db.DBError) {
+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.DBError) {
+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{}
@@ -267,7 +267,7 @@ func (a *accountDB) GetAccountStatuses(accountID string, limit int, excludeRepli
return statuses, nil
}
-func (a *accountDB) GetAccountBlocks(accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, db.DBError) {
+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).
diff --git a/internal/db/pg/admin.go b/internal/db/pg/admin.go
index fe3d48b54..854f56ef0 100644
--- a/internal/db/pg/admin.go
+++ b/internal/db/pg/admin.go
@@ -45,7 +45,7 @@ type adminDB struct {
cancel context.CancelFunc
}
-func (a *adminDB) IsUsernameAvailable(username string) db.DBError {
+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
@@ -57,7 +57,7 @@ func (a *adminDB) IsUsernameAvailable(username string) db.DBError {
return nil
}
-func (a *adminDB) IsEmailAvailable(email string) db.DBError {
+func (a *adminDB) IsEmailAvailable(email string) db.Error {
// parse the domain from the email
m, err := mail.ParseAddress(email)
if err != nil {
@@ -85,7 +85,7 @@ func (a *adminDB) IsEmailAvailable(email string) db.DBError {
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.DBError) {
+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)
@@ -168,7 +168,7 @@ func (a *adminDB) NewSignup(username string, reason string, requireApproval bool
return u, nil
}
-func (a *adminDB) CreateInstanceAccount() db.DBError {
+func (a *adminDB) CreateInstanceAccount() db.Error {
username := a.config.Host
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
@@ -210,7 +210,7 @@ func (a *adminDB) CreateInstanceAccount() db.DBError {
return nil
}
-func (a *adminDB) CreateInstanceInstance() db.DBError {
+func (a *adminDB) CreateInstanceInstance() db.Error {
iID, err := id.NewRandomULID()
if err != nil {
return err
diff --git a/internal/db/pg/basic.go b/internal/db/pg/basic.go
index 337c6add8..6e76b4450 100644
--- a/internal/db/pg/basic.go
+++ b/internal/db/pg/basic.go
@@ -38,7 +38,7 @@ type basicDB struct {
cancel context.CancelFunc
}
-func (b *basicDB) Put(i interface{}) db.DBError {
+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
@@ -46,7 +46,7 @@ func (b *basicDB) Put(i interface{}) db.DBError {
return err
}
-func (b *basicDB) GetByID(id string, i interface{}) db.DBError {
+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
@@ -57,7 +57,7 @@ func (b *basicDB) GetByID(id string, i interface{}) db.DBError {
return nil
}
-func (b *basicDB) GetWhere(where []db.Where, i interface{}) db.DBError {
+func (b *basicDB) GetWhere(where []db.Where, i interface{}) db.Error {
if len(where) == 0 {
return errors.New("no queries provided")
}
@@ -85,7 +85,7 @@ func (b *basicDB) GetWhere(where []db.Where, i interface{}) db.DBError {
return nil
}
-func (b *basicDB) GetAll(i interface{}) db.DBError {
+func (b *basicDB) GetAll(i interface{}) db.Error {
if err := b.conn.Model(i).Select(); err != nil {
if err == pg.ErrNoRows {
return db.ErrNoEntries
@@ -95,7 +95,7 @@ func (b *basicDB) GetAll(i interface{}) db.DBError {
return nil
}
-func (b *basicDB) DeleteByID(id string, i interface{}) db.DBError {
+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
@@ -106,7 +106,7 @@ func (b *basicDB) DeleteByID(id string, i interface{}) db.DBError {
return nil
}
-func (b *basicDB) DeleteWhere(where []db.Where, i interface{}) db.DBError {
+func (b *basicDB) DeleteWhere(where []db.Where, i interface{}) db.Error {
if len(where) == 0 {
return errors.New("no queries provided")
}
@@ -126,7 +126,7 @@ func (b *basicDB) DeleteWhere(where []db.Where, i interface{}) db.DBError {
return nil
}
-func (b *basicDB) Upsert(i interface{}, conflictColumn string) db.DBError {
+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
@@ -136,7 +136,7 @@ func (b *basicDB) Upsert(i interface{}, conflictColumn string) db.DBError {
return nil
}
-func (b *basicDB) UpdateByID(id string, i interface{}) db.DBError {
+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
@@ -146,12 +146,12 @@ func (b *basicDB) UpdateByID(id string, i interface{}) db.DBError {
return nil
}
-func (b *basicDB) UpdateOneByID(id string, key string, value interface{}, i interface{}) db.DBError {
+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.DBError {
+func (b *basicDB) UpdateWhere(where []db.Where, key string, value interface{}, i interface{}) db.Error {
q := b.conn.Model(i)
for _, w := range where {
@@ -173,28 +173,28 @@ func (b *basicDB) UpdateWhere(where []db.Where, key string, value interface{}, i
return err
}
-func (b *basicDB) CreateTable(i interface{}) db.DBError {
+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.DBError {
+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.DBError {
+func (b *basicDB) RegisterTable(i interface{}) db.Error {
orm.RegisterTable(i)
return nil
}
-func (b *basicDB) IsHealthy(ctx context.Context) db.DBError {
+func (b *basicDB) IsHealthy(ctx context.Context) db.Error {
return b.conn.Ping(ctx)
}
-func (b *basicDB) Stop(ctx context.Context) db.DBError {
+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
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/instance.go b/internal/db/pg/instance.go
index e2e92b23d..71ef06e0a 100644
--- a/internal/db/pg/instance.go
+++ b/internal/db/pg/instance.go
@@ -35,7 +35,7 @@ type instanceDB struct {
cancel context.CancelFunc
}
-func (i *instanceDB) GetUserCountForInstance(domain string) (int, db.DBError) {
+func (i *instanceDB) GetUserCountForInstance(domain string) (int, db.Error) {
q := i.conn.Model(&[]*gtsmodel.Account{})
if domain == i.config.Host {
@@ -51,7 +51,7 @@ func (i *instanceDB) GetUserCountForInstance(domain string) (int, db.DBError) {
return q.Count()
}
-func (i *instanceDB) GetStatusCountForInstance(domain string) (int, db.DBError) {
+func (i *instanceDB) GetStatusCountForInstance(domain string) (int, db.Error) {
q := i.conn.Model(&[]*gtsmodel.Status{})
if domain == i.config.Host {
@@ -66,7 +66,7 @@ func (i *instanceDB) GetStatusCountForInstance(domain string) (int, db.DBError)
return q.Count()
}
-func (i *instanceDB) GetDomainCountForInstance(domain string) (int, db.DBError) {
+func (i *instanceDB) GetDomainCountForInstance(domain string) (int, db.Error) {
q := i.conn.Model(&[]*gtsmodel.Instance{})
if domain == i.config.Host {
@@ -81,7 +81,7 @@ func (i *instanceDB) GetDomainCountForInstance(domain string) (int, db.DBError)
return q.Count()
}
-func (i *instanceDB) GetAccountsForInstance(domain string, maxID string, limit int) ([]*gtsmodel.Account, db.DBError) {
+func (i *instanceDB) GetAccountsForInstance(domain string, maxID string, limit int) ([]*gtsmodel.Account, db.Error) {
i.log.Debug("GetAccountsForInstance")
accounts := []*gtsmodel.Account{}
diff --git a/internal/db/pg/media.go b/internal/db/pg/media.go
index dff301fa5..618030af3 100644
--- a/internal/db/pg/media.go
+++ b/internal/db/pg/media.go
@@ -41,7 +41,7 @@ func (m *mediaDB) newMediaQ(i interface{}) *orm.Query {
Relation("Account")
}
-func (m *mediaDB) GetAttachmentByID(id string) (*gtsmodel.MediaAttachment, db.DBError) {
+func (m *mediaDB) GetAttachmentByID(id string) (*gtsmodel.MediaAttachment, db.Error) {
attachment := >smodel.MediaAttachment{}
q := m.newMediaQ(attachment).
diff --git a/internal/db/pg/mention.go b/internal/db/pg/mention.go
index 7ab395756..b31f07b67 100644
--- a/internal/db/pg/mention.go
+++ b/internal/db/pg/mention.go
@@ -24,6 +24,7 @@ import (
"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"
@@ -34,6 +35,36 @@ type mentionDB struct {
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 {
@@ -43,7 +74,11 @@ func (m *mentionDB) newMentionQ(i interface{}) *orm.Query {
Relation("TargetAccount")
}
-func (m *mentionDB) GetMention(id string) (*gtsmodel.Mention, db.DBError) {
+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).
@@ -51,20 +86,23 @@ func (m *mentionDB) GetMention(id string) (*gtsmodel.Mention, db.DBError) {
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.DBError) {
+func (m *mentionDB) GetMentions(ids []string) ([]*gtsmodel.Mention, db.Error) {
mentions := []*gtsmodel.Mention{}
- if len(ids) == 0 {
- return mentions, nil
+ for _, i := range ids {
+ mention, err := m.GetMention(i)
+ if err != nil {
+ return nil, processErrorResponse(err)
+ }
+ mentions = append(mentions, mention)
}
- q := m.newMentionQ(&mentions).
- Where("mention.id in (?)", pg.In(ids))
-
- err := processErrorResponse(q.Select())
-
- return mentions, err
+ return mentions, nil
}
diff --git a/internal/db/pg/notification.go b/internal/db/pg/notification.go
index 84359a981..ac3a2149b 100644
--- a/internal/db/pg/notification.go
+++ b/internal/db/pg/notification.go
@@ -24,7 +24,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
-func (ps *postgresService) GetNotificationsForAccount(accountID string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, db.DBError) {
+func (ps *postgresService) GetNotificationsForAccount(accountID string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, db.Error) {
notifications := []*gtsmodel.Notification{}
q := ps.conn.Model(¬ifications).Where("target_account_id = ?", accountID)
diff --git a/internal/db/pg/pg.go b/internal/db/pg/pg.go
index f63c62cf4..9d9c8e572 100644
--- a/internal/db/pg/pg.go
+++ b/internal/db/pg/pg.go
@@ -49,6 +49,7 @@ type postgresService struct {
db.Account
db.Admin
db.Basic
+ db.Domain
db.Instance
db.Media
db.Mention
@@ -123,6 +124,12 @@ func NewPostgresService(ctx context.Context, c *config.Config, log *logrus.Logge
log: log,
cancel: cancel,
},
+ Domain: &domainDB{
+ config: c,
+ conn: conn,
+ log: log,
+ cancel: cancel,
+ },
Instance: &instanceDB{
config: c,
conn: conn,
diff --git a/internal/db/pg/relationship.go b/internal/db/pg/relationship.go
index aa4f35104..e9349ce7f 100644
--- a/internal/db/pg/relationship.go
+++ b/internal/db/pg/relationship.go
@@ -43,7 +43,7 @@ func (r *relationshipDB) newBlockQ(block *gtsmodel.Block) *orm.Query {
Relation("TargetAccount")
}
-func (r *relationshipDB) processResponse(block *gtsmodel.Block, err error) (*gtsmodel.Block, db.DBError) {
+func (r *relationshipDB) processResponse(block *gtsmodel.Block, err error) (*gtsmodel.Block, db.Error) {
switch err {
case pg.ErrNoRows:
return nil, db.ErrNoEntries
@@ -54,7 +54,7 @@ func (r *relationshipDB) processResponse(block *gtsmodel.Block, err error) (*gts
}
}
-func (r *relationshipDB) Blocked(account1 string, account2 string, eitherDirection bool) (bool, db.DBError) {
+func (r *relationshipDB) Blocked(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 {
@@ -64,7 +64,7 @@ func (r *relationshipDB) Blocked(account1 string, account2 string, eitherDirecti
return q.Exists()
}
-func (r *relationshipDB) GetBlock(account1 string, account2 string) (*gtsmodel.Block, db.DBError) {
+func (r *relationshipDB) GetBlock(account1 string, account2 string) (*gtsmodel.Block, db.Error) {
block := >smodel.Block{}
q := r.newBlockQ(block).
@@ -74,7 +74,7 @@ func (r *relationshipDB) GetBlock(account1 string, account2 string) (*gtsmodel.B
return r.processResponse(block, q.Select())
}
-func (r *relationshipDB) GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, db.DBError) {
+func (r *relationshipDB) GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, db.Error) {
rel := >smodel.Relationship{
ID: targetAccount,
}
@@ -128,7 +128,7 @@ func (r *relationshipDB) GetRelationship(requestingAccount string, targetAccount
return rel, nil
}
-func (r *relationshipDB) Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, db.DBError) {
+func (r *relationshipDB) Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, db.Error) {
if sourceAccount == nil || targetAccount == nil {
return false, nil
}
@@ -136,7 +136,7 @@ func (r *relationshipDB) Follows(sourceAccount *gtsmodel.Account, targetAccount
return r.conn.Model(>smodel.Follow{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists()
}
-func (r *relationshipDB) FollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, db.DBError) {
+func (r *relationshipDB) FollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, db.Error) {
if sourceAccount == nil || targetAccount == nil {
return false, nil
}
@@ -144,7 +144,7 @@ func (r *relationshipDB) FollowRequested(sourceAccount *gtsmodel.Account, target
return r.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists()
}
-func (r *relationshipDB) Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, db.DBError) {
+func (r *relationshipDB) Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, db.Error) {
if account1 == nil || account2 == nil {
return false, nil
}
@@ -170,7 +170,7 @@ func (r *relationshipDB) Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.
return f1 && f2, nil
}
-func (r *relationshipDB) AcceptFollowRequest(originAccountID string, targetAccountID string) (*gtsmodel.Follow, db.DBError) {
+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 {
diff --git a/internal/db/pg/status.go b/internal/db/pg/status.go
index 227c48353..99790428e 100644
--- a/internal/db/pg/status.go
+++ b/internal/db/pg/status.go
@@ -27,6 +27,7 @@ import (
"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"
@@ -37,6 +38,36 @@ type statusDB struct {
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 {
@@ -60,7 +91,11 @@ func (s *statusDB) newFaveQ(faves interface{}) *orm.Query {
Relation("Status")
}
-func (s *statusDB) GetStatusByID(id string) (*gtsmodel.Status, db.DBError) {
+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).
@@ -68,10 +103,18 @@ func (s *statusDB) GetStatusByID(id string) (*gtsmodel.Status, db.DBError) {
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.DBError) {
+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).
@@ -79,10 +122,18 @@ func (s *statusDB) GetStatusByURI(uri string) (*gtsmodel.Status, db.DBError) {
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.DBError) {
+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).
@@ -90,10 +141,14 @@ func (s *statusDB) GetStatusByURL(uri string) (*gtsmodel.Status, db.DBError) {
err := processErrorResponse(q.Select())
+ if err == nil && status != nil {
+ s.cacheStatus(uri, status)
+ }
+
return status, err
}
-func (s *statusDB) PutStatus(status *gtsmodel.Status) db.DBError {
+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 {
@@ -133,7 +188,7 @@ func (s *statusDB) PutStatus(status *gtsmodel.Status) db.DBError {
return processErrorResponse(s.conn.RunInTransaction(context.Background(), transaction))
}
-func (s *statusDB) GetStatusParents(status *gtsmodel.Status, onlyDirect bool) ([]*gtsmodel.Status, db.DBError) {
+func (s *statusDB) GetStatusParents(status *gtsmodel.Status, onlyDirect bool) ([]*gtsmodel.Status, db.Error) {
parents := []*gtsmodel.Status{}
s.statusParent(status, &parents, onlyDirect)
@@ -157,7 +212,7 @@ func (s *statusDB) statusParent(status *gtsmodel.Status, foundStatuses *[]*gtsmo
s.statusParent(parentStatus, foundStatuses, false)
}
-func (s *statusDB) GetStatusChildren(status *gtsmodel.Status, onlyDirect bool, minID string) ([]*gtsmodel.Status, db.DBError) {
+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)
@@ -212,35 +267,35 @@ func (s *statusDB) statusChildren(status *gtsmodel.Status, foundStatuses *list.L
}
}
-func (s *statusDB) CountStatusReplies(status *gtsmodel.Status) (int, db.DBError) {
+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.DBError) {
+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.DBError) {
+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.DBError) {
+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.DBError) {
+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.DBError) {
+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.DBError) {
+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.DBError) {
+func (s *statusDB) GetStatusFaves(status *gtsmodel.Status) ([]*gtsmodel.StatusFave, db.Error) {
faves := []*gtsmodel.StatusFave{}
q := s.newFaveQ(&faves).
@@ -251,7 +306,7 @@ func (s *statusDB) GetStatusFaves(status *gtsmodel.Status) ([]*gtsmodel.StatusFa
return faves, err
}
-func (s *statusDB) GetStatusReblogs(status *gtsmodel.Status) ([]*gtsmodel.Status, db.DBError) {
+func (s *statusDB) GetStatusReblogs(status *gtsmodel.Status) ([]*gtsmodel.Status, db.Error) {
reblogs := []*gtsmodel.Status{}
q := s.newStatusQ(&reblogs).
diff --git a/internal/db/pg/status_test.go b/internal/db/pg/status_test.go
index 9d699f0b9..8a185757c 100644
--- a/internal/db/pg/status_test.go
+++ b/internal/db/pg/status_test.go
@@ -19,7 +19,9 @@
package pg_test
import (
+ "fmt"
"testing"
+ "time"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/testrig"
@@ -94,6 +96,39 @@ func (suite *StatusTestSuite) TestGetStatusWithExtras() {
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/timeline.go b/internal/db/pg/timeline.go
index 87e306da8..41599a785 100644
--- a/internal/db/pg/timeline.go
+++ b/internal/db/pg/timeline.go
@@ -36,7 +36,7 @@ type timelineDB struct {
cancel context.CancelFunc
}
-func (t *timelineDB) GetHomeTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, db.DBError) {
+func (t *timelineDB) GetHomeTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, db.Error) {
statuses := []*gtsmodel.Status{}
q := t.conn.Model(&statuses)
@@ -96,7 +96,7 @@ func (t *timelineDB) GetHomeTimelineForAccount(accountID string, maxID string, s
return statuses, nil
}
-func (t *timelineDB) GetPublicTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, db.DBError) {
+func (t *timelineDB) GetPublicTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, db.Error) {
statuses := []*gtsmodel.Status{}
q := t.conn.Model(&statuses).
@@ -143,7 +143,7 @@ func (t *timelineDB) GetPublicTimelineForAccount(accountID string, maxID string,
// 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 (t *timelineDB) GetFavedTimelineForAccount(accountID string, maxID string, minID string, limit int) ([]*gtsmodel.Status, string, string, db.DBError) {
+func (t *timelineDB) GetFavedTimelineForAccount(accountID string, maxID string, minID string, limit int) ([]*gtsmodel.Status, string, string, db.Error) {
faves := []*gtsmodel.StatusFave{}
diff --git a/internal/db/pg/util.go b/internal/db/pg/util.go
index e6d901961..17c09b720 100644
--- a/internal/db/pg/util.go
+++ b/internal/db/pg/util.go
@@ -8,7 +8,7 @@ import (
)
// processErrorResponse parses the given error and returns an appropriate DBError.
-func processErrorResponse(err error) db.DBError {
+func processErrorResponse(err error) db.Error {
switch err {
case nil:
return nil
diff --git a/internal/db/relationship.go b/internal/db/relationship.go
index be171f9b1..8966b9145 100644
--- a/internal/db/relationship.go
+++ b/internal/db/relationship.go
@@ -20,32 +20,33 @@ package db
import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+// Relationship contains functions for getting or modifying the relationship between two accounts.
type Relationship interface {
// Blocked 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.
- Blocked(account1 string, account2 string, eitherDirection bool) (bool, DBError)
+ Blocked(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, DBError)
+ 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, DBError)
+ 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, DBError)
+ 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, DBError)
+ 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, DBError)
+ Mutuals(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, DBError)
+ AcceptFollowRequest(originAccountID string, targetAccountID string) (*gtsmodel.Follow, Error)
}
diff --git a/internal/db/status.go b/internal/db/status.go
index 2a6e527e2..9d206c198 100644
--- a/internal/db/status.go
+++ b/internal/db/status.go
@@ -20,55 +20,56 @@ 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, DBError)
+ 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, DBError)
+ 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, DBError)
+ GetStatusByURL(uri string) (*gtsmodel.Status, Error)
// PutStatus stores one status in the database.
- PutStatus(status *gtsmodel.Status) DBError
+ 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, DBError)
+ 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, DBError)
+ 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, DBError)
+ CountStatusFaves(status *gtsmodel.Status) (int, Error)
- // GetStatusParents get the parent statuses of a given status.
+ // 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, DBError)
+ 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, DBError)
+ 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, DBError)
+ 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, DBError)
+ 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, DBError)
+ 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, DBError)
+ 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, DBError)
+ 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, DBError)
+ GetStatusReblogs(status *gtsmodel.Status) ([]*gtsmodel.Status, Error)
}
diff --git a/internal/db/timeline.go b/internal/db/timeline.go
index 638d2d10d..ed84b268a 100644
--- a/internal/db/timeline.go
+++ b/internal/db/timeline.go
@@ -20,17 +20,18 @@ 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 {
// 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, DBError)
+ 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, DBError)
+ 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.
@@ -39,5 +40,5 @@ type Timeline interface {
// 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, DBError)
+ GetFavedTimelineForAccount(accountID string, maxID string, minID string, limit int) ([]*gtsmodel.Status, string, string, Error)
}
diff --git a/internal/gtsmodel/account.go b/internal/gtsmodel/account.go
index 369fd6b77..435caea6d 100644
--- a/internal/gtsmodel/account.go
+++ b/internal/gtsmodel/account.go
@@ -45,13 +45,13 @@ type Account struct {
*/
// ID of the avatar as a media attachment
- AvatarMediaAttachmentID string `pg:"type:CHAR(26)"`
- AvatarMediaAttachment *MediaAttachment `pg:"rel:has-one"`
+ 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)"`
- HeaderMediaAttachment *MediaAttachment `pg:"rel:has-one"`
+ 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 809250276..1bed86d8f 100644
--- a/internal/gtsmodel/domainblock.go
+++ b/internal/gtsmodel/domainblock.go
@@ -31,8 +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"`
- CreatedByAccount *Account `pg:"rel:belongs-to"`
+ 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 03618dd3a..374454374 100644
--- a/internal/gtsmodel/emaildomainblock.go
+++ b/internal/gtsmodel/emaildomainblock.go
@@ -31,6 +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"`
- CreatedByAccount *Account `pg:"rel:belongs-to"`
+ CreatedByAccountID string `pg:"type:CHAR(26),notnull"`
+ CreatedByAccount *Account `pg:"rel:belongs-to"`
}
diff --git a/internal/gtsmodel/follow.go b/internal/gtsmodel/follow.go
index 22f87768e..8f169f8c4 100644
--- a/internal/gtsmodel/follow.go
+++ b/internal/gtsmodel/follow.go
@@ -29,11 +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"`
- Account *Account `pg:"rel:belongs-to"`
+ 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"`
- TargetAccount *Account `pg:"rel:has-one"`
+ 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/instance.go b/internal/gtsmodel/instance.go
index e0eb1435a..7b453a0b3 100644
--- a/internal/gtsmodel/instance.go
+++ b/internal/gtsmodel/instance.go
@@ -19,8 +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)"`
- DomainBlock *DomainBlock `pg:"rel:has-one"`
+ DomainBlockID string `pg:"type:CHAR(26)"`
+ DomainBlock *DomainBlock `pg:"rel:has-one"`
// Short description of this instance
ShortDescription string
// Longer description of this instance
@@ -32,8 +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)"`
- ContactAccount *Account `pg:"rel:has-one"`
+ 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 4a8510bf4..0f12caaad 100644
--- a/internal/gtsmodel/mediaattachment.go
+++ b/internal/gtsmodel/mediaattachment.go
@@ -42,8 +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"`
- Account *Account `pg:"rel:belongs-to"`
+ 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 5252e80d7..931e681db 100644
--- a/internal/gtsmodel/mention.go
+++ b/internal/gtsmodel/mention.go
@@ -25,20 +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"`
- Status *Status `pg:"rel:belongs-to"`
+ 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"`
- OriginAccount *Account `pg:"rel:has-one"`
+ 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"`
- TargetAccount *Account `pg:"rel:has-one"`
+ TargetAccountID string `pg:"type:CHAR(26),notnull"`
+ TargetAccount *Account `pg:"rel:has-one"`
// Prevent this mention from generating a notification?
Silent bool
diff --git a/internal/gtsmodel/statusbookmark.go b/internal/gtsmodel/statusbookmark.go
index de66ebc87..468939bae 100644
--- a/internal/gtsmodel/statusbookmark.go
+++ b/internal/gtsmodel/statusbookmark.go
@@ -27,11 +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"`
- Account *Account `pg:"rel:belongs-to"`
+ 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"`
- TargetAccount *Account `pg:"rel:has-one"`
+ 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 012360bff..17952673a 100644
--- a/internal/gtsmodel/statusfave.go
+++ b/internal/gtsmodel/statusfave.go
@@ -27,14 +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"`
- Account *Account `pg:"rel:has-one"`
+ 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"`
- TargetAccount *Account `pg:"rel:has-one"`
+ 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"`
- Status *Status `pg:"rel:has-one"`
+ StatusID string `pg:"type:CHAR(26),notnull"`
+ Status *Status `pg:"rel:has-one"`
// ActivityPub URI of this fave
URI string `pg:",notnull"`
}
diff --git a/internal/gtsmodel/statusmute.go b/internal/gtsmodel/statusmute.go
index 4d7fa5ce2..472a5ec09 100644
--- a/internal/gtsmodel/statusmute.go
+++ b/internal/gtsmodel/statusmute.go
@@ -27,12 +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"`
- Account *Account `pg:"rel:belongs-to"`
+ 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"`
- TargetAccount *Account `pg:"rel:has-one"`
+ 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"`
- Status *Status `pg:"rel:has-one"`
+ StatusID string `pg:"type:CHAR(26),notnull"`
+ Status *Status `pg:"rel:has-one"`
}
diff --git a/internal/processing/account/getstatuses.go b/internal/processing/account/getstatuses.go
index 69a605d67..e1e3d0006 100644
--- a/internal/processing/account/getstatuses.go
+++ b/internal/processing/account/getstatuses.go
@@ -35,7 +35,7 @@ func (p *processor) StatusesGet(requestingAccount *gtsmodel.Account, targetAccou
}
apiStatuses := []apimodel.Status{}
-
+
statuses, err := p.db.GetAccountStatuses(targetAccountID, limit, excludeReplies, maxID, pinnedOnly, mediaOnly)
if err != nil {
if err == db.ErrNoEntries {
diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go
index 3a90cfa53..e477a6135 100644
--- a/internal/typeutils/converter.go
+++ b/internal/typeutils/converter.go
@@ -176,16 +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/internaltoas.go b/internal/typeutils/internaltoas.go
index 61ddef1c8..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,13 +339,21 @@ 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.Account == nil {
- a := >smodel.Account{}
- if err := c.db.GetByID(s.AccountID, a); err != 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.Account = a
@@ -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
}
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 ecb1d6857..d1c553a77 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 == "") {
@@ -38,8 +63,8 @@ func (f *filter) StatusHometimelineable(targetStatus *gtsmodel.Status, timelineO
if targetStatus.InReplyToID != "" {
// pin the reply to status on to this status if it hasn't been done already
if targetStatus.InReplyTo == nil {
- rs := >smodel.Status{}
- if err := f.db.GetByID(targetStatus.InReplyToID, rs); err != 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.InReplyTo = rs
@@ -47,8 +72,8 @@ func (f *filter) StatusHometimelineable(targetStatus *gtsmodel.Status, timelineO
// pin the reply to account on to this status if it hasn't been done already
if targetStatus.InReplyToAccount == nil {
- ra := >smodel.Account{}
- if err := f.db.GetByID(targetStatus.InReplyToAccountID, ra); err != 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.InReplyToAccount = ra
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 887f5c313..588114ed5 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,7 @@ 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
@@ -117,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, true); err != nil {
+ if relevantAccounts.InReplyToAccount != nil && relevantAccounts.InReplyToAccount.ID != requestingAccount.ID {
+ if blocked, err := f.db.Blocked(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")
@@ -127,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.Follows(requestingAccount, relevantAccounts.InReplyToAccount)
if err != nil {
return false, err
}
@@ -139,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, true); err != nil {
+ if relevantAccounts.BoostedAccount != nil {
+ if blocked, err := f.db.Blocked(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")
@@ -149,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, true); err != nil {
+ if relevantAccounts.BoostedInReplyToAccount != nil {
+ if blocked, err := f.db.Blocked(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")
diff --git a/internal/visibility/util.go b/internal/visibility/util.go
deleted file mode 100644
index 91b250b3d..000000000
--- a/internal/visibility/util.go
+++ /dev/null
@@ -1,210 +0,0 @@
-package visibility
-
-import (
- "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 it's not set on the status already
- if targetStatus.Account == nil {
- statusAuthor, err := f.db.GetAccountByID(targetStatus.AccountID)
- if err == nil {
- targetStatus.Account = statusAuthor
- }
- }
- accounts.StatusAuthor = targetStatus.Account
-
- // now get all accounts with IDs that are mentioned in the status
- if targetStatus.MentionIDs != nil && targetStatus.Mentions == nil {
- mentions, err := f.db.GetMentions(targetStatus.MentionIDs)
- if err == nil {
- targetStatus.Mentions = mentions
- }
- }
-
- for _, m := range targetStatus.Mentions {
- if m.TargetAccount == nil {
- t, err := f.db.GetAccountByID(m.TargetAccountID)
- if err == nil {
- m.TargetAccount = t
- }
- }
- accounts.MentionedAccounts = append(accounts.MentionedAccounts, m.TargetAccount)
- }
-
- // get the replied to account if it's not set on the status already
- if targetStatus.InReplyToAccountID != "" && targetStatus.InReplyToAccount == nil {
- repliedToAccount, err := f.db.GetAccountByID(targetStatus.InReplyToAccountID)
- if err == nil {
- targetStatus.InReplyToAccount = repliedToAccount
- }
- }
- accounts.ReplyToAccount = targetStatus.InReplyToAccount
-
- // get the boosted status if it's not set on the status already
- if targetStatus.BoostOfID != "" && targetStatus.BoostOf == nil {
- boostedStatus, err := f.db.GetStatusByID(targetStatus.BoostOfID)
- if err == nil {
- targetStatus.BoostOf = boostedStatus
- }
- }
-
- // get the boosted account if it's not set on the status already
- if targetStatus.BoostOfAccountID != "" && targetStatus.BoostOfAccount == nil {
- if targetStatus.BoostOf != nil && targetStatus.BoostOf.Account != nil {
- targetStatus.BoostOfAccount = targetStatus.BoostOf.Account
- } else {
- boostedAccount, err := f.db.GetAccountByID(targetStatus.BoostOfAccountID)
- if err == nil {
- targetStatus.BoostOfAccount = boostedAccount
- }
- }
- }
- accounts.BoostedStatusAuthor = targetStatus.BoostOfAccount
-
- if targetStatus.BoostOf != nil {
- // the boosted status might be a reply to another account so we should get that too
- if targetStatus.BoostOf.InReplyToAccountID != "" && targetStatus.BoostOf.InReplyToAccount == nil {
- boostOfInReplyToAccount, err := f.db.GetAccountByID(targetStatus.BoostOf.InReplyToAccountID)
- if err == nil {
- targetStatus.BoostOf.InReplyToAccount = boostOfInReplyToAccount
- }
- }
- accounts.BoostedReplyToAccount = targetStatus.BoostOf.InReplyToAccount
-
- // now get all accounts with IDs that are mentioned in the boosted status
- if targetStatus.BoostOf.MentionIDs != nil && targetStatus.BoostOf.Mentions == nil {
- mentions, err := f.db.GetMentions(targetStatus.BoostOf.MentionIDs)
- if err == nil {
- targetStatus.BoostOf.Mentions = mentions
- }
- }
-
- for _, m := range targetStatus.BoostOf.Mentions {
- if m.TargetAccount == nil {
- t, err := f.db.GetAccountByID(m.TargetAccountID)
- if err == nil {
- m.TargetAccount = t
- }
- }
- accounts.BoostedMentionedAccounts = append(accounts.BoostedMentionedAccounts, m.TargetAccount)
- }
- }
-
- 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 err == db.ErrNoEntries {
- // 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 {
- if a == nil {
- continue
- }
- 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 {
- if a == nil {
- continue
- }
- b, err := f.blockedDomain(a.Domain)
- if err != nil {
- return false, err
- }
- if b {
- return true, nil
- }
- }
-
- return false, nil
-}
diff --git a/testrig/testmodels.go b/testrig/testmodels.go
index 5e1906629..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: !",
- AttachmentIDs: []string{"01F8MH6NEM8D7527KZAECTCR76"},
- TagIDs: []string{"01F8MHA1A2NF9MJ3WCCQ3K8BSZ"},
- MentionIDs: []string{},
- EmojiIDs: []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",
- AttachmentIDs: []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!",
- AttachmentIDs: []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,17 +1120,29 @@ 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",
+ 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",
+ },
}
}