diff options
Diffstat (limited to 'internal')
149 files changed, 4245 insertions, 2592 deletions
diff --git a/internal/ap/extract.go b/internal/ap/extract.go index baffd4bf2..1ee0e008e 100644 --- a/internal/ap/extract.go +++ b/internal/ap/extract.go @@ -581,7 +581,7 @@ func ExtractMention(i Mentionable) (*gtsmodel.Mention, error) {  	if hrefProp == nil || !hrefProp.IsIRI() {  		return nil, errors.New("no href prop")  	} -	mention.MentionedAccountURI = hrefProp.GetIRI().String() +	mention.TargetAccountURI = hrefProp.GetIRI().String()  	return mention, nil  } diff --git a/internal/api/client/auth/callback.go b/internal/api/client/auth/callback.go index 8bf2a50b5..a26838aa3 100644 --- a/internal/api/client/auth/callback.go +++ b/internal/api/client/auth/callback.go @@ -116,7 +116,7 @@ func (m *Module) parseUserFromClaims(claims *oidc.Claims, ip net.IP, appID strin  		return user, nil  	} -	if _, ok := err.(db.ErrNoEntries); !ok { +	if err != db.ErrNoEntries {  		// we have an actual error in the database  		return nil, fmt.Errorf("error checking database for email %s: %s", claims.Email, err)  	} @@ -128,7 +128,7 @@ func (m *Module) parseUserFromClaims(claims *oidc.Claims, ip net.IP, appID strin  		return nil, fmt.Errorf("user with email address %s is unconfirmed", claims.Email)  	} -	if _, ok := err.(db.ErrNoEntries); !ok { +	if err != db.ErrNoEntries {  		// we have an actual error in the database  		return nil, fmt.Errorf("error checking database for email %s: %s", claims.Email, err)  	} diff --git a/internal/api/security/signaturecheck.go b/internal/api/security/signaturecheck.go index b852c92ab..88b0b4dff 100644 --- a/internal/api/security/signaturecheck.go +++ b/internal/api/security/signaturecheck.go @@ -6,8 +6,6 @@ import (  	"github.com/gin-gonic/gin"  	"github.com/go-fed/httpsig" -	"github.com/superseriousbusiness/gotosocial/internal/db" -	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/util"  ) @@ -33,13 +31,13 @@ func (m *Module) SignatureCheck(c *gin.Context) {  			// we managed to parse the url!  			// if the domain is blocked we want to bail as early as possible -			blockedDomain, err := m.blockedDomain(requestingPublicKeyID.Host) +			blocked, err := m.db.IsURIBlocked(requestingPublicKeyID)  			if err != nil {  				l.Errorf("could not tell if domain %s was blocked or not: %s", requestingPublicKeyID.Host, err)  				c.AbortWithStatus(http.StatusInternalServerError)  				return  			} -			if blockedDomain { +			if blocked {  				l.Infof("domain %s is blocked", requestingPublicKeyID.Host)  				c.AbortWithStatus(http.StatusForbidden)  				return @@ -50,20 +48,3 @@ func (m *Module) SignatureCheck(c *gin.Context) {  		}  	}  } - -func (m *Module) blockedDomain(host string) (bool, error) { -	b := >smodel.DomainBlock{} -	err := m.db.GetWhere([]db.Where{{Key: "domain", Value: host, CaseInsensitive: true}}, b) -	if err == nil { -		// block exists -		return true, nil -	} - -	if _, ok := err.(db.ErrNoEntries); ok { -		// there are no entries so there's no block -		return false, nil -	} - -	// there's an actual error -	return false, err -} diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 1d2d0533b..eb3744cfe 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -18,8 +18,28 @@  package cache +import ( +	"time" + +	"github.com/ReneKroon/ttlcache" +) +  // Cache defines an in-memory cache that is safe to be wiped when the application is restarted  type Cache interface {  	Store(k string, v interface{}) error  	Fetch(k string) (interface{}, error)  } + +type cache struct { +	c *ttlcache.Cache +} + +// New returns a new in-memory cache. +func New() Cache { +	c := ttlcache.NewCache() +	c.SetTTL(30 * time.Second) +	cache := &cache{ +		c: c, +	} +	return cache +} diff --git a/internal/cache/error.go b/internal/cache/error.go new file mode 100644 index 000000000..3f32aa7ce --- /dev/null +++ b/internal/cache/error.go @@ -0,0 +1,27 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package cache + +import "errors" + +// Error models an error returned by the in-memory cache. +type Error error + +// ErrNotFound means that a value for the requested key was not found in the cache. +var ErrNotFound = errors.New("value not found in cache") diff --git a/internal/db/pg/put.go b/internal/cache/fetch.go index 09beca14b..c107b9b26 100644 --- a/internal/db/pg/put.go +++ b/internal/cache/fetch.go @@ -16,18 +16,13 @@     along with this program.  If not, see <http://www.gnu.org/licenses/>.  */ -package pg +package cache -import ( -	"strings" - -	"github.com/superseriousbusiness/gotosocial/internal/db" -) - -func (ps *postgresService) Put(i interface{}) error { -	_, err := ps.conn.Model(i).Insert(i) -	if err != nil && strings.Contains(err.Error(), "duplicate key value violates unique constraint") { -		return db.ErrAlreadyExists{} +func (c *cache) Fetch(k string) (interface{}, error) { +	i, stored := c.c.Get(k) +	if !stored { +		return nil, ErrNotFound  	} -	return err + +	return i, nil  } diff --git a/internal/cache/store.go b/internal/cache/store.go new file mode 100644 index 000000000..6b4024476 --- /dev/null +++ b/internal/cache/store.go @@ -0,0 +1,24 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package cache + +func (c *cache) Store(k string, v interface{}) error { +	c.c.Set(k, v) +	return nil +} diff --git a/internal/cliactions/admin/account/account.go b/internal/cliactions/admin/account/account.go index 732527aa4..0ae7f32de 100644 --- a/internal/cliactions/admin/account/account.go +++ b/internal/cliactions/admin/account/account.go @@ -88,8 +88,8 @@ var Confirm cliactions.GTSAction = func(ctx context.Context, c *config.Config, l  		return err  	} -	a := >smodel.Account{} -	if err := dbConn.GetLocalAccountByUsername(username, a); err != nil { +	a, err := dbConn.GetLocalAccountByUsername(username) +	if err != nil {  		return err  	} @@ -123,8 +123,8 @@ var Promote cliactions.GTSAction = func(ctx context.Context, c *config.Config, l  		return err  	} -	a := >smodel.Account{} -	if err := dbConn.GetLocalAccountByUsername(username, a); err != nil { +	a, err := dbConn.GetLocalAccountByUsername(username) +	if err != nil {  		return err  	} @@ -155,8 +155,8 @@ var Demote cliactions.GTSAction = func(ctx context.Context, c *config.Config, lo  		return err  	} -	a := >smodel.Account{} -	if err := dbConn.GetLocalAccountByUsername(username, a); err != nil { +	a, err := dbConn.GetLocalAccountByUsername(username) +	if err != nil {  		return err  	} @@ -187,8 +187,8 @@ var Disable cliactions.GTSAction = func(ctx context.Context, c *config.Config, l  		return err  	} -	a := >smodel.Account{} -	if err := dbConn.GetLocalAccountByUsername(username, a); err != nil { +	a, err := dbConn.GetLocalAccountByUsername(username) +	if err != nil {  		return err  	} @@ -233,8 +233,8 @@ var Password cliactions.GTSAction = func(ctx context.Context, c *config.Config,  		return err  	} -	a := >smodel.Account{} -	if err := dbConn.GetLocalAccountByUsername(username, a); err != nil { +	a, err := dbConn.GetLocalAccountByUsername(username) +	if err != nil {  		return err  	} diff --git a/internal/cliactions/server/server.go b/internal/cliactions/server/server.go index 2314e2608..72c6cfadf 100644 --- a/internal/cliactions/server/server.go +++ b/internal/cliactions/server/server.go @@ -62,6 +62,8 @@ var models []interface{} = []interface{}{  	>smodel.MediaAttachment{},  	>smodel.Mention{},  	>smodel.Status{}, +	>smodel.StatusToEmoji{}, +	>smodel.StatusToTag{},  	>smodel.StatusFave{},  	>smodel.StatusBookmark{},  	>smodel.StatusMute{}, diff --git a/internal/db/account.go b/internal/db/account.go new file mode 100644 index 000000000..0e1575f9b --- /dev/null +++ b/internal/db/account.go @@ -0,0 +1,66 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package db + +import ( +	"time" + +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +// Account contains functions related to account getting/setting/creation. +type Account interface { +	// GetAccountByID returns one account with the given ID, or an error if something goes wrong. +	GetAccountByID(id string) (*gtsmodel.Account, Error) + +	// GetAccountByURI returns one account with the given URI, or an error if something goes wrong. +	GetAccountByURI(uri string) (*gtsmodel.Account, Error) + +	// GetAccountByURL returns one account with the given URL, or an error if something goes wrong. +	GetAccountByURL(uri string) (*gtsmodel.Account, Error) + +	// GetLocalAccountByUsername returns an account on this instance by its username. +	GetLocalAccountByUsername(username string) (*gtsmodel.Account, Error) + +	// GetAccountFaves fetches faves/likes created by the target accountID. +	GetAccountFaves(accountID string) ([]*gtsmodel.StatusFave, Error) + +	// GetAccountStatusesCount is a shortcut for the common action of counting statuses produced by accountID. +	CountAccountStatuses(accountID string) (int, Error) + +	// GetAccountStatuses is a shortcut for getting the most recent statuses. accountID is optional, if not provided +	// then all statuses will be returned. If limit is set to 0, the size of the returned slice will not be limited. This can +	// be very memory intensive so you probably shouldn't do this! +	// In case of no entries, a 'no entries' error will be returned +	GetAccountStatuses(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, Error) + +	GetAccountBlocks(accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, Error) + +	// GetAccountLastPosted simply gets the timestamp of the most recent post by the account. +	// +	// The returned time will be zero if account has never posted anything. +	GetAccountLastPosted(accountID string) (time.Time, Error) + +	// SetAccountHeaderOrAvatar sets the header or avatar for the given accountID to the given media attachment. +	SetAccountHeaderOrAvatar(mediaAttachment *gtsmodel.MediaAttachment, accountID string) Error + +	// GetInstanceAccount returns the instance account for the given domain. +	// If domain is empty, this instance account will be returned. +	GetInstanceAccount(domain string) (*gtsmodel.Account, Error) +} diff --git a/internal/db/admin.go b/internal/db/admin.go new file mode 100644 index 000000000..aa2b22f47 --- /dev/null +++ b/internal/db/admin.go @@ -0,0 +1,53 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package db + +import ( +	"net" + +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +// Admin contains functions related to instance administration (new signups etc). +type Admin interface { +	// IsUsernameAvailable checks whether a given username is available on our domain. +	// Returns an error if the username is already taken, or something went wrong in the db. +	IsUsernameAvailable(username string) Error + +	// IsEmailAvailable checks whether a given email address for a new account is available to be used on our domain. +	// Return an error if: +	// A) the email is already associated with an account +	// B) we block signups from this email domain +	// C) something went wrong in the db +	IsEmailAvailable(email string) Error + +	// NewSignup creates a new user in the database with the given parameters. +	// By the time this function is called, it should be assumed that all the parameters have passed validation! +	NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string, emailVerified bool, admin bool) (*gtsmodel.User, Error) + +	// CreateInstanceAccount creates an account in the database with the same username as the instance host value. +	// Ie., if the instance is hosted at 'example.org' the instance user will have a username of 'example.org'. +	// This is needed for things like serving files that belong to the instance and not an individual user/account. +	CreateInstanceAccount() Error + +	// CreateInstanceInstance creates an instance in the database with the same domain as the instance host value. +	// Ie., if the instance is hosted at 'example.org' the instance will have a domain of 'example.org'. +	// This is needed for things like serving instance information through /api/v1/instance +	CreateInstanceInstance() Error +} diff --git a/internal/db/basic.go b/internal/db/basic.go new file mode 100644 index 000000000..729920bba --- /dev/null +++ b/internal/db/basic.go @@ -0,0 +1,87 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package db + +import "context" + +// Basic wraps basic database functionality. +type Basic interface { +	// CreateTable creates a table for the given interface. +	// For implementations that don't use tables, this can just return nil. +	CreateTable(i interface{}) Error + +	// DropTable drops the table for the given interface. +	// For implementations that don't use tables, this can just return nil. +	DropTable(i interface{}) Error + +	// RegisterTable registers a table for use in many2many relations. +	// For implementations that don't use tables, or many2many relations, this can just return nil. +	RegisterTable(i interface{}) Error + +	// Stop should stop and close the database connection cleanly, returning an error if this is not possible. +	// If the database implementation doesn't need to be stopped, this can just return nil. +	Stop(ctx context.Context) Error + +	// IsHealthy should return nil if the database connection is healthy, or an error if not. +	IsHealthy(ctx context.Context) Error + +	// GetByID gets one entry by its id. In a database like postgres, this might be the 'id' field of the entry, +	// for other implementations (for example, in-memory) it might just be the key of a map. +	// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. +	// In case of no entries, a 'no entries' error will be returned +	GetByID(id string, i interface{}) Error + +	// GetWhere gets one entry where key = value. This is similar to GetByID but allows the caller to specify the +	// name of the key to select from. +	// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. +	// In case of no entries, a 'no entries' error will be returned +	GetWhere(where []Where, i interface{}) Error + +	// GetAll will try to get all entries of type i. +	// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. +	// In case of no entries, a 'no entries' error will be returned +	GetAll(i interface{}) Error + +	// Put simply stores i. It is up to the implementation to figure out how to store it, and using what key. +	// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. +	Put(i interface{}) Error + +	// Upsert stores or updates i based on the given conflict column, as in https://www.postgresqltutorial.com/postgresql-upsert/ +	// It is up to the implementation to figure out how to store it, and using what key. +	// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. +	Upsert(i interface{}, conflictColumn string) Error + +	// UpdateByID updates i with id id. +	// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. +	UpdateByID(id string, i interface{}) Error + +	// UpdateOneByID updates interface i with database the given database id. It will update one field of key key and value value. +	UpdateOneByID(id string, key string, value interface{}, i interface{}) Error + +	// UpdateWhere updates column key of interface i with the given value, where the given parameters apply. +	UpdateWhere(where []Where, key string, value interface{}, i interface{}) Error + +	// DeleteByID removes i with id id. +	// If i didn't exist anyway, then no error should be returned. +	DeleteByID(id string, i interface{}) Error + +	// DeleteWhere deletes i where key = value +	// If i didn't exist anyway, then no error should be returned. +	DeleteWhere(where []Where, i interface{}) Error +} diff --git a/internal/db/db.go b/internal/db/db.go index d0b23fbc6..d6ac883e4 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -19,9 +19,6 @@  package db  import ( -	"context" -	"net" -  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  ) @@ -30,257 +27,19 @@ const (  	DBTypePostgres string = "POSTGRES"  ) -// DB provides methods for interacting with an underlying database or other storage mechanism (for now, just postgres). -// Note that in all of the functions below, the passed interface should be a pointer or a slice, which will then be populated -// by whatever is returned from the database. +// DB provides methods for interacting with an underlying database or other storage mechanism.  type DB interface { -	/* -		BASIC DB FUNCTIONALITY -	*/ - -	// CreateTable creates a table for the given interface. -	// For implementations that don't use tables, this can just return nil. -	CreateTable(i interface{}) error - -	// DropTable drops the table for the given interface. -	// For implementations that don't use tables, this can just return nil. -	DropTable(i interface{}) error - -	// Stop should stop and close the database connection cleanly, returning an error if this is not possible. -	// If the database implementation doesn't need to be stopped, this can just return nil. -	Stop(ctx context.Context) error - -	// IsHealthy should return nil if the database connection is healthy, or an error if not. -	IsHealthy(ctx context.Context) error - -	// GetByID gets one entry by its id. In a database like postgres, this might be the 'id' field of the entry, -	// for other implementations (for example, in-memory) it might just be the key of a map. -	// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. -	// In case of no entries, a 'no entries' error will be returned -	GetByID(id string, i interface{}) error - -	// GetWhere gets one entry where key = value. This is similar to GetByID but allows the caller to specify the -	// name of the key to select from. -	// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. -	// In case of no entries, a 'no entries' error will be returned -	GetWhere(where []Where, i interface{}) error - -	// GetAll will try to get all entries of type i. -	// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. -	// In case of no entries, a 'no entries' error will be returned -	GetAll(i interface{}) error - -	// Put simply stores i. It is up to the implementation to figure out how to store it, and using what key. -	// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. -	Put(i interface{}) error - -	// Upsert stores or updates i based on the given conflict column, as in https://www.postgresqltutorial.com/postgresql-upsert/ -	// It is up to the implementation to figure out how to store it, and using what key. -	// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. -	Upsert(i interface{}, conflictColumn string) error - -	// UpdateByID updates i with id id. -	// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. -	UpdateByID(id string, i interface{}) error - -	// UpdateOneByID updates interface i with database the given database id. It will update one field of key key and value value. -	UpdateOneByID(id string, key string, value interface{}, i interface{}) error - -	// UpdateWhere updates column key of interface i with the given value, where the given parameters apply. -	UpdateWhere(where []Where, key string, value interface{}, i interface{}) error - -	// DeleteByID removes i with id id. -	// If i didn't exist anyway, then no error should be returned. -	DeleteByID(id string, i interface{}) error - -	// DeleteWhere deletes i where key = value -	// If i didn't exist anyway, then no error should be returned. -	DeleteWhere(where []Where, i interface{}) error - -	/* -		HANDY SHORTCUTS -	*/ - -	// AcceptFollowRequest moves a follow request in the database from the follow_requests table to the follows table. -	// In other words, it should create the follow, and delete the existing follow request. -	// -	// It will return the newly created follow for further processing. -	AcceptFollowRequest(originAccountID string, targetAccountID string) (*gtsmodel.Follow, error) - -	// CreateInstanceAccount creates an account in the database with the same username as the instance host value. -	// Ie., if the instance is hosted at 'example.org' the instance user will have a username of 'example.org'. -	// This is needed for things like serving files that belong to the instance and not an individual user/account. -	CreateInstanceAccount() error - -	// CreateInstanceInstance creates an instance in the database with the same domain as the instance host value. -	// Ie., if the instance is hosted at 'example.org' the instance will have a domain of 'example.org'. -	// This is needed for things like serving instance information through /api/v1/instance -	CreateInstanceInstance() error - -	// GetAccountByUserID is a shortcut for the common action of fetching an account corresponding to a user ID. -	// The given account pointer will be set to the result of the query, whatever it is. -	// In case of no entries, a 'no entries' error will be returned -	GetAccountByUserID(userID string, account *gtsmodel.Account) error - -	// GetLocalAccountByUsername is a shortcut for the common action of fetching an account ON THIS INSTANCE -	// according to its username, which should be unique. -	// The given account pointer will be set to the result of the query, whatever it is. -	// In case of no entries, a 'no entries' error will be returned -	GetLocalAccountByUsername(username string, account *gtsmodel.Account) error - -	// GetFollowRequestsForAccountID is a shortcut for the common action of fetching a list of follow requests targeting the given account ID. -	// The given slice 'followRequests' will be set to the result of the query, whatever it is. -	// In case of no entries, a 'no entries' error will be returned -	GetFollowRequestsForAccountID(accountID string, followRequests *[]gtsmodel.FollowRequest) error - -	// GetFollowingByAccountID is a shortcut for the common action of fetching a list of accounts that accountID is following. -	// The given slice 'following' will be set to the result of the query, whatever it is. -	// In case of no entries, a 'no entries' error will be returned -	GetFollowingByAccountID(accountID string, following *[]gtsmodel.Follow) error - -	// GetFollowersByAccountID is a shortcut for the common action of fetching a list of accounts that accountID is followed by. -	// The given slice 'followers' will be set to the result of the query, whatever it is. -	// In case of no entries, a 'no entries' error will be returned -	// -	// If localOnly is set to true, then only followers from *this instance* will be returned. -	GetFollowersByAccountID(accountID string, followers *[]gtsmodel.Follow, localOnly bool) error - -	// GetFavesByAccountID is a shortcut for the common action of fetching a list of faves made by the given accountID. -	// The given slice 'faves' will be set to the result of the query, whatever it is. -	// In case of no entries, a 'no entries' error will be returned -	GetFavesByAccountID(accountID string, faves *[]gtsmodel.StatusFave) error - -	// CountStatusesByAccountID is a shortcut for the common action of counting statuses produced by accountID. -	CountStatusesByAccountID(accountID string) (int, error) - -	// GetStatusesForAccount is a shortcut for getting the most recent statuses. accountID is optional, if not provided -	// then all statuses will be returned. If limit is set to 0, the size of the returned slice will not be limited. This can -	// be very memory intensive so you probably shouldn't do this! -	// In case of no entries, a 'no entries' error will be returned -	GetStatusesForAccount(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, error) - -	GetBlocksForAccount(accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, error) - -	// GetLastStatusForAccountID simply gets the most recent status by the given account. -	// The given slice 'status' pointer will be set to the result of the query, whatever it is. -	// In case of no entries, a 'no entries' error will be returned -	GetLastStatusForAccountID(accountID string, status *gtsmodel.Status) error - -	// IsUsernameAvailable checks whether a given username is available on our domain. -	// Returns an error if the username is already taken, or something went wrong in the db. -	IsUsernameAvailable(username string) error - -	// IsEmailAvailable checks whether a given email address for a new account is available to be used on our domain. -	// Return an error if: -	// A) the email is already associated with an account -	// B) we block signups from this email domain -	// C) something went wrong in the db -	IsEmailAvailable(email string) error - -	// NewSignup creates a new user in the database with the given parameters. -	// By the time this function is called, it should be assumed that all the parameters have passed validation! -	NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string, emailVerified bool, admin bool) (*gtsmodel.User, error) - -	// SetHeaderOrAvatarForAccountID sets the header or avatar for the given accountID to the given media attachment. -	SetHeaderOrAvatarForAccountID(mediaAttachment *gtsmodel.MediaAttachment, accountID string) error - -	// GetHeaderAvatarForAccountID gets the current avatar for the given account ID. -	// The passed mediaAttachment pointer will be populated with the value of the avatar, if it exists. -	GetAvatarForAccountID(avatar *gtsmodel.MediaAttachment, accountID string) error - -	// GetHeaderForAccountID gets the current header for the given account ID. -	// The passed mediaAttachment pointer will be populated with the value of the header, if it exists. -	GetHeaderForAccountID(header *gtsmodel.MediaAttachment, accountID string) error - -	// Blocked checks whether a block exists in eiher direction between two accounts. -	// That is, it returns true if account1 blocks account2, OR if account2 blocks account1. -	Blocked(account1 string, account2 string) (bool, error) - -	// GetRelationship retrieves the relationship of the targetAccount to the requestingAccount. -	GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, error) - -	// Follows returns true if sourceAccount follows target account, or an error if something goes wrong while finding out. -	Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) - -	// FollowRequested returns true if sourceAccount has requested to follow target account, or an error if something goes wrong while finding out. -	FollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) - -	// Mutuals returns true if account1 and account2 both follow each other, or an error if something goes wrong while finding out. -	Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error) - -	// GetReplyCountForStatus returns the amount of replies recorded for a status, or an error if something goes wrong -	GetReplyCountForStatus(status *gtsmodel.Status) (int, error) - -	// GetReblogCountForStatus returns the amount of reblogs/boosts recorded for a status, or an error if something goes wrong -	GetReblogCountForStatus(status *gtsmodel.Status) (int, error) - -	// GetFaveCountForStatus returns the amount of faves/likes recorded for a status, or an error if something goes wrong -	GetFaveCountForStatus(status *gtsmodel.Status) (int, error) - -	// StatusParents get the parent statuses of a given status. -	// -	// If onlyDirect is true, only the immediate parent will be returned. -	StatusParents(status *gtsmodel.Status, onlyDirect bool) ([]*gtsmodel.Status, error) - -	// StatusChildren gets the child statuses of a given status. -	// -	// If onlyDirect is true, only the immediate children will be returned. -	StatusChildren(status *gtsmodel.Status, onlyDirect bool, minID string) ([]*gtsmodel.Status, error) - -	// StatusFavedBy checks if a given status has been faved by a given account ID -	StatusFavedBy(status *gtsmodel.Status, accountID string) (bool, error) - -	// StatusRebloggedBy checks if a given status has been reblogged/boosted by a given account ID -	StatusRebloggedBy(status *gtsmodel.Status, accountID string) (bool, error) - -	// StatusMutedBy checks if a given status has been muted by a given account ID -	StatusMutedBy(status *gtsmodel.Status, accountID string) (bool, error) - -	// StatusBookmarkedBy checks if a given status has been bookmarked by a given account ID -	StatusBookmarkedBy(status *gtsmodel.Status, accountID string) (bool, error) - -	// WhoFavedStatus returns a slice of accounts who faved the given status. -	// This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user. -	WhoFavedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error) - -	// WhoBoostedStatus returns a slice of accounts who boosted the given status. -	// This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user. -	WhoBoostedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error) - -	// GetHomeTimelineForAccount returns a slice of statuses from accounts that are followed by the given account id. -	// -	// Statuses should be returned in descending order of when they were created (newest first). -	GetHomeTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error) - -	// GetPublicTimelineForAccount fetches the account's PUBLIC timeline -- ie., posts and replies that are public. -	// It will use the given filters and try to return as many statuses as possible up to the limit. -	// -	// Statuses should be returned in descending order of when they were created (newest first). -	GetPublicTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error) - -	// GetFavedTimelineForAccount fetches the account's FAVED timeline -- ie., posts and replies that the requesting account has faved. -	// It will use the given filters and try to return as many statuses as possible up to the limit. -	// -	// Note that unlike the other GetTimeline functions, the returned statuses will be arranged by their FAVE id, not the STATUS id. -	// In other words, they'll be returned in descending order of when they were faved by the requesting user, not when they were created. -	// -	// Also note the extra return values, which correspond to the nextMaxID and prevMinID for building Link headers. -	GetFavedTimelineForAccount(accountID string, maxID string, minID string, limit int) ([]*gtsmodel.Status, string, string, error) - -	// GetNotificationsForAccount returns a list of notifications that pertain to the given accountID. -	GetNotificationsForAccount(accountID string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, error) - -	// GetUserCountForInstance returns the number of known accounts registered with the given domain. -	GetUserCountForInstance(domain string) (int, error) - -	// GetStatusCountForInstance returns the number of known statuses posted from the given domain. -	GetStatusCountForInstance(domain string) (int, error) - -	// GetDomainCountForInstance returns the number of known instances known that the given domain federates with. -	GetDomainCountForInstance(domain string) (int, error) - -	// GetAccountsForInstance returns a slice of accounts from the given instance, arranged by ID. -	GetAccountsForInstance(domain string, maxID string, limit int) ([]*gtsmodel.Account, error) +	Account +	Admin +	Basic +	Domain +	Instance +	Media +	Mention +	Notification +	Relationship +	Status +	Timeline  	/*  		USEFUL CONVERSION FUNCTIONS diff --git a/internal/db/domain.go b/internal/db/domain.go new file mode 100644 index 000000000..a6583c80c --- /dev/null +++ b/internal/db/domain.go @@ -0,0 +1,36 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package db + +import "net/url" + +// Domain contains DB functions related to domains and domain blocks. +type Domain interface { +	// IsDomainBlocked checks if an instance-level domain block exists for the given domain string (eg., `example.org`). +	IsDomainBlocked(domain string) (bool, Error) + +	// AreDomainsBlocked checks if an instance-level domain block exists for any of the given domains strings, and returns true if even one is found. +	AreDomainsBlocked(domains []string) (bool, Error) + +	// IsURIBlocked checks if an instance-level domain block exists for the `host` in the given URI (eg., `https://example.org/users/whatever`). +	IsURIBlocked(uri *url.URL) (bool, Error) + +	// AreURIsBlocked checks if an instance-level domain block exists for any `host` in the given URI slice, and returns true if even one is found. +	AreURIsBlocked(uris []*url.URL) (bool, Error) +} diff --git a/internal/db/error.go b/internal/db/error.go index 197c7bd68..c13bd78dd 100644 --- a/internal/db/error.go +++ b/internal/db/error.go @@ -18,16 +18,18 @@  package db -// ErrNoEntries is to be returned from the DB interface when no entries are found for a given query. -type ErrNoEntries struct{} - -func (e ErrNoEntries) Error() string { -	return "no entries" -} - -// ErrAlreadyExists is to be returned from the DB interface when an entry already exists for a given query or its constraints. -type ErrAlreadyExists struct{} - -func (e ErrAlreadyExists) Error() string { -	return "already exists" -} +import "fmt" + +// Error denotes a database error. +type Error error + +var ( +	// ErrNoEntries is returned when a caller expected an entry for a query, but none was found. +	ErrNoEntries Error = fmt.Errorf("no entries") +	// ErrMultipleEntries is returned when a caller expected ONE entry for a query, but multiples were found. +	ErrMultipleEntries Error = fmt.Errorf("multiple entries") +	// ErrAlreadyExists is returned when a caller tries to insert a database entry that already exists in the db. +	ErrAlreadyExists Error = fmt.Errorf("already exists") +	// ErrUnknown denotes an unknown database error. +	ErrUnknown Error = fmt.Errorf("unknown error") +) diff --git a/internal/db/instance.go b/internal/db/instance.go new file mode 100644 index 000000000..1f7c83e4f --- /dev/null +++ b/internal/db/instance.go @@ -0,0 +1,36 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package db + +import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + +// Instance contains functions for instance-level actions (counting instance users etc.). +type Instance interface { +	// CountInstanceUsers returns the number of known accounts registered with the given domain. +	CountInstanceUsers(domain string) (int, Error) + +	// CountInstanceStatuses returns the number of known statuses posted from the given domain. +	CountInstanceStatuses(domain string) (int, Error) + +	// CountInstanceDomains returns the number of known instances known that the given domain federates with. +	CountInstanceDomains(domain string) (int, Error) + +	// GetInstanceAccounts returns a slice of accounts from the given instance, arranged by ID. +	GetInstanceAccounts(domain string, maxID string, limit int) ([]*gtsmodel.Account, Error) +} diff --git a/internal/db/media.go b/internal/db/media.go new file mode 100644 index 000000000..db4db3411 --- /dev/null +++ b/internal/db/media.go @@ -0,0 +1,27 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package db + +import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + +// Media contains functions related to creating/getting/removing media attachments. +type Media interface { +	// GetAttachmentByID gets a single attachment by its ID +	GetAttachmentByID(id string) (*gtsmodel.MediaAttachment, Error) +} diff --git a/internal/db/mention.go b/internal/db/mention.go new file mode 100644 index 000000000..cb1c56dc1 --- /dev/null +++ b/internal/db/mention.go @@ -0,0 +1,30 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package db + +import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + +// Mention contains functions for getting/creating mentions in the database. +type Mention interface { +	// GetMention gets a single mention by ID +	GetMention(id string) (*gtsmodel.Mention, Error) + +	// GetMentions gets multiple mentions. +	GetMentions(ids []string) ([]*gtsmodel.Mention, Error) +} diff --git a/internal/db/notification.go b/internal/db/notification.go new file mode 100644 index 000000000..326f0f149 --- /dev/null +++ b/internal/db/notification.go @@ -0,0 +1,31 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package db + +import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + +// Notification contains functions for creating and getting notifications. +type Notification interface { +	// GetNotifications returns a slice of notifications that pertain to the given accountID. +	// +	// Returned notifications will be ordered ID descending (ie., highest/newest to lowest/oldest). +	GetNotifications(accountID string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, Error) +	// GetNotification returns one notification according to its id. +	GetNotification(id string) (*gtsmodel.Notification, Error) +} diff --git a/internal/db/pg/account.go b/internal/db/pg/account.go new file mode 100644 index 000000000..3889c6601 --- /dev/null +++ b/internal/db/pg/account.go @@ -0,0 +1,256 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package pg + +import ( +	"context" +	"errors" +	"fmt" +	"time" + +	"github.com/go-pg/pg/v10" +	"github.com/go-pg/pg/v10/orm" +	"github.com/sirupsen/logrus" +	"github.com/superseriousbusiness/gotosocial/internal/config" +	"github.com/superseriousbusiness/gotosocial/internal/db" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +type accountDB struct { +	config *config.Config +	conn   *pg.DB +	log    *logrus.Logger +	cancel context.CancelFunc +} + +func (a *accountDB) newAccountQ(account *gtsmodel.Account) *orm.Query { +	return a.conn.Model(account). +		Relation("AvatarMediaAttachment"). +		Relation("HeaderMediaAttachment") +} + +func (a *accountDB) GetAccountByID(id string) (*gtsmodel.Account, db.Error) { +	account := >smodel.Account{} + +	q := a.newAccountQ(account). +		Where("account.id = ?", id) + +	err := processErrorResponse(q.Select()) + +	return account, err +} + +func (a *accountDB) GetAccountByURI(uri string) (*gtsmodel.Account, db.Error) { +	account := >smodel.Account{} + +	q := a.newAccountQ(account). +		Where("account.uri = ?", uri) + +	err := processErrorResponse(q.Select()) + +	return account, err +} + +func (a *accountDB) GetAccountByURL(uri string) (*gtsmodel.Account, db.Error) { +	account := >smodel.Account{} + +	q := a.newAccountQ(account). +		Where("account.url = ?", uri) + +	err := processErrorResponse(q.Select()) + +	return account, err +} + +func (a *accountDB) GetInstanceAccount(domain string) (*gtsmodel.Account, db.Error) { +	account := >smodel.Account{} + +	q := a.newAccountQ(account) + +	if domain == "" { +		q = q. +			Where("account.username = ?", domain). +			Where("account.domain = ?", domain) +	} else { +		q = q. +			Where("account.username = ?", domain). +			Where("? IS NULL", pg.Ident("domain")) +	} + +	err := processErrorResponse(q.Select()) + +	return account, err +} + +func (a *accountDB) GetAccountLastPosted(accountID string) (time.Time, db.Error) { +	status := >smodel.Status{} + +	q := a.conn.Model(status). +		Order("id DESC"). +		Limit(1). +		Where("account_id = ?", accountID). +		Column("created_at") + +	err := processErrorResponse(q.Select()) + +	return status.CreatedAt, err +} + +func (a *accountDB) SetAccountHeaderOrAvatar(mediaAttachment *gtsmodel.MediaAttachment, accountID string) db.Error { +	if mediaAttachment.Avatar && mediaAttachment.Header { +		return errors.New("one media attachment cannot be both header and avatar") +	} + +	var headerOrAVI string +	if mediaAttachment.Avatar { +		headerOrAVI = "avatar" +	} else if mediaAttachment.Header { +		headerOrAVI = "header" +	} else { +		return errors.New("given media attachment was neither a header nor an avatar") +	} + +	// TODO: there are probably more side effects here that need to be handled +	if _, err := a.conn.Model(mediaAttachment).OnConflict("(id) DO UPDATE").Insert(); err != nil { +		return err +	} + +	if _, err := a.conn.Model(>smodel.Account{}).Set(fmt.Sprintf("%s_media_attachment_id = ?", headerOrAVI), mediaAttachment.ID).Where("id = ?", accountID).Update(); err != nil { +		return err +	} +	return nil +} + +func (a *accountDB) GetLocalAccountByUsername(username string) (*gtsmodel.Account, db.Error) { +	account := >smodel.Account{} + +	q := a.newAccountQ(account). +		Where("username = ?", username). +		Where("? IS NULL", pg.Ident("domain")) + +	err := processErrorResponse(q.Select()) + +	return account, err +} + +func (a *accountDB) GetAccountFaves(accountID string) ([]*gtsmodel.StatusFave, db.Error) { +	faves := []*gtsmodel.StatusFave{} + +	if err := a.conn.Model(&faves). +		Where("account_id = ?", accountID). +		Select(); err != nil { +		if err == pg.ErrNoRows { +			return faves, nil +		} +		return nil, err +	} +	return faves, nil +} + +func (a *accountDB) CountAccountStatuses(accountID string) (int, db.Error) { +	return a.conn.Model(>smodel.Status{}).Where("account_id = ?", accountID).Count() +} + +func (a *accountDB) GetAccountStatuses(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, db.Error) { +	a.log.Debugf("getting statuses for account %s", accountID) +	statuses := []*gtsmodel.Status{} + +	q := a.conn.Model(&statuses).Order("id DESC") +	if accountID != "" { +		q = q.Where("account_id = ?", accountID) +	} + +	if limit != 0 { +		q = q.Limit(limit) +	} + +	if excludeReplies { +		q = q.Where("? IS NULL", pg.Ident("in_reply_to_id")) +	} + +	if pinnedOnly { +		q = q.Where("pinned = ?", true) +	} + +	if mediaOnly { +		q = q.WhereGroup(func(q *pg.Query) (*pg.Query, error) { +			return q.Where("? IS NOT NULL", pg.Ident("attachments")).Where("attachments != '{}'"), nil +		}) +	} + +	if maxID != "" { +		q = q.Where("id < ?", maxID) +	} + +	if err := q.Select(); err != nil { +		if err == pg.ErrNoRows { +			return nil, db.ErrNoEntries +		} +		return nil, err +	} + +	if len(statuses) == 0 { +		return nil, db.ErrNoEntries +	} + +	a.log.Debugf("returning statuses for account %s", accountID) +	return statuses, nil +} + +func (a *accountDB) GetAccountBlocks(accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, db.Error) { +	blocks := []*gtsmodel.Block{} + +	fq := a.conn.Model(&blocks). +		Where("block.account_id = ?", accountID). +		Relation("TargetAccount"). +		Order("block.id DESC") + +	if maxID != "" { +		fq = fq.Where("block.id < ?", maxID) +	} + +	if sinceID != "" { +		fq = fq.Where("block.id > ?", sinceID) +	} + +	if limit > 0 { +		fq = fq.Limit(limit) +	} + +	err := fq.Select() +	if err != nil { +		if err == pg.ErrNoRows { +			return nil, "", "", db.ErrNoEntries +		} +		return nil, "", "", err +	} + +	if len(blocks) == 0 { +		return nil, "", "", db.ErrNoEntries +	} + +	accounts := []*gtsmodel.Account{} +	for _, b := range blocks { +		accounts = append(accounts, b.TargetAccount) +	} + +	nextMaxID := blocks[len(blocks)-1].ID +	prevMinID := blocks[0].ID +	return accounts, nextMaxID, prevMinID, nil +} diff --git a/internal/db/pg/account_test.go b/internal/db/pg/account_test.go new file mode 100644 index 000000000..7ea5ff39a --- /dev/null +++ b/internal/db/pg/account_test.go @@ -0,0 +1,70 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package pg_test + +import ( +	"testing" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/testrig" +) + +type AccountTestSuite struct { +	PGStandardTestSuite +} + +func (suite *AccountTestSuite) SetupSuite() { +	suite.testTokens = testrig.NewTestTokens() +	suite.testClients = testrig.NewTestClients() +	suite.testApplications = testrig.NewTestApplications() +	suite.testUsers = testrig.NewTestUsers() +	suite.testAccounts = testrig.NewTestAccounts() +	suite.testAttachments = testrig.NewTestAttachments() +	suite.testStatuses = testrig.NewTestStatuses() +	suite.testTags = testrig.NewTestTags() +	suite.testMentions = testrig.NewTestMentions() +} + +func (suite *AccountTestSuite) SetupTest() { +	suite.config = testrig.NewTestConfig() +	suite.db = testrig.NewTestDB() +	suite.log = testrig.NewTestLog() + +	testrig.StandardDBSetup(suite.db, suite.testAccounts) +} + +func (suite *AccountTestSuite) TearDownTest() { +	testrig.StandardDBTeardown(suite.db) +} + +func (suite *AccountTestSuite) TestGetAccountByIDWithExtras() { +	account, err := suite.db.GetAccountByID(suite.testAccounts["local_account_1"].ID) +	if err != nil { +		suite.FailNow(err.Error()) +	} +	suite.NotNil(account) +	suite.NotNil(account.AvatarMediaAttachment) +	suite.NotEmpty(account.AvatarMediaAttachment.URL) +	suite.NotNil(account.HeaderMediaAttachment) +	suite.NotEmpty(account.HeaderMediaAttachment.URL) +} + +func TestAccountTestSuite(t *testing.T) { +	suite.Run(t, new(AccountTestSuite)) +} diff --git a/internal/db/pg/admin.go b/internal/db/pg/admin.go new file mode 100644 index 000000000..854f56ef0 --- /dev/null +++ b/internal/db/pg/admin.go @@ -0,0 +1,235 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package pg + +import ( +	"context" +	"crypto/rand" +	"crypto/rsa" +	"fmt" +	"net" +	"net/mail" +	"strings" +	"time" + +	"github.com/go-pg/pg/v10" +	"github.com/sirupsen/logrus" +	"github.com/superseriousbusiness/gotosocial/internal/config" +	"github.com/superseriousbusiness/gotosocial/internal/db" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/id" +	"github.com/superseriousbusiness/gotosocial/internal/util" +	"golang.org/x/crypto/bcrypt" +) + +type adminDB struct { +	config *config.Config +	conn   *pg.DB +	log    *logrus.Logger +	cancel context.CancelFunc +} + +func (a *adminDB) IsUsernameAvailable(username string) db.Error { +	// if no error we fail because it means we found something +	// if error but it's not pg.ErrNoRows then we fail +	// if err is pg.ErrNoRows we're good, we found nothing so continue +	if err := a.conn.Model(>smodel.Account{}).Where("username = ?", username).Where("domain = ?", nil).Select(); err == nil { +		return fmt.Errorf("username %s already in use", username) +	} else if err != pg.ErrNoRows { +		return fmt.Errorf("db error: %s", err) +	} +	return nil +} + +func (a *adminDB) IsEmailAvailable(email string) db.Error { +	// parse the domain from the email +	m, err := mail.ParseAddress(email) +	if err != nil { +		return fmt.Errorf("error parsing email address %s: %s", email, err) +	} +	domain := strings.Split(m.Address, "@")[1] // domain will always be the second part after @ + +	// check if the email domain is blocked +	if err := a.conn.Model(>smodel.EmailDomainBlock{}).Where("domain = ?", domain).Select(); err == nil { +		// fail because we found something +		return fmt.Errorf("email domain %s is blocked", domain) +	} else if err != pg.ErrNoRows { +		// fail because we got an unexpected error +		return fmt.Errorf("db error: %s", err) +	} + +	// check if this email is associated with a user already +	if err := a.conn.Model(>smodel.User{}).Where("email = ?", email).WhereOr("unconfirmed_email = ?", email).Select(); err == nil { +		// fail because we found something +		return fmt.Errorf("email %s already in use", email) +	} else if err != pg.ErrNoRows { +		// fail because we got an unexpected error +		return fmt.Errorf("db error: %s", err) +	} +	return nil +} + +func (a *adminDB) NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string, emailVerified bool, admin bool) (*gtsmodel.User, db.Error) { +	key, err := rsa.GenerateKey(rand.Reader, 2048) +	if err != nil { +		a.log.Errorf("error creating new rsa key: %s", err) +		return nil, err +	} + +	// if something went wrong while creating a user, we might already have an account, so check here first... +	acct := >smodel.Account{} +	err = a.conn.Model(acct).Where("username = ?", username).Where("? IS NULL", pg.Ident("domain")).Select() +	if err != nil { +		// there's been an actual error +		if err != pg.ErrNoRows { +			return nil, fmt.Errorf("db error checking existence of account: %s", err) +		} + +		// we just don't have an account yet create one +		newAccountURIs := util.GenerateURIsForAccount(username, a.config.Protocol, a.config.Host) +		newAccountID, err := id.NewRandomULID() +		if err != nil { +			return nil, err +		} + +		acct = >smodel.Account{ +			ID:                    newAccountID, +			Username:              username, +			DisplayName:           username, +			Reason:                reason, +			URL:                   newAccountURIs.UserURL, +			PrivateKey:            key, +			PublicKey:             &key.PublicKey, +			PublicKeyURI:          newAccountURIs.PublicKeyURI, +			ActorType:             gtsmodel.ActivityStreamsPerson, +			URI:                   newAccountURIs.UserURI, +			InboxURI:              newAccountURIs.InboxURI, +			OutboxURI:             newAccountURIs.OutboxURI, +			FollowersURI:          newAccountURIs.FollowersURI, +			FollowingURI:          newAccountURIs.FollowingURI, +			FeaturedCollectionURI: newAccountURIs.CollectionURI, +		} +		if _, err = a.conn.Model(acct).Insert(); err != nil { +			return nil, err +		} +	} + +	pw, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) +	if err != nil { +		return nil, fmt.Errorf("error hashing password: %s", err) +	} + +	newUserID, err := id.NewRandomULID() +	if err != nil { +		return nil, err +	} + +	u := >smodel.User{ +		ID:                     newUserID, +		AccountID:              acct.ID, +		EncryptedPassword:      string(pw), +		SignUpIP:               signUpIP.To4(), +		Locale:                 locale, +		UnconfirmedEmail:       email, +		CreatedByApplicationID: appID, +		Approved:               !requireApproval, // if we don't require moderator approval, just pre-approve the user +	} + +	if emailVerified { +		u.ConfirmedAt = time.Now() +		u.Email = email +	} + +	if admin { +		u.Admin = true +		u.Moderator = true +	} + +	if _, err = a.conn.Model(u).Insert(); err != nil { +		return nil, err +	} + +	return u, nil +} + +func (a *adminDB) CreateInstanceAccount() db.Error { +	username := a.config.Host +	key, err := rsa.GenerateKey(rand.Reader, 2048) +	if err != nil { +		a.log.Errorf("error creating new rsa key: %s", err) +		return err +	} + +	aID, err := id.NewRandomULID() +	if err != nil { +		return err +	} + +	newAccountURIs := util.GenerateURIsForAccount(username, a.config.Protocol, a.config.Host) +	acct := >smodel.Account{ +		ID:                    aID, +		Username:              a.config.Host, +		DisplayName:           username, +		URL:                   newAccountURIs.UserURL, +		PrivateKey:            key, +		PublicKey:             &key.PublicKey, +		PublicKeyURI:          newAccountURIs.PublicKeyURI, +		ActorType:             gtsmodel.ActivityStreamsPerson, +		URI:                   newAccountURIs.UserURI, +		InboxURI:              newAccountURIs.InboxURI, +		OutboxURI:             newAccountURIs.OutboxURI, +		FollowersURI:          newAccountURIs.FollowersURI, +		FollowingURI:          newAccountURIs.FollowingURI, +		FeaturedCollectionURI: newAccountURIs.CollectionURI, +	} +	inserted, err := a.conn.Model(acct).Where("username = ?", username).SelectOrInsert() +	if err != nil { +		return err +	} +	if inserted { +		a.log.Infof("created instance account %s with id %s", username, acct.ID) +	} else { +		a.log.Infof("instance account %s already exists with id %s", username, acct.ID) +	} +	return nil +} + +func (a *adminDB) CreateInstanceInstance() db.Error { +	iID, err := id.NewRandomULID() +	if err != nil { +		return err +	} + +	i := >smodel.Instance{ +		ID:     iID, +		Domain: a.config.Host, +		Title:  a.config.Host, +		URI:    fmt.Sprintf("%s://%s", a.config.Protocol, a.config.Host), +	} +	inserted, err := a.conn.Model(i).Where("domain = ?", a.config.Host).SelectOrInsert() +	if err != nil { +		return err +	} +	if inserted { +		a.log.Infof("created instance instance %s with id %s", a.config.Host, i.ID) +	} else { +		a.log.Infof("instance instance %s already exists with id %s", a.config.Host, i.ID) +	} +	return nil +} diff --git a/internal/db/pg/basic.go b/internal/db/pg/basic.go new file mode 100644 index 000000000..6e76b4450 --- /dev/null +++ b/internal/db/pg/basic.go @@ -0,0 +1,205 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package pg + +import ( +	"context" +	"errors" +	"fmt" +	"strings" + +	"github.com/go-pg/pg/v10" +	"github.com/go-pg/pg/v10/orm" +	"github.com/sirupsen/logrus" +	"github.com/superseriousbusiness/gotosocial/internal/config" +	"github.com/superseriousbusiness/gotosocial/internal/db" +) + +type basicDB struct { +	config *config.Config +	conn   *pg.DB +	log    *logrus.Logger +	cancel context.CancelFunc +} + +func (b *basicDB) Put(i interface{}) db.Error { +	_, err := b.conn.Model(i).Insert(i) +	if err != nil && strings.Contains(err.Error(), "duplicate key value violates unique constraint") { +		return db.ErrAlreadyExists +	} +	return err +} + +func (b *basicDB) GetByID(id string, i interface{}) db.Error { +	if err := b.conn.Model(i).Where("id = ?", id).Select(); err != nil { +		if err == pg.ErrNoRows { +			return db.ErrNoEntries +		} +		return err + +	} +	return nil +} + +func (b *basicDB) GetWhere(where []db.Where, i interface{}) db.Error { +	if len(where) == 0 { +		return errors.New("no queries provided") +	} + +	q := b.conn.Model(i) +	for _, w := range where { + +		if w.Value == nil { +			q = q.Where("? IS NULL", pg.Ident(w.Key)) +		} else { +			if w.CaseInsensitive { +				q = q.Where("LOWER(?) = LOWER(?)", pg.Safe(w.Key), w.Value) +			} else { +				q = q.Where("? = ?", pg.Safe(w.Key), w.Value) +			} +		} +	} + +	if err := q.Select(); err != nil { +		if err == pg.ErrNoRows { +			return db.ErrNoEntries +		} +		return err +	} +	return nil +} + +func (b *basicDB) GetAll(i interface{}) db.Error { +	if err := b.conn.Model(i).Select(); err != nil { +		if err == pg.ErrNoRows { +			return db.ErrNoEntries +		} +		return err +	} +	return nil +} + +func (b *basicDB) DeleteByID(id string, i interface{}) db.Error { +	if _, err := b.conn.Model(i).Where("id = ?", id).Delete(); err != nil { +		// if there are no rows *anyway* then that's fine +		// just return err if there's an actual error +		if err != pg.ErrNoRows { +			return err +		} +	} +	return nil +} + +func (b *basicDB) DeleteWhere(where []db.Where, i interface{}) db.Error { +	if len(where) == 0 { +		return errors.New("no queries provided") +	} + +	q := b.conn.Model(i) +	for _, w := range where { +		q = q.Where("? = ?", pg.Safe(w.Key), w.Value) +	} + +	if _, err := q.Delete(); err != nil { +		// if there are no rows *anyway* then that's fine +		// just return err if there's an actual error +		if err != pg.ErrNoRows { +			return err +		} +	} +	return nil +} + +func (b *basicDB) Upsert(i interface{}, conflictColumn string) db.Error { +	if _, err := b.conn.Model(i).OnConflict(fmt.Sprintf("(%s) DO UPDATE", conflictColumn)).Insert(); err != nil { +		if err == pg.ErrNoRows { +			return db.ErrNoEntries +		} +		return err +	} +	return nil +} + +func (b *basicDB) UpdateByID(id string, i interface{}) db.Error { +	if _, err := b.conn.Model(i).Where("id = ?", id).OnConflict("(id) DO UPDATE").Insert(); err != nil { +		if err == pg.ErrNoRows { +			return db.ErrNoEntries +		} +		return err +	} +	return nil +} + +func (b *basicDB) UpdateOneByID(id string, key string, value interface{}, i interface{}) db.Error { +	_, err := b.conn.Model(i).Set("? = ?", pg.Safe(key), value).Where("id = ?", id).Update() +	return err +} + +func (b *basicDB) UpdateWhere(where []db.Where, key string, value interface{}, i interface{}) db.Error { +	q := b.conn.Model(i) + +	for _, w := range where { +		if w.Value == nil { +			q = q.Where("? IS NULL", pg.Ident(w.Key)) +		} else { +			if w.CaseInsensitive { +				q = q.Where("LOWER(?) = LOWER(?)", pg.Safe(w.Key), w.Value) +			} else { +				q = q.Where("? = ?", pg.Safe(w.Key), w.Value) +			} +		} +	} + +	q = q.Set("? = ?", pg.Safe(key), value) + +	_, err := q.Update() + +	return err +} + +func (b *basicDB) CreateTable(i interface{}) db.Error { +	return b.conn.Model(i).CreateTable(&orm.CreateTableOptions{ +		IfNotExists: true, +	}) +} + +func (b *basicDB) DropTable(i interface{}) db.Error { +	return b.conn.Model(i).DropTable(&orm.DropTableOptions{ +		IfExists: true, +	}) +} + +func (b *basicDB) RegisterTable(i interface{}) db.Error { +	orm.RegisterTable(i) +	return nil +} + +func (b *basicDB) IsHealthy(ctx context.Context) db.Error { +	return b.conn.Ping(ctx) +} + +func (b *basicDB) Stop(ctx context.Context) db.Error { +	b.log.Info("closing db connection") +	if err := b.conn.Close(); err != nil { +		// only cancel if there's a problem closing the db +		b.cancel() +		return err +	} +	return nil +} diff --git a/internal/db/pg/blocks.go b/internal/db/pg/blocks.go deleted file mode 100644 index a6fc1f859..000000000 --- a/internal/db/pg/blocks.go +++ /dev/null @@ -1,67 +0,0 @@ -/* -   GoToSocial -   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org - -   This program is free software: you can redistribute it and/or modify -   it under the terms of the GNU Affero General Public License as published by -   the Free Software Foundation, either version 3 of the License, or -   (at your option) any later version. - -   This program is distributed in the hope that it will be useful, -   but WITHOUT ANY WARRANTY; without even the implied warranty of -   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the -   GNU Affero General Public License for more details. - -   You should have received a copy of the GNU Affero General Public License -   along with this program.  If not, see <http://www.gnu.org/licenses/>. -*/ - -package pg - -import ( -	"github.com/go-pg/pg/v10" -	"github.com/superseriousbusiness/gotosocial/internal/db" -	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func (ps *postgresService) GetBlocksForAccount(accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, error) { -	blocks := []*gtsmodel.Block{} - -	fq := ps.conn.Model(&blocks). -		Where("block.account_id = ?", accountID). -		Relation("TargetAccount"). -		Order("block.id DESC") - -	if maxID != "" { -		fq = fq.Where("block.id < ?", maxID) -	} - -	if sinceID != "" { -		fq = fq.Where("block.id > ?", sinceID) -	} - -	if limit > 0 { -		fq = fq.Limit(limit) -	} - -	err := fq.Select() -	if err != nil { -		if err == pg.ErrNoRows { -			return nil, "", "", db.ErrNoEntries{} -		} -		return nil, "", "", err -	} - -	if len(blocks) == 0 { -		return nil, "", "", db.ErrNoEntries{} -	} - -	accounts := []*gtsmodel.Account{} -	for _, b := range blocks { -		accounts = append(accounts, b.TargetAccount) -	} - -	nextMaxID := blocks[len(blocks)-1].ID -	prevMinID := blocks[0].ID -	return accounts, nextMaxID, prevMinID, nil -} diff --git a/internal/db/pg/domain.go b/internal/db/pg/domain.go new file mode 100644 index 000000000..4e9b2ab48 --- /dev/null +++ b/internal/db/pg/domain.go @@ -0,0 +1,83 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package pg + +import ( +	"context" +	"net/url" + +	"github.com/go-pg/pg/v10" +	"github.com/sirupsen/logrus" +	"github.com/superseriousbusiness/gotosocial/internal/config" +	"github.com/superseriousbusiness/gotosocial/internal/db" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/util" +) + +type domainDB struct { +	config *config.Config +	conn   *pg.DB +	log    *logrus.Logger +	cancel context.CancelFunc +} + +func (d *domainDB) IsDomainBlocked(domain string) (bool, db.Error) { +	if domain == "" { +		return false, nil +	} + +	blocked, err := d.conn. +		Model(>smodel.DomainBlock{}). +		Where("LOWER(domain) = LOWER(?)", domain). +		Exists() + +	err = processErrorResponse(err) + +	return blocked, err +} + +func (d *domainDB) AreDomainsBlocked(domains []string) (bool, db.Error) { +	// filter out any doubles +	uniqueDomains := util.UniqueStrings(domains) + +	for _, domain := range uniqueDomains { +		if blocked, err := d.IsDomainBlocked(domain); err != nil { +			return false, err +		} else if blocked { +			return blocked, nil +		} +	} + +	// no blocks found +	return false, nil +} + +func (d *domainDB) IsURIBlocked(uri *url.URL) (bool, db.Error) { +	domain := uri.Hostname() +	return d.IsDomainBlocked(domain) +} + +func (d *domainDB) AreURIsBlocked(uris []*url.URL) (bool, db.Error) { +	domains := []string{} +	for _, uri := range uris { +		domains = append(domains, uri.Hostname()) +	} + +	return d.AreDomainsBlocked(domains) +} diff --git a/internal/db/pg/get.go b/internal/db/pg/get.go deleted file mode 100644 index d48c43520..000000000 --- a/internal/db/pg/get.go +++ /dev/null @@ -1,75 +0,0 @@ -/* -   GoToSocial -   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org - -   This program is free software: you can redistribute it and/or modify -   it under the terms of the GNU Affero General Public License as published by -   the Free Software Foundation, either version 3 of the License, or -   (at your option) any later version. - -   This program is distributed in the hope that it will be useful, -   but WITHOUT ANY WARRANTY; without even the implied warranty of -   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the -   GNU Affero General Public License for more details. - -   You should have received a copy of the GNU Affero General Public License -   along with this program.  If not, see <http://www.gnu.org/licenses/>. -*/ - -package pg - -import ( -	"errors" - -	"github.com/go-pg/pg/v10" -	"github.com/superseriousbusiness/gotosocial/internal/db" -) - -func (ps *postgresService) GetByID(id string, i interface{}) error { -	if err := ps.conn.Model(i).Where("id = ?", id).Select(); err != nil { -		if err == pg.ErrNoRows { -			return db.ErrNoEntries{} -		} -		return err - -	} -	return nil -} - -func (ps *postgresService) GetWhere(where []db.Where, i interface{}) error { -	if len(where) == 0 { -		return errors.New("no queries provided") -	} - -	q := ps.conn.Model(i) -	for _, w := range where { - -		if w.Value == nil { -			q = q.Where("? IS NULL", pg.Ident(w.Key)) -		} else { -			if w.CaseInsensitive { -				q = q.Where("LOWER(?) = LOWER(?)", pg.Safe(w.Key), w.Value) -			} else { -				q = q.Where("? = ?", pg.Safe(w.Key), w.Value) -			} -		} -	} - -	if err := q.Select(); err != nil { -		if err == pg.ErrNoRows { -			return db.ErrNoEntries{} -		} -		return err -	} -	return nil -} - -func (ps *postgresService) GetAll(i interface{}) error { -	if err := ps.conn.Model(i).Select(); err != nil { -		if err == pg.ErrNoRows { -			return db.ErrNoEntries{} -		} -		return err -	} -	return nil -} diff --git a/internal/db/pg/instance.go b/internal/db/pg/instance.go index c551b2a49..968832ca5 100644 --- a/internal/db/pg/instance.go +++ b/internal/db/pg/instance.go @@ -19,15 +19,26 @@  package pg  import ( +	"context" +  	"github.com/go-pg/pg/v10" +	"github.com/sirupsen/logrus" +	"github.com/superseriousbusiness/gotosocial/internal/config"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  ) -func (ps *postgresService) GetUserCountForInstance(domain string) (int, error) { -	q := ps.conn.Model(&[]*gtsmodel.Account{}) +type instanceDB struct { +	config *config.Config +	conn   *pg.DB +	log    *logrus.Logger +	cancel context.CancelFunc +} + +func (i *instanceDB) CountInstanceUsers(domain string) (int, db.Error) { +	q := i.conn.Model(&[]*gtsmodel.Account{}) -	if domain == ps.config.Host { +	if domain == i.config.Host {  		// if the domain is *this* domain, just count where the domain field is null  		q = q.Where("? IS NULL", pg.Ident("domain"))  	} else { @@ -40,10 +51,10 @@ func (ps *postgresService) GetUserCountForInstance(domain string) (int, error) {  	return q.Count()  } -func (ps *postgresService) GetStatusCountForInstance(domain string) (int, error) { -	q := ps.conn.Model(&[]*gtsmodel.Status{}) +func (i *instanceDB) CountInstanceStatuses(domain string) (int, db.Error) { +	q := i.conn.Model(&[]*gtsmodel.Status{}) -	if domain == ps.config.Host { +	if domain == i.config.Host {  		// if the domain is *this* domain, just count where local is true  		q = q.Where("local = ?", true)  	} else { @@ -55,10 +66,10 @@ func (ps *postgresService) GetStatusCountForInstance(domain string) (int, error)  	return q.Count()  } -func (ps *postgresService) GetDomainCountForInstance(domain string) (int, error) { -	q := ps.conn.Model(&[]*gtsmodel.Instance{}) +func (i *instanceDB) CountInstanceDomains(domain string) (int, db.Error) { +	q := i.conn.Model(&[]*gtsmodel.Instance{}) -	if domain == ps.config.Host { +	if domain == i.config.Host {  		// if the domain is *this* domain, just count other instances it knows about  		// exclude domains that are blocked  		q = q.Where("domain != ?", domain).Where("? IS NULL", pg.Ident("suspended_at")) @@ -70,12 +81,12 @@ func (ps *postgresService) GetDomainCountForInstance(domain string) (int, error)  	return q.Count()  } -func (ps *postgresService) GetAccountsForInstance(domain string, maxID string, limit int) ([]*gtsmodel.Account, error) { -	ps.log.Debug("GetAccountsForInstance") +func (i *instanceDB) GetInstanceAccounts(domain string, maxID string, limit int) ([]*gtsmodel.Account, db.Error) { +	i.log.Debug("GetAccountsForInstance")  	accounts := []*gtsmodel.Account{} -	q := ps.conn.Model(&accounts).Where("domain = ?", domain).Order("id DESC") +	q := i.conn.Model(&accounts).Where("domain = ?", domain).Order("id DESC")  	if maxID != "" {  		q = q.Where("id < ?", maxID) @@ -88,13 +99,13 @@ func (ps *postgresService) GetAccountsForInstance(domain string, maxID string, l  	err := q.Select()  	if err != nil {  		if err == pg.ErrNoRows { -			return nil, db.ErrNoEntries{} +			return nil, db.ErrNoEntries  		}  		return nil, err  	}  	if len(accounts) == 0 { -		return nil, db.ErrNoEntries{} +		return nil, db.ErrNoEntries  	}  	return accounts, nil diff --git a/internal/db/pg/delete.go b/internal/db/pg/media.go index 0f288353e..618030af3 100644 --- a/internal/db/pg/delete.go +++ b/internal/db/pg/media.go @@ -19,39 +19,35 @@  package pg  import ( -	"errors" +	"context"  	"github.com/go-pg/pg/v10" +	"github.com/go-pg/pg/v10/orm" +	"github.com/sirupsen/logrus" +	"github.com/superseriousbusiness/gotosocial/internal/config"  	"github.com/superseriousbusiness/gotosocial/internal/db" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  ) -func (ps *postgresService) DeleteByID(id string, i interface{}) error { -	if _, err := ps.conn.Model(i).Where("id = ?", id).Delete(); err != nil { -		// if there are no rows *anyway* then that's fine -		// just return err if there's an actual error -		if err != pg.ErrNoRows { -			return err -		} -	} -	return nil +type mediaDB struct { +	config *config.Config +	conn   *pg.DB +	log    *logrus.Logger +	cancel context.CancelFunc  } -func (ps *postgresService) DeleteWhere(where []db.Where, i interface{}) error { -	if len(where) == 0 { -		return errors.New("no queries provided") -	} - -	q := ps.conn.Model(i) -	for _, w := range where { -		q = q.Where("? = ?", pg.Safe(w.Key), w.Value) -	} - -	if _, err := q.Delete(); err != nil { -		// if there are no rows *anyway* then that's fine -		// just return err if there's an actual error -		if err != pg.ErrNoRows { -			return err -		} -	} -	return nil +func (m *mediaDB) newMediaQ(i interface{}) *orm.Query { +	return m.conn.Model(i). +		Relation("Account") +} + +func (m *mediaDB) GetAttachmentByID(id string) (*gtsmodel.MediaAttachment, db.Error) { +	attachment := >smodel.MediaAttachment{} + +	q := m.newMediaQ(attachment). +		Where("media_attachment.id = ?", id) + +	err := processErrorResponse(q.Select()) + +	return attachment, err  } diff --git a/internal/db/pg/mention.go b/internal/db/pg/mention.go new file mode 100644 index 000000000..b31f07b67 --- /dev/null +++ b/internal/db/pg/mention.go @@ -0,0 +1,108 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package pg + +import ( +	"context" + +	"github.com/go-pg/pg/v10" +	"github.com/go-pg/pg/v10/orm" +	"github.com/sirupsen/logrus" +	"github.com/superseriousbusiness/gotosocial/internal/cache" +	"github.com/superseriousbusiness/gotosocial/internal/config" +	"github.com/superseriousbusiness/gotosocial/internal/db" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +type mentionDB struct { +	config *config.Config +	conn   *pg.DB +	log    *logrus.Logger +	cancel context.CancelFunc +	cache  cache.Cache +} + +func (m *mentionDB) cacheMention(id string, mention *gtsmodel.Mention) { +	if m.cache == nil { +		m.cache = cache.New() +	} + +	if err := m.cache.Store(id, mention); err != nil { +		m.log.Panicf("mentionDB: error storing in cache: %s", err) +	} +} + +func (m *mentionDB) mentionCached(id string) (*gtsmodel.Mention, bool) { +	if m.cache == nil { +		m.cache = cache.New() +		return nil, false +	} + +	mI, err := m.cache.Fetch(id) +	if err != nil || mI == nil { +		return nil, false +	} + +	mention, ok := mI.(*gtsmodel.Mention) +	if !ok { +		m.log.Panicf("mentionDB: cached interface with key %s was not a mention", id) +	} + +	return mention, true +} + +func (m *mentionDB) newMentionQ(i interface{}) *orm.Query { +	return m.conn.Model(i). +		Relation("Status"). +		Relation("OriginAccount"). +		Relation("TargetAccount") +} + +func (m *mentionDB) GetMention(id string) (*gtsmodel.Mention, db.Error) { +	if mention, cached := m.mentionCached(id); cached { +		return mention, nil +	} + +	mention := >smodel.Mention{} + +	q := m.newMentionQ(mention). +		Where("mention.id = ?", id) + +	err := processErrorResponse(q.Select()) + +	if err == nil && mention != nil { +		m.cacheMention(id, mention) +	} + +	return mention, err +} + +func (m *mentionDB) GetMentions(ids []string) ([]*gtsmodel.Mention, db.Error) { +	mentions := []*gtsmodel.Mention{} + +	for _, i := range ids { +		mention, err := m.GetMention(i) +		if err != nil { +			return nil, processErrorResponse(err) +		} +		mentions = append(mentions, mention) +	} + +	return mentions, nil +} diff --git a/internal/db/pg/notification.go b/internal/db/pg/notification.go new file mode 100644 index 000000000..281a76d85 --- /dev/null +++ b/internal/db/pg/notification.go @@ -0,0 +1,135 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package pg + +import ( +	"context" + +	"github.com/go-pg/pg/v10" +	"github.com/go-pg/pg/v10/orm" +	"github.com/sirupsen/logrus" +	"github.com/superseriousbusiness/gotosocial/internal/cache" +	"github.com/superseriousbusiness/gotosocial/internal/config" +	"github.com/superseriousbusiness/gotosocial/internal/db" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +type notificationDB struct { +	config *config.Config +	conn   *pg.DB +	log    *logrus.Logger +	cancel context.CancelFunc +	cache  cache.Cache +} + +func (n *notificationDB) cacheNotification(id string, notification *gtsmodel.Notification) { +	if n.cache == nil { +		n.cache = cache.New() +	} + +	if err := n.cache.Store(id, notification); err != nil { +		n.log.Panicf("notificationDB: error storing in cache: %s", err) +	} +} + +func (n *notificationDB) notificationCached(id string) (*gtsmodel.Notification, bool) { +	if n.cache == nil { +		n.cache = cache.New() +		return nil, false +	} + +	nI, err := n.cache.Fetch(id) +	if err != nil || nI == nil { +		return nil, false +	} + +	notification, ok := nI.(*gtsmodel.Notification) +	if !ok { +		n.log.Panicf("notificationDB: cached interface with key %s was not a notification", id) +	} + +	return notification, true +} + +func (n *notificationDB) newNotificationQ(i interface{}) *orm.Query { +	return n.conn.Model(i). +		Relation("OriginAccount"). +		Relation("TargetAccount"). +		Relation("Status") +} + +func (n *notificationDB) GetNotification(id string) (*gtsmodel.Notification, db.Error) { +	if notification, cached := n.notificationCached(id); cached { +		return notification, nil +	} + +	notification := >smodel.Notification{} + +	q := n.newNotificationQ(notification). +		Where("notification.id = ?", id) + +	err := processErrorResponse(q.Select()) + +	if err == nil && notification != nil { +		n.cacheNotification(id, notification) +	} + +	return notification, err +} + +func (n *notificationDB) GetNotifications(accountID string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, db.Error) { +	// begin by selecting just the IDs +	notifIDs := []*gtsmodel.Notification{} +	q := n.conn. +		Model(¬ifIDs). +		Column("id"). +		Where("target_account_id = ?", accountID). +		Order("id DESC") + +	if maxID != "" { +		q = q.Where("id < ?", maxID) +	} + +	if sinceID != "" { +		q = q.Where("id > ?", sinceID) +	} + +	if limit != 0 { +		q = q.Limit(limit) +	} + +	err := processErrorResponse(q.Select()) +	if err != nil { +		return nil, err +	} + +	// now we have the IDs, select the notifs one by one +	// reason for this is that for each notif, we can instead get it from our cache if it's cached +	notifications := []*gtsmodel.Notification{} +	for _, notifID := range notifIDs { +		notif, err := n.GetNotification(notifID.ID) +		errP := processErrorResponse(err) +		if errP != nil { +			return nil, errP +		} +		notifications = append(notifications, notif) +	} + +	return notifications, nil +} diff --git a/internal/db/pg/pg.go b/internal/db/pg/pg.go index d49c50114..0437baf02 100644 --- a/internal/db/pg/pg.go +++ b/internal/db/pg/pg.go @@ -20,15 +20,11 @@ package pg  import (  	"context" -	"crypto/rand" -	"crypto/rsa"  	"crypto/tls"  	"crypto/x509"  	"encoding/pem"  	"errors"  	"fmt" -	"net" -	"net/mail"  	"os"  	"strings"  	"time" @@ -41,12 +37,26 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/id" -	"github.com/superseriousbusiness/gotosocial/internal/util" -	"golang.org/x/crypto/bcrypt"  ) +var registerTables []interface{} = []interface{}{ +	>smodel.StatusToEmoji{}, +	>smodel.StatusToTag{}, +} +  // postgresService satisfies the DB interface  type postgresService struct { +	db.Account +	db.Admin +	db.Basic +	db.Domain +	db.Instance +	db.Media +	db.Mention +	db.Notification +	db.Relationship +	db.Status +	db.Timeline  	config *config.Config  	conn   *pg.DB  	log    *logrus.Logger @@ -56,6 +66,11 @@ type postgresService struct {  // NewPostgresService returns a postgresService derived from the provided config, which implements the go-fed DB interface.  // Under the hood, it uses https://github.com/go-pg/pg to create and maintain a database connection.  func NewPostgresService(ctx context.Context, c *config.Config, log *logrus.Logger) (db.DB, error) { +	for _, t := range registerTables { +		// https://pg.uptrace.dev/orm/many-to-many-relation/ +		orm.RegisterTable(t) +	} +  	opts, err := derivePGOptions(c)  	if err != nil {  		return nil, fmt.Errorf("could not create postgres service: %s", err) @@ -91,6 +106,72 @@ func NewPostgresService(ctx context.Context, c *config.Config, log *logrus.Logge  	log.Infof("connected to postgres version: %s", version)  	ps := &postgresService{ +		Account: &accountDB{ +			config: c, +			conn:   conn, +			log:    log, +			cancel: cancel, +		}, +		Admin: &adminDB{ +			config: c, +			conn:   conn, +			log:    log, +			cancel: cancel, +		}, +		Basic: &basicDB{ +			config: c, +			conn:   conn, +			log:    log, +			cancel: cancel, +		}, +		Domain: &domainDB{ +			config: c, +			conn:   conn, +			log:    log, +			cancel: cancel, +		}, +		Instance: &instanceDB{ +			config: c, +			conn:   conn, +			log:    log, +			cancel: cancel, +		}, +		Media: &mediaDB{ +			config: c, +			conn:   conn, +			log:    log, +			cancel: cancel, +		}, +		Mention: &mentionDB{ +			config: c, +			conn:   conn, +			log:    log, +			cancel: cancel, +		}, +		Notification: ¬ificationDB{ +			config: c, +			conn:   conn, +			log:    log, +			cancel: cancel, +		}, +		Relationship: &relationshipDB{ +			config: c, +			conn:   conn, +			log:    log, +			cancel: cancel, +		}, +		Status: &statusDB{ +			config: c, +			conn:   conn, +			log:    log, +			cancel: cancel, +		}, +		Timeline: &timelineDB{ +			config: c, +			conn:   conn, +			log:    log, +			cancel: cancel, +		},  		config: c,  		conn:   conn,  		log:    log, @@ -200,724 +281,6 @@ func derivePGOptions(c *config.Config) (*pg.Options, error) {  }  /* -	BASIC DB FUNCTIONALITY -*/ - -func (ps *postgresService) CreateTable(i interface{}) error { -	return ps.conn.Model(i).CreateTable(&orm.CreateTableOptions{ -		IfNotExists: true, -	}) -} - -func (ps *postgresService) DropTable(i interface{}) error { -	return ps.conn.Model(i).DropTable(&orm.DropTableOptions{ -		IfExists: true, -	}) -} - -func (ps *postgresService) Stop(ctx context.Context) error { -	ps.log.Info("closing db connection") -	if err := ps.conn.Close(); err != nil { -		// only cancel if there's a problem closing the db -		ps.cancel() -		return err -	} -	return nil -} - -func (ps *postgresService) IsHealthy(ctx context.Context) error { -	return ps.conn.Ping(ctx) -} - -func (ps *postgresService) CreateSchema(ctx context.Context) error { -	models := []interface{}{ -		(*gtsmodel.Account)(nil), -		(*gtsmodel.Status)(nil), -		(*gtsmodel.User)(nil), -	} -	ps.log.Info("creating db schema") - -	for _, model := range models { -		err := ps.conn.Model(model).CreateTable(&orm.CreateTableOptions{ -			IfNotExists: true, -		}) -		if err != nil { -			return err -		} -	} - -	ps.log.Info("db schema created") -	return nil -} - -/* -	HANDY SHORTCUTS -*/ - -func (ps *postgresService) AcceptFollowRequest(originAccountID string, targetAccountID string) (*gtsmodel.Follow, error) { -	// make sure the original follow request exists -	fr := >smodel.FollowRequest{} -	if err := ps.conn.Model(fr).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Select(); err != nil { -		if err == pg.ErrMultiRows { -			return nil, db.ErrNoEntries{} -		} -		return nil, err -	} - -	// create a new follow to 'replace' the request with -	follow := >smodel.Follow{ -		ID:              fr.ID, -		AccountID:       originAccountID, -		TargetAccountID: targetAccountID, -		URI:             fr.URI, -	} - -	// if the follow already exists, just update the URI -- we don't need to do anything else -	if _, err := ps.conn.Model(follow).OnConflict("ON CONSTRAINT follows_account_id_target_account_id_key DO UPDATE set uri = ?", follow.URI).Insert(); err != nil { -		return nil, err -	} - -	// now remove the follow request -	if _, err := ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Delete(); err != nil { -		return nil, err -	} - -	return follow, nil -} - -func (ps *postgresService) CreateInstanceAccount() error { -	username := ps.config.Host -	key, err := rsa.GenerateKey(rand.Reader, 2048) -	if err != nil { -		ps.log.Errorf("error creating new rsa key: %s", err) -		return err -	} - -	aID, err := id.NewRandomULID() -	if err != nil { -		return err -	} - -	newAccountURIs := util.GenerateURIsForAccount(username, ps.config.Protocol, ps.config.Host) -	a := >smodel.Account{ -		ID:                    aID, -		Username:              ps.config.Host, -		DisplayName:           username, -		URL:                   newAccountURIs.UserURL, -		PrivateKey:            key, -		PublicKey:             &key.PublicKey, -		PublicKeyURI:          newAccountURIs.PublicKeyURI, -		ActorType:             gtsmodel.ActivityStreamsPerson, -		URI:                   newAccountURIs.UserURI, -		InboxURI:              newAccountURIs.InboxURI, -		OutboxURI:             newAccountURIs.OutboxURI, -		FollowersURI:          newAccountURIs.FollowersURI, -		FollowingURI:          newAccountURIs.FollowingURI, -		FeaturedCollectionURI: newAccountURIs.CollectionURI, -	} -	inserted, err := ps.conn.Model(a).Where("username = ?", username).SelectOrInsert() -	if err != nil { -		return err -	} -	if inserted { -		ps.log.Infof("created instance account %s with id %s", username, a.ID) -	} else { -		ps.log.Infof("instance account %s already exists with id %s", username, a.ID) -	} -	return nil -} - -func (ps *postgresService) CreateInstanceInstance() error { -	iID, err := id.NewRandomULID() -	if err != nil { -		return err -	} - -	i := >smodel.Instance{ -		ID:     iID, -		Domain: ps.config.Host, -		Title:  ps.config.Host, -		URI:    fmt.Sprintf("%s://%s", ps.config.Protocol, ps.config.Host), -	} -	inserted, err := ps.conn.Model(i).Where("domain = ?", ps.config.Host).SelectOrInsert() -	if err != nil { -		return err -	} -	if inserted { -		ps.log.Infof("created instance instance %s with id %s", ps.config.Host, i.ID) -	} else { -		ps.log.Infof("instance instance %s already exists with id %s", ps.config.Host, i.ID) -	} -	return nil -} - -func (ps *postgresService) GetAccountByUserID(userID string, account *gtsmodel.Account) error { -	user := >smodel.User{ -		ID: userID, -	} -	if err := ps.conn.Model(user).Where("id = ?", userID).Select(); err != nil { -		if err == pg.ErrNoRows { -			return db.ErrNoEntries{} -		} -		return err -	} -	if err := ps.conn.Model(account).Where("id = ?", user.AccountID).Select(); err != nil { -		if err == pg.ErrNoRows { -			return db.ErrNoEntries{} -		} -		return err -	} -	return nil -} - -func (ps *postgresService) GetLocalAccountByUsername(username string, account *gtsmodel.Account) error { -	if err := ps.conn.Model(account).Where("username = ?", username).Where("? IS NULL", pg.Ident("domain")).Select(); err != nil { -		if err == pg.ErrNoRows { -			return db.ErrNoEntries{} -		} -		return err -	} -	return nil -} - -func (ps *postgresService) GetFollowRequestsForAccountID(accountID string, followRequests *[]gtsmodel.FollowRequest) error { -	if err := ps.conn.Model(followRequests).Where("target_account_id = ?", accountID).Select(); err != nil { -		if err == pg.ErrNoRows { -			return nil -		} -		return err -	} -	return nil -} - -func (ps *postgresService) GetFollowingByAccountID(accountID string, following *[]gtsmodel.Follow) error { -	if err := ps.conn.Model(following).Where("account_id = ?", accountID).Select(); err != nil { -		if err == pg.ErrNoRows { -			return nil -		} -		return err -	} -	return nil -} - -func (ps *postgresService) GetFollowersByAccountID(accountID string, followers *[]gtsmodel.Follow, localOnly bool) error { - -	q := ps.conn.Model(followers) - -	if localOnly { -		// for local accounts let's get where domain is null OR where domain is an empty string, just to be safe -		whereGroup := func(q *pg.Query) (*pg.Query, error) { -			q = q. -				WhereOr("? IS NULL", pg.Ident("a.domain")). -				WhereOr("a.domain = ?", "") -			return q, nil -		} - -		q = q.ColumnExpr("follow.*"). -			Join("JOIN accounts AS a ON follow.account_id = TEXT(a.id)"). -			Where("follow.target_account_id = ?", accountID). -			WhereGroup(whereGroup) -	} else { -		q = q.Where("target_account_id = ?", accountID) -	} - -	if err := q.Select(); err != nil { -		if err == pg.ErrNoRows { -			return nil -		} -		return err -	} -	return nil -} - -func (ps *postgresService) GetFavesByAccountID(accountID string, faves *[]gtsmodel.StatusFave) error { -	if err := ps.conn.Model(faves).Where("account_id = ?", accountID).Select(); err != nil { -		if err == pg.ErrNoRows { -			return nil -		} -		return err -	} -	return nil -} - -func (ps *postgresService) CountStatusesByAccountID(accountID string) (int, error) { -	count, err := ps.conn.Model(>smodel.Status{}).Where("account_id = ?", accountID).Count() -	if err != nil { -		if err == pg.ErrNoRows { -			return 0, nil -		} -		return 0, err -	} -	return count, nil -} - -func (ps *postgresService) GetStatusesForAccount(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, error) { -	ps.log.Debugf("getting statuses for account %s", accountID) -	statuses := []*gtsmodel.Status{} - -	q := ps.conn.Model(&statuses).Order("id DESC") -	if accountID != "" { -		q = q.Where("account_id = ?", accountID) -	} - -	if limit != 0 { -		q = q.Limit(limit) -	} - -	if excludeReplies { -		q = q.Where("? IS NULL", pg.Ident("in_reply_to_id")) -	} - -	if pinnedOnly { -		q = q.Where("pinned = ?", true) -	} - -	if mediaOnly { -		q = q.WhereGroup(func(q *pg.Query) (*pg.Query, error) { -			return q.Where("? IS NOT NULL", pg.Ident("attachments")).Where("attachments != '{}'"), nil -		}) -	} - -	if maxID != "" { -		q = q.Where("id < ?", maxID) -	} - -	if err := q.Select(); err != nil { -		if err == pg.ErrNoRows { -			return nil, db.ErrNoEntries{} -		} -		return nil, err -	} - -	if len(statuses) == 0 { -		return nil, db.ErrNoEntries{} -	} - -	ps.log.Debugf("returning statuses for account %s", accountID) -	return statuses, nil -} - -func (ps *postgresService) GetLastStatusForAccountID(accountID string, status *gtsmodel.Status) error { -	if err := ps.conn.Model(status).Order("created_at DESC").Limit(1).Where("account_id = ?", accountID).Select(); err != nil { -		if err == pg.ErrNoRows { -			return db.ErrNoEntries{} -		} -		return err -	} -	return nil - -} - -func (ps *postgresService) IsUsernameAvailable(username string) error { -	// if no error we fail because it means we found something -	// if error but it's not pg.ErrNoRows then we fail -	// if err is pg.ErrNoRows we're good, we found nothing so continue -	if err := ps.conn.Model(>smodel.Account{}).Where("username = ?", username).Where("domain = ?", nil).Select(); err == nil { -		return fmt.Errorf("username %s already in use", username) -	} else if err != pg.ErrNoRows { -		return fmt.Errorf("db error: %s", err) -	} -	return nil -} - -func (ps *postgresService) IsEmailAvailable(email string) error { -	// parse the domain from the email -	m, err := mail.ParseAddress(email) -	if err != nil { -		return fmt.Errorf("error parsing email address %s: %s", email, err) -	} -	domain := strings.Split(m.Address, "@")[1] // domain will always be the second part after @ - -	// check if the email domain is blocked -	if err := ps.conn.Model(>smodel.EmailDomainBlock{}).Where("domain = ?", domain).Select(); err == nil { -		// fail because we found something -		return fmt.Errorf("email domain %s is blocked", domain) -	} else if err != pg.ErrNoRows { -		// fail because we got an unexpected error -		return fmt.Errorf("db error: %s", err) -	} - -	// check if this email is associated with a user already -	if err := ps.conn.Model(>smodel.User{}).Where("email = ?", email).WhereOr("unconfirmed_email = ?", email).Select(); err == nil { -		// fail because we found something -		return fmt.Errorf("email %s already in use", email) -	} else if err != pg.ErrNoRows { -		// fail because we got an unexpected error -		return fmt.Errorf("db error: %s", err) -	} -	return nil -} - -func (ps *postgresService) NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string, emailVerified bool, admin bool) (*gtsmodel.User, error) { -	key, err := rsa.GenerateKey(rand.Reader, 2048) -	if err != nil { -		ps.log.Errorf("error creating new rsa key: %s", err) -		return nil, err -	} - -	// if something went wrong while creating a user, we might already have an account, so check here first... -	a := >smodel.Account{} -	err = ps.conn.Model(a).Where("username = ?", username).Where("? IS NULL", pg.Ident("domain")).Select() -	if err != nil { -		// there's been an actual error -		if err != pg.ErrNoRows { -			return nil, fmt.Errorf("db error checking existence of account: %s", err) -		} - -		// we just don't have an account yet create one -		newAccountURIs := util.GenerateURIsForAccount(username, ps.config.Protocol, ps.config.Host) -		newAccountID, err := id.NewRandomULID() -		if err != nil { -			return nil, err -		} - -		a = >smodel.Account{ -			ID:                    newAccountID, -			Username:              username, -			DisplayName:           username, -			Reason:                reason, -			URL:                   newAccountURIs.UserURL, -			PrivateKey:            key, -			PublicKey:             &key.PublicKey, -			PublicKeyURI:          newAccountURIs.PublicKeyURI, -			ActorType:             gtsmodel.ActivityStreamsPerson, -			URI:                   newAccountURIs.UserURI, -			InboxURI:              newAccountURIs.InboxURI, -			OutboxURI:             newAccountURIs.OutboxURI, -			FollowersURI:          newAccountURIs.FollowersURI, -			FollowingURI:          newAccountURIs.FollowingURI, -			FeaturedCollectionURI: newAccountURIs.CollectionURI, -		} -		if _, err = ps.conn.Model(a).Insert(); err != nil { -			return nil, err -		} -	} - -	pw, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) -	if err != nil { -		return nil, fmt.Errorf("error hashing password: %s", err) -	} - -	newUserID, err := id.NewRandomULID() -	if err != nil { -		return nil, err -	} - -	u := >smodel.User{ -		ID:                     newUserID, -		AccountID:              a.ID, -		EncryptedPassword:      string(pw), -		SignUpIP:               signUpIP.To4(), -		Locale:                 locale, -		UnconfirmedEmail:       email, -		CreatedByApplicationID: appID, -		Approved:               !requireApproval, // if we don't require moderator approval, just pre-approve the user -	} - -	if emailVerified { -		u.ConfirmedAt = time.Now() -		u.Email = email -	} - -	if admin { -		u.Admin = true -		u.Moderator = true -	} - -	if _, err = ps.conn.Model(u).Insert(); err != nil { -		return nil, err -	} - -	return u, nil -} - -func (ps *postgresService) SetHeaderOrAvatarForAccountID(mediaAttachment *gtsmodel.MediaAttachment, accountID string) error { -	if mediaAttachment.Avatar && mediaAttachment.Header { -		return errors.New("one media attachment cannot be both header and avatar") -	} - -	var headerOrAVI string -	if mediaAttachment.Avatar { -		headerOrAVI = "avatar" -	} else if mediaAttachment.Header { -		headerOrAVI = "header" -	} else { -		return errors.New("given media attachment was neither a header nor an avatar") -	} - -	// TODO: there are probably more side effects here that need to be handled -	if _, err := ps.conn.Model(mediaAttachment).OnConflict("(id) DO UPDATE").Insert(); err != nil { -		return err -	} - -	if _, err := ps.conn.Model(>smodel.Account{}).Set(fmt.Sprintf("%s_media_attachment_id = ?", headerOrAVI), mediaAttachment.ID).Where("id = ?", accountID).Update(); err != nil { -		return err -	} -	return nil -} - -func (ps *postgresService) GetHeaderForAccountID(header *gtsmodel.MediaAttachment, accountID string) error { -	acct := >smodel.Account{} -	if err := ps.conn.Model(acct).Where("id = ?", accountID).Select(); err != nil { -		if err == pg.ErrNoRows { -			return db.ErrNoEntries{} -		} -		return err -	} - -	if acct.HeaderMediaAttachmentID == "" { -		return db.ErrNoEntries{} -	} - -	if err := ps.conn.Model(header).Where("id = ?", acct.HeaderMediaAttachmentID).Select(); err != nil { -		if err == pg.ErrNoRows { -			return db.ErrNoEntries{} -		} -		return err -	} -	return nil -} - -func (ps *postgresService) GetAvatarForAccountID(avatar *gtsmodel.MediaAttachment, accountID string) error { -	acct := >smodel.Account{} -	if err := ps.conn.Model(acct).Where("id = ?", accountID).Select(); err != nil { -		if err == pg.ErrNoRows { -			return db.ErrNoEntries{} -		} -		return err -	} - -	if acct.AvatarMediaAttachmentID == "" { -		return db.ErrNoEntries{} -	} - -	if err := ps.conn.Model(avatar).Where("id = ?", acct.AvatarMediaAttachmentID).Select(); err != nil { -		if err == pg.ErrNoRows { -			return db.ErrNoEntries{} -		} -		return err -	} -	return nil -} - -func (ps *postgresService) Blocked(account1 string, account2 string) (bool, error) { -	// TODO: check domain blocks as well -	var blocked bool -	if err := ps.conn.Model(>smodel.Block{}). -		Where("account_id = ?", account1).Where("target_account_id = ?", account2). -		WhereOr("target_account_id = ?", account1).Where("account_id = ?", account2). -		Select(); err != nil { -		if err == pg.ErrNoRows { -			blocked = false -			return blocked, nil -		} -		return blocked, err -	} -	blocked = true -	return blocked, nil -} - -func (ps *postgresService) GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, error) { -	r := >smodel.Relationship{ -		ID: targetAccount, -	} - -	// check if the requesting account follows the target account -	follow := >smodel.Follow{} -	if err := ps.conn.Model(follow).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Select(); err != nil { -		if err != pg.ErrNoRows { -			// a proper error -			return nil, fmt.Errorf("getrelationship: error checking follow existence: %s", err) -		} -		// no follow exists so these are all false -		r.Following = false -		r.ShowingReblogs = false -		r.Notifying = false -	} else { -		// follow exists so we can fill these fields out... -		r.Following = true -		r.ShowingReblogs = follow.ShowReblogs -		r.Notifying = follow.Notify -	} - -	// check if the target account follows the requesting account -	followedBy, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", targetAccount).Where("target_account_id = ?", requestingAccount).Exists() -	if err != nil { -		return nil, fmt.Errorf("getrelationship: error checking followed_by existence: %s", err) -	} -	r.FollowedBy = followedBy - -	// check if the requesting account blocks the target account -	blocking, err := ps.conn.Model(>smodel.Block{}).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Exists() -	if err != nil { -		return nil, fmt.Errorf("getrelationship: error checking blocking existence: %s", err) -	} -	r.Blocking = blocking - -	// check if the target account blocks the requesting account -	blockedBy, err := ps.conn.Model(>smodel.Block{}).Where("account_id = ?", targetAccount).Where("target_account_id = ?", requestingAccount).Exists() -	if err != nil { -		return nil, fmt.Errorf("getrelationship: error checking blocked existence: %s", err) -	} -	r.BlockedBy = blockedBy - -	// check if there's a pending following request from requesting account to target account -	requested, err := ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Exists() -	if err != nil { -		return nil, fmt.Errorf("getrelationship: error checking blocked existence: %s", err) -	} -	r.Requested = requested - -	return r, nil -} - -func (ps *postgresService) Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) { -	if sourceAccount == nil || targetAccount == nil { -		return false, nil -	} - -	return ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists() -} - -func (ps *postgresService) FollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) { -	if sourceAccount == nil || targetAccount == nil { -		return false, nil -	} - -	return ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists() -} - -func (ps *postgresService) Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error) { -	if account1 == nil || account2 == nil { -		return false, nil -	} - -	// make sure account 1 follows account 2 -	f1, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", account1.ID).Where("target_account_id = ?", account2.ID).Exists() -	if err != nil { -		if err == pg.ErrNoRows { -			return false, nil -		} -		return false, err -	} - -	// make sure account 2 follows account 1 -	f2, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", account2.ID).Where("target_account_id = ?", account1.ID).Exists() -	if err != nil { -		if err == pg.ErrNoRows { -			return false, nil -		} -		return false, err -	} - -	return f1 && f2, nil -} - -func (ps *postgresService) GetReplyCountForStatus(status *gtsmodel.Status) (int, error) { -	return ps.conn.Model(>smodel.Status{}).Where("in_reply_to_id = ?", status.ID).Count() -} - -func (ps *postgresService) GetReblogCountForStatus(status *gtsmodel.Status) (int, error) { -	return ps.conn.Model(>smodel.Status{}).Where("boost_of_id = ?", status.ID).Count() -} - -func (ps *postgresService) GetFaveCountForStatus(status *gtsmodel.Status) (int, error) { -	return ps.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Count() -} - -func (ps *postgresService) StatusFavedBy(status *gtsmodel.Status, accountID string) (bool, error) { -	return ps.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() -} - -func (ps *postgresService) StatusRebloggedBy(status *gtsmodel.Status, accountID string) (bool, error) { -	return ps.conn.Model(>smodel.Status{}).Where("boost_of_id = ?", status.ID).Where("account_id = ?", accountID).Exists() -} - -func (ps *postgresService) StatusMutedBy(status *gtsmodel.Status, accountID string) (bool, error) { -	return ps.conn.Model(>smodel.StatusMute{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() -} - -func (ps *postgresService) StatusBookmarkedBy(status *gtsmodel.Status, accountID string) (bool, error) { -	return ps.conn.Model(>smodel.StatusBookmark{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() -} - -func (ps *postgresService) WhoFavedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error) { -	accounts := []*gtsmodel.Account{} - -	faves := []*gtsmodel.StatusFave{} -	if err := ps.conn.Model(&faves).Where("status_id = ?", status.ID).Select(); err != nil { -		if err == pg.ErrNoRows { -			return accounts, nil // no rows just means nobody has faved this status, so that's fine -		} -		return nil, err // an actual error has occurred -	} - -	for _, f := range faves { -		acc := >smodel.Account{} -		if err := ps.conn.Model(acc).Where("id = ?", f.AccountID).Select(); err != nil { -			if err == pg.ErrNoRows { -				continue // the account doesn't exist for some reason??? but this isn't the place to worry about that so just skip it -			} -			return nil, err // an actual error has occurred -		} -		accounts = append(accounts, acc) -	} -	return accounts, nil -} - -func (ps *postgresService) WhoBoostedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error) { -	accounts := []*gtsmodel.Account{} - -	boosts := []*gtsmodel.Status{} -	if err := ps.conn.Model(&boosts).Where("boost_of_id = ?", status.ID).Select(); err != nil { -		if err == pg.ErrNoRows { -			return accounts, nil // no rows just means nobody has boosted this status, so that's fine -		} -		return nil, err // an actual error has occurred -	} - -	for _, f := range boosts { -		acc := >smodel.Account{} -		if err := ps.conn.Model(acc).Where("id = ?", f.AccountID).Select(); err != nil { -			if err == pg.ErrNoRows { -				continue // the account doesn't exist for some reason??? but this isn't the place to worry about that so just skip it -			} -			return nil, err // an actual error has occurred -		} -		accounts = append(accounts, acc) -	} -	return accounts, nil -} - -func (ps *postgresService) GetNotificationsForAccount(accountID string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, error) { -	notifications := []*gtsmodel.Notification{} - -	q := ps.conn.Model(¬ifications).Where("target_account_id = ?", accountID) - -	if maxID != "" { -		q = q.Where("id < ?", maxID) -	} - -	if sinceID != "" { -		q = q.Where("id > ?", sinceID) -	} - -	if limit != 0 { -		q = q.Limit(limit) -	} - -	q = q.Order("created_at DESC") - -	if err := q.Select(); err != nil { -		if err != pg.ErrNoRows { -			return nil, err -		} - -	} -	return notifications, nil -} - -/*  	CONVERSION FUNCTIONS  */ @@ -988,14 +351,14 @@ func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, ori  		// id, createdAt and updatedAt will be populated by the db, so we have everything we need!  		menchies = append(menchies, >smodel.Mention{ -			StatusID:            statusID, -			OriginAccountID:     ogAccount.ID, -			OriginAccountURI:    ogAccount.URI, -			TargetAccountID:     mentionedAccount.ID, -			NameString:          a, -			MentionedAccountURI: mentionedAccount.URI, -			MentionedAccountURL: mentionedAccount.URL, -			GTSAccount:          mentionedAccount, +			StatusID:         statusID, +			OriginAccountID:  ogAccount.ID, +			OriginAccountURI: ogAccount.URI, +			TargetAccountID:  mentionedAccount.ID, +			NameString:       a, +			TargetAccountURI: mentionedAccount.URI, +			TargetAccountURL: mentionedAccount.URL, +			OriginAccount:    mentionedAccount,  		})  	}  	return menchies, nil diff --git a/internal/db/pg/pg_test.go b/internal/db/pg/pg_test.go new file mode 100644 index 000000000..c1e10abdf --- /dev/null +++ b/internal/db/pg/pg_test.go @@ -0,0 +1,47 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package pg_test + +import ( +	"github.com/sirupsen/logrus" +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/config" +	"github.com/superseriousbusiness/gotosocial/internal/db" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +type PGStandardTestSuite struct { +	// standard suite interfaces +	suite.Suite +	config *config.Config +	db     db.DB +	log    *logrus.Logger + +	// standard suite models +	testTokens       map[string]*oauth.Token +	testClients      map[string]*oauth.Client +	testApplications map[string]*gtsmodel.Application +	testUsers        map[string]*gtsmodel.User +	testAccounts     map[string]*gtsmodel.Account +	testAttachments  map[string]*gtsmodel.MediaAttachment +	testStatuses     map[string]*gtsmodel.Status +	testTags         map[string]*gtsmodel.Tag +	testMentions     map[string]*gtsmodel.Mention +} diff --git a/internal/db/pg/relationship.go b/internal/db/pg/relationship.go new file mode 100644 index 000000000..76bd50c76 --- /dev/null +++ b/internal/db/pg/relationship.go @@ -0,0 +1,276 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package pg + +import ( +	"context" +	"fmt" + +	"github.com/go-pg/pg/v10" +	"github.com/go-pg/pg/v10/orm" +	"github.com/sirupsen/logrus" +	"github.com/superseriousbusiness/gotosocial/internal/config" +	"github.com/superseriousbusiness/gotosocial/internal/db" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +type relationshipDB struct { +	config *config.Config +	conn   *pg.DB +	log    *logrus.Logger +	cancel context.CancelFunc +} + +func (r *relationshipDB) newBlockQ(block *gtsmodel.Block) *orm.Query { +	return r.conn.Model(block). +		Relation("Account"). +		Relation("TargetAccount") +} + +func (r *relationshipDB) newFollowQ(follow interface{}) *orm.Query { +	return r.conn.Model(follow). +		Relation("Account"). +		Relation("TargetAccount") +} + +func (r *relationshipDB) IsBlocked(account1 string, account2 string, eitherDirection bool) (bool, db.Error) { +	q := r.conn. +		Model(>smodel.Block{}). +		Where("account_id = ?", account1). +		Where("target_account_id = ?", account2) + +	if eitherDirection { +		q = q. +			WhereOr("target_account_id = ?", account1). +			Where("account_id = ?", account2) +	} + +	return q.Exists() +} + +func (r *relationshipDB) GetBlock(account1 string, account2 string) (*gtsmodel.Block, db.Error) { +	block := >smodel.Block{} + +	q := r.newBlockQ(block). +		Where("block.account_id = ?", account1). +		Where("block.target_account_id = ?", account2) + +	err := processErrorResponse(q.Select()) + +	return block, err +} + +func (r *relationshipDB) GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, db.Error) { +	rel := >smodel.Relationship{ +		ID: targetAccount, +	} + +	// check if the requesting account follows the target account +	follow := >smodel.Follow{} +	if err := r.conn.Model(follow).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Select(); err != nil { +		if err != pg.ErrNoRows { +			// a proper error +			return nil, fmt.Errorf("getrelationship: error checking follow existence: %s", err) +		} +		// no follow exists so these are all false +		rel.Following = false +		rel.ShowingReblogs = false +		rel.Notifying = false +	} else { +		// follow exists so we can fill these fields out... +		rel.Following = true +		rel.ShowingReblogs = follow.ShowReblogs +		rel.Notifying = follow.Notify +	} + +	// check if the target account follows the requesting account +	followedBy, err := r.conn.Model(>smodel.Follow{}).Where("account_id = ?", targetAccount).Where("target_account_id = ?", requestingAccount).Exists() +	if err != nil { +		return nil, fmt.Errorf("getrelationship: error checking followed_by existence: %s", err) +	} +	rel.FollowedBy = followedBy + +	// check if the requesting account blocks the target account +	blocking, err := r.conn.Model(>smodel.Block{}).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Exists() +	if err != nil { +		return nil, fmt.Errorf("getrelationship: error checking blocking existence: %s", err) +	} +	rel.Blocking = blocking + +	// check if the target account blocks the requesting account +	blockedBy, err := r.conn.Model(>smodel.Block{}).Where("account_id = ?", targetAccount).Where("target_account_id = ?", requestingAccount).Exists() +	if err != nil { +		return nil, fmt.Errorf("getrelationship: error checking blocked existence: %s", err) +	} +	rel.BlockedBy = blockedBy + +	// check if there's a pending following request from requesting account to target account +	requested, err := r.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Exists() +	if err != nil { +		return nil, fmt.Errorf("getrelationship: error checking blocked existence: %s", err) +	} +	rel.Requested = requested + +	return rel, nil +} + +func (r *relationshipDB) IsFollowing(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, db.Error) { +	if sourceAccount == nil || targetAccount == nil { +		return false, nil +	} + +	q := r.conn. +		Model(>smodel.Follow{}). +		Where("account_id = ?", sourceAccount.ID). +		Where("target_account_id = ?", targetAccount.ID) + +	return q.Exists() +} + +func (r *relationshipDB) IsFollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, db.Error) { +	if sourceAccount == nil || targetAccount == nil { +		return false, nil +	} + +	q := r.conn. +		Model(>smodel.FollowRequest{}). +		Where("account_id = ?", sourceAccount.ID). +		Where("target_account_id = ?", targetAccount.ID) + +	return q.Exists() +} + +func (r *relationshipDB) IsMutualFollowing(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, db.Error) { +	if account1 == nil || account2 == nil { +		return false, nil +	} + +	// make sure account 1 follows account 2 +	f1, err := r.IsFollowing(account1, account2) +	if err != nil { +		return false, processErrorResponse(err) +	} + +	// make sure account 2 follows account 1 +	f2, err := r.IsFollowing(account2, account1) +	if err != nil { +		return false, processErrorResponse(err) +	} + +	return f1 && f2, nil +} + +func (r *relationshipDB) AcceptFollowRequest(originAccountID string, targetAccountID string) (*gtsmodel.Follow, db.Error) { +	// make sure the original follow request exists +	fr := >smodel.FollowRequest{} +	if err := r.conn.Model(fr).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Select(); err != nil { +		if err == pg.ErrMultiRows { +			return nil, db.ErrNoEntries +		} +		return nil, err +	} + +	// create a new follow to 'replace' the request with +	follow := >smodel.Follow{ +		ID:              fr.ID, +		AccountID:       originAccountID, +		TargetAccountID: targetAccountID, +		URI:             fr.URI, +	} + +	// if the follow already exists, just update the URI -- we don't need to do anything else +	if _, err := r.conn.Model(follow).OnConflict("ON CONSTRAINT follows_account_id_target_account_id_key DO UPDATE set uri = ?", follow.URI).Insert(); err != nil { +		return nil, err +	} + +	// now remove the follow request +	if _, err := r.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Delete(); err != nil { +		return nil, err +	} + +	return follow, nil +} + +func (r *relationshipDB) GetAccountFollowRequests(accountID string) ([]*gtsmodel.FollowRequest, db.Error) { +	followRequests := []*gtsmodel.FollowRequest{} + +	q := r.newFollowQ(&followRequests). +		Where("target_account_id = ?", accountID) + +	err := processErrorResponse(q.Select()) + +	return followRequests, err +} + +func (r *relationshipDB) GetAccountFollows(accountID string) ([]*gtsmodel.Follow, db.Error) { +	follows := []*gtsmodel.Follow{} + +	q := r.newFollowQ(&follows). +		Where("account_id = ?", accountID) + +	err := processErrorResponse(q.Select()) + +	return follows, err +} + +func (r *relationshipDB) CountAccountFollows(accountID string, localOnly bool) (int, db.Error) { +	return r.conn. +		Model(&[]*gtsmodel.Follow{}). +		Where("account_id = ?", accountID). +		Count() +} + +func (r *relationshipDB) GetAccountFollowedBy(accountID string, localOnly bool) ([]*gtsmodel.Follow, db.Error) { + +	follows := []*gtsmodel.Follow{} + +	q := r.conn.Model(&follows) + +	if localOnly { +		// for local accounts let's get where domain is null OR where domain is an empty string, just to be safe +		whereGroup := func(q *pg.Query) (*pg.Query, error) { +			q = q. +				WhereOr("? IS NULL", pg.Ident("a.domain")). +				WhereOr("a.domain = ?", "") +			return q, nil +		} + +		q = q.ColumnExpr("follow.*"). +			Join("JOIN accounts AS a ON follow.account_id = TEXT(a.id)"). +			Where("follow.target_account_id = ?", accountID). +			WhereGroup(whereGroup) +	} else { +		q = q.Where("target_account_id = ?", accountID) +	} + +	if err := q.Select(); err != nil { +		if err == pg.ErrNoRows { +			return follows, nil +		} +		return nil, err +	} +	return follows, nil +} + +func (r *relationshipDB) CountAccountFollowedBy(accountID string, localOnly bool) (int, db.Error) { +	return r.conn. +		Model(&[]*gtsmodel.Follow{}). +		Where("target_account_id = ?", accountID). +		Count() +} diff --git a/internal/db/pg/status.go b/internal/db/pg/status.go new file mode 100644 index 000000000..99790428e --- /dev/null +++ b/internal/db/pg/status.go @@ -0,0 +1,318 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package pg + +import ( +	"container/list" +	"context" +	"errors" +	"time" + +	"github.com/go-pg/pg/v10" +	"github.com/go-pg/pg/v10/orm" +	"github.com/sirupsen/logrus" +	"github.com/superseriousbusiness/gotosocial/internal/cache" +	"github.com/superseriousbusiness/gotosocial/internal/config" +	"github.com/superseriousbusiness/gotosocial/internal/db" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +type statusDB struct { +	config *config.Config +	conn   *pg.DB +	log    *logrus.Logger +	cancel context.CancelFunc +	cache  cache.Cache +} + +func (s *statusDB) cacheStatus(id string, status *gtsmodel.Status) { +	if s.cache == nil { +		s.cache = cache.New() +	} + +	if err := s.cache.Store(id, status); err != nil { +		s.log.Panicf("statusDB: error storing in cache: %s", err) +	} +} + +func (s *statusDB) statusCached(id string) (*gtsmodel.Status, bool) { +	if s.cache == nil { +		s.cache = cache.New() +		return nil, false +	} + +	sI, err := s.cache.Fetch(id) +	if err != nil || sI == nil { +		return nil, false +	} + +	status, ok := sI.(*gtsmodel.Status) +	if !ok { +		s.log.Panicf("statusDB: cached interface with key %s was not a status", id) +	} + +	return status, true +} + +func (s *statusDB) newStatusQ(status interface{}) *orm.Query { +	return s.conn.Model(status). +		Relation("Attachments"). +		Relation("Tags"). +		Relation("Mentions"). +		Relation("Emojis"). +		Relation("Account"). +		Relation("InReplyTo"). +		Relation("InReplyToAccount"). +		Relation("BoostOf"). +		Relation("BoostOfAccount"). +		Relation("CreatedWithApplication") +} + +func (s *statusDB) newFaveQ(faves interface{}) *orm.Query { +	return s.conn.Model(faves). +		Relation("Account"). +		Relation("TargetAccount"). +		Relation("Status") +} + +func (s *statusDB) GetStatusByID(id string) (*gtsmodel.Status, db.Error) { +	if status, cached := s.statusCached(id); cached { +		return status, nil +	} + +	status := >smodel.Status{} + +	q := s.newStatusQ(status). +		Where("status.id = ?", id) + +	err := processErrorResponse(q.Select()) + +	if err == nil && status != nil { +		s.cacheStatus(id, status) +	} + +	return status, err +} + +func (s *statusDB) GetStatusByURI(uri string) (*gtsmodel.Status, db.Error) { +	if status, cached := s.statusCached(uri); cached { +		return status, nil +	} + +	status := >smodel.Status{} + +	q := s.newStatusQ(status). +		Where("LOWER(status.uri) = LOWER(?)", uri) + +	err := processErrorResponse(q.Select()) + +	if err == nil && status != nil { +		s.cacheStatus(uri, status) +	} + +	return status, err +} + +func (s *statusDB) GetStatusByURL(uri string) (*gtsmodel.Status, db.Error) { +	if status, cached := s.statusCached(uri); cached { +		return status, nil +	} + +	status := >smodel.Status{} + +	q := s.newStatusQ(status). +		Where("LOWER(status.url) = LOWER(?)", uri) + +	err := processErrorResponse(q.Select()) + +	if err == nil && status != nil { +		s.cacheStatus(uri, status) +	} + +	return status, err +} + +func (s *statusDB) PutStatus(status *gtsmodel.Status) db.Error { +	transaction := func(tx *pg.Tx) error { +		// create links between this status and any emojis it uses +		for _, i := range status.EmojiIDs { +			if _, err := tx.Model(>smodel.StatusToEmoji{ +				StatusID: status.ID, +				EmojiID:  i, +			}).Insert(); err != nil { +				return err +			} +		} + +		// create links between this status and any tags it uses +		for _, i := range status.TagIDs { +			if _, err := tx.Model(>smodel.StatusToTag{ +				StatusID: status.ID, +				TagID:    i, +			}).Insert(); err != nil { +				return err +			} +		} + +		// change the status ID of the media attachments to the new status +		for _, a := range status.Attachments { +			a.StatusID = status.ID +			a.UpdatedAt = time.Now() +			if _, err := s.conn.Model(a). +				Where("id = ?", a.ID). +				Update(); err != nil { +				return err +			} +		} + +		_, err := tx.Model(status).Insert() +		return err +	} + +	return processErrorResponse(s.conn.RunInTransaction(context.Background(), transaction)) +} + +func (s *statusDB) GetStatusParents(status *gtsmodel.Status, onlyDirect bool) ([]*gtsmodel.Status, db.Error) { +	parents := []*gtsmodel.Status{} +	s.statusParent(status, &parents, onlyDirect) + +	return parents, nil +} + +func (s *statusDB) statusParent(status *gtsmodel.Status, foundStatuses *[]*gtsmodel.Status, onlyDirect bool) { +	if status.InReplyToID == "" { +		return +	} + +	parentStatus, err := s.GetStatusByID(status.InReplyToID) +	if err == nil { +		*foundStatuses = append(*foundStatuses, parentStatus) +	} + +	if onlyDirect { +		return +	} + +	s.statusParent(parentStatus, foundStatuses, false) +} + +func (s *statusDB) GetStatusChildren(status *gtsmodel.Status, onlyDirect bool, minID string) ([]*gtsmodel.Status, db.Error) { +	foundStatuses := &list.List{} +	foundStatuses.PushFront(status) +	s.statusChildren(status, foundStatuses, onlyDirect, minID) + +	children := []*gtsmodel.Status{} +	for e := foundStatuses.Front(); e != nil; e = e.Next() { +		entry, ok := e.Value.(*gtsmodel.Status) +		if !ok { +			panic(errors.New("entry in foundStatuses was not a *gtsmodel.Status")) +		} + +		// only append children, not the overall parent status +		if entry.ID != status.ID { +			children = append(children, entry) +		} +	} + +	return children, nil +} + +func (s *statusDB) statusChildren(status *gtsmodel.Status, foundStatuses *list.List, onlyDirect bool, minID string) { +	immediateChildren := []*gtsmodel.Status{} + +	q := s.conn.Model(&immediateChildren).Where("in_reply_to_id = ?", status.ID) +	if minID != "" { +		q = q.Where("status.id > ?", minID) +	} + +	if err := q.Select(); err != nil { +		return +	} + +	for _, child := range immediateChildren { +	insertLoop: +		for e := foundStatuses.Front(); e != nil; e = e.Next() { +			entry, ok := e.Value.(*gtsmodel.Status) +			if !ok { +				panic(errors.New("entry in foundStatuses was not a *gtsmodel.Status")) +			} + +			if child.InReplyToAccountID != "" && entry.ID == child.InReplyToID { +				foundStatuses.InsertAfter(child, e) +				break insertLoop +			} +		} + +		// only do one loop if we only want direct children +		if onlyDirect { +			return +		} +		s.statusChildren(child, foundStatuses, false, minID) +	} +} + +func (s *statusDB) CountStatusReplies(status *gtsmodel.Status) (int, db.Error) { +	return s.conn.Model(>smodel.Status{}).Where("in_reply_to_id = ?", status.ID).Count() +} + +func (s *statusDB) CountStatusReblogs(status *gtsmodel.Status) (int, db.Error) { +	return s.conn.Model(>smodel.Status{}).Where("boost_of_id = ?", status.ID).Count() +} + +func (s *statusDB) CountStatusFaves(status *gtsmodel.Status) (int, db.Error) { +	return s.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Count() +} + +func (s *statusDB) IsStatusFavedBy(status *gtsmodel.Status, accountID string) (bool, db.Error) { +	return s.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() +} + +func (s *statusDB) IsStatusRebloggedBy(status *gtsmodel.Status, accountID string) (bool, db.Error) { +	return s.conn.Model(>smodel.Status{}).Where("boost_of_id = ?", status.ID).Where("account_id = ?", accountID).Exists() +} + +func (s *statusDB) IsStatusMutedBy(status *gtsmodel.Status, accountID string) (bool, db.Error) { +	return s.conn.Model(>smodel.StatusMute{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() +} + +func (s *statusDB) IsStatusBookmarkedBy(status *gtsmodel.Status, accountID string) (bool, db.Error) { +	return s.conn.Model(>smodel.StatusBookmark{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() +} + +func (s *statusDB) GetStatusFaves(status *gtsmodel.Status) ([]*gtsmodel.StatusFave, db.Error) { +	faves := []*gtsmodel.StatusFave{} + +	q := s.newFaveQ(&faves). +		Where("status_id = ?", status.ID) + +	err := processErrorResponse(q.Select()) + +	return faves, err +} + +func (s *statusDB) GetStatusReblogs(status *gtsmodel.Status) ([]*gtsmodel.Status, db.Error) { +	reblogs := []*gtsmodel.Status{} + +	q := s.newStatusQ(&reblogs). +		Where("boost_of_id = ?", status.ID) + +	err := processErrorResponse(q.Select()) + +	return reblogs, err +} diff --git a/internal/db/pg/status_test.go b/internal/db/pg/status_test.go new file mode 100644 index 000000000..8a185757c --- /dev/null +++ b/internal/db/pg/status_test.go @@ -0,0 +1,134 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package pg_test + +import ( +	"fmt" +	"testing" +	"time" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/testrig" +) + +type StatusTestSuite struct { +	PGStandardTestSuite +} + +func (suite *StatusTestSuite) SetupSuite() { +	suite.testTokens = testrig.NewTestTokens() +	suite.testClients = testrig.NewTestClients() +	suite.testApplications = testrig.NewTestApplications() +	suite.testUsers = testrig.NewTestUsers() +	suite.testAccounts = testrig.NewTestAccounts() +	suite.testAttachments = testrig.NewTestAttachments() +	suite.testStatuses = testrig.NewTestStatuses() +	suite.testTags = testrig.NewTestTags() +	suite.testMentions = testrig.NewTestMentions() +} + +func (suite *StatusTestSuite) SetupTest() { +	suite.config = testrig.NewTestConfig() +	suite.db = testrig.NewTestDB() +	suite.log = testrig.NewTestLog() + +	testrig.StandardDBSetup(suite.db, suite.testAccounts) +} + +func (suite *StatusTestSuite) TearDownTest() { +	testrig.StandardDBTeardown(suite.db) +} + +func (suite *StatusTestSuite) TestGetStatusByID() { +	status, err := suite.db.GetStatusByID(suite.testStatuses["local_account_1_status_1"].ID) +	if err != nil { +		suite.FailNow(err.Error()) +	} +	suite.NotNil(status) +	suite.NotNil(status.Account) +	suite.NotNil(status.CreatedWithApplication) +	suite.Nil(status.BoostOf) +	suite.Nil(status.BoostOfAccount) +	suite.Nil(status.InReplyTo) +	suite.Nil(status.InReplyToAccount) +} + +func (suite *StatusTestSuite) TestGetStatusByURI() { +	status, err := suite.db.GetStatusByURI(suite.testStatuses["local_account_1_status_1"].URI) +	if err != nil { +		suite.FailNow(err.Error()) +	} +	suite.NotNil(status) +	suite.NotNil(status.Account) +	suite.NotNil(status.CreatedWithApplication) +	suite.Nil(status.BoostOf) +	suite.Nil(status.BoostOfAccount) +	suite.Nil(status.InReplyTo) +	suite.Nil(status.InReplyToAccount) +} + +func (suite *StatusTestSuite) TestGetStatusWithExtras() { +	status, err := suite.db.GetStatusByID(suite.testStatuses["admin_account_status_1"].ID) +	if err != nil { +		suite.FailNow(err.Error()) +	} +	suite.NotNil(status) +	suite.NotNil(status.Account) +	suite.NotNil(status.CreatedWithApplication) +	suite.NotEmpty(status.Tags) +	suite.NotEmpty(status.Attachments) +	suite.NotEmpty(status.Emojis) +} + +func (suite *StatusTestSuite) TestGetStatusWithMention() { +	status, err := suite.db.GetStatusByID(suite.testStatuses["local_account_2_status_5"].ID) +	if err != nil { +		suite.FailNow(err.Error()) +	} +	suite.NotNil(status) +	suite.NotNil(status.Account) +	suite.NotNil(status.CreatedWithApplication) +	suite.NotEmpty(status.Mentions) +	suite.NotEmpty(status.MentionIDs) +	suite.NotNil(status.InReplyTo) +	suite.NotNil(status.InReplyToAccount) +} + +func (suite *StatusTestSuite) TestGetStatusTwice() { +	before1 := time.Now() +	_, err := suite.db.GetStatusByURI(suite.testStatuses["local_account_1_status_1"].URI) +	suite.NoError(err) +	after1 := time.Now() +	duration1 := after1.Sub(before1) +	fmt.Println(duration1.Nanoseconds()) + +	before2 := time.Now() +	_, err = suite.db.GetStatusByURI(suite.testStatuses["local_account_1_status_1"].URI) +	suite.NoError(err) +	after2 := time.Now() +	duration2 := after2.Sub(before2) +	fmt.Println(duration2.Nanoseconds()) + +	// second retrieval should be several orders faster since it will be cached now +	suite.Less(duration2, duration1) +} + +func TestStatusTestSuite(t *testing.T) { +	suite.Run(t, new(StatusTestSuite)) +} diff --git a/internal/db/pg/statuscontext.go b/internal/db/pg/statuscontext.go deleted file mode 100644 index 2ff1a20bb..000000000 --- a/internal/db/pg/statuscontext.go +++ /dev/null @@ -1,104 +0,0 @@ -/* -   GoToSocial -   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org - -   This program is free software: you can redistribute it and/or modify -   it under the terms of the GNU Affero General Public License as published by -   the Free Software Foundation, either version 3 of the License, or -   (at your option) any later version. - -   This program is distributed in the hope that it will be useful, -   but WITHOUT ANY WARRANTY; without even the implied warranty of -   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the -   GNU Affero General Public License for more details. - -   You should have received a copy of the GNU Affero General Public License -   along with this program.  If not, see <http://www.gnu.org/licenses/>. -*/ - -package pg - -import ( -	"container/list" -	"errors" - -	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func (ps *postgresService) StatusParents(status *gtsmodel.Status, onlyDirect bool) ([]*gtsmodel.Status, error) { -	parents := []*gtsmodel.Status{} -	ps.statusParent(status, &parents, onlyDirect) - -	return parents, nil -} - -func (ps *postgresService) statusParent(status *gtsmodel.Status, foundStatuses *[]*gtsmodel.Status, onlyDirect bool) { -	if status.InReplyToID == "" { -		return -	} - -	parentStatus := >smodel.Status{} -	if err := ps.conn.Model(parentStatus).Where("id = ?", status.InReplyToID).Select(); err == nil { -		*foundStatuses = append(*foundStatuses, parentStatus) -	} - -	if onlyDirect { -		return -	} -	ps.statusParent(parentStatus, foundStatuses, false) -} - -func (ps *postgresService) StatusChildren(status *gtsmodel.Status, onlyDirect bool, minID string) ([]*gtsmodel.Status, error) { -	foundStatuses := &list.List{} -	foundStatuses.PushFront(status) -	ps.statusChildren(status, foundStatuses, onlyDirect, minID) - -	children := []*gtsmodel.Status{} -	for e := foundStatuses.Front(); e != nil; e = e.Next() { -		entry, ok := e.Value.(*gtsmodel.Status) -		if !ok { -			panic(errors.New("entry in foundStatuses was not a *gtsmodel.Status")) -		} - -		// only append children, not the overall parent status -		if entry.ID != status.ID { -			children = append(children, entry) -		} -	} - -	return children, nil -} - -func (ps *postgresService) statusChildren(status *gtsmodel.Status, foundStatuses *list.List, onlyDirect bool, minID string) { -	immediateChildren := []*gtsmodel.Status{} - -	q := ps.conn.Model(&immediateChildren).Where("in_reply_to_id = ?", status.ID) -	if minID != "" { -		q = q.Where("status.id > ?", minID) -	} - -	if err := q.Select(); err != nil { -		return -	} - -	for _, child := range immediateChildren { -	insertLoop: -		for e := foundStatuses.Front(); e != nil; e = e.Next() { -			entry, ok := e.Value.(*gtsmodel.Status) -			if !ok { -				panic(errors.New("entry in foundStatuses was not a *gtsmodel.Status")) -			} - -			if child.InReplyToAccountID != "" && entry.ID == child.InReplyToID { -				foundStatuses.InsertAfter(child, e) -				break insertLoop -			} -		} - -		// only do one loop if we only want direct children -		if onlyDirect { -			return -		} -		ps.statusChildren(child, foundStatuses, false, minID) -	} -} diff --git a/internal/db/pg/timeline.go b/internal/db/pg/timeline.go index 585ca3067..fa8b07aab 100644 --- a/internal/db/pg/timeline.go +++ b/internal/db/pg/timeline.go @@ -19,16 +19,26 @@  package pg  import ( +	"context"  	"sort"  	"github.com/go-pg/pg/v10" +	"github.com/sirupsen/logrus" +	"github.com/superseriousbusiness/gotosocial/internal/config"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  ) -func (ps *postgresService) GetHomeTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error) { +type timelineDB struct { +	config *config.Config +	conn   *pg.DB +	log    *logrus.Logger +	cancel context.CancelFunc +} + +func (t *timelineDB) GetHomeTimeline(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, db.Error) {  	statuses := []*gtsmodel.Status{} -	q := ps.conn.Model(&statuses) +	q := t.conn.Model(&statuses)  	q = q.ColumnExpr("status.*").  		// Find out who accountID follows. @@ -74,22 +84,22 @@ func (ps *postgresService) GetHomeTimelineForAccount(accountID string, maxID str  	err := q.Select()  	if err != nil {  		if err == pg.ErrNoRows { -			return nil, db.ErrNoEntries{} +			return nil, db.ErrNoEntries  		}  		return nil, err  	}  	if len(statuses) == 0 { -		return nil, db.ErrNoEntries{} +		return nil, db.ErrNoEntries  	}  	return statuses, nil  } -func (ps *postgresService) GetPublicTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error) { +func (t *timelineDB) GetPublicTimeline(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, db.Error) {  	statuses := []*gtsmodel.Status{} -	q := ps.conn.Model(&statuses). +	q := t.conn.Model(&statuses).  		Where("visibility = ?", gtsmodel.VisibilityPublic).  		Where("? IS NULL", pg.Ident("in_reply_to_id")).  		Where("? IS NULL", pg.Ident("in_reply_to_uri")). @@ -119,13 +129,13 @@ func (ps *postgresService) GetPublicTimelineForAccount(accountID string, maxID s  	err := q.Select()  	if err != nil {  		if err == pg.ErrNoRows { -			return nil, db.ErrNoEntries{} +			return nil, db.ErrNoEntries  		}  		return nil, err  	}  	if len(statuses) == 0 { -		return nil, db.ErrNoEntries{} +		return nil, db.ErrNoEntries  	}  	return statuses, nil @@ -133,11 +143,11 @@ func (ps *postgresService) GetPublicTimelineForAccount(accountID string, maxID s  // TODO optimize this query and the logic here, because it's slow as balls -- it takes like a literal second to return with a limit of 20!  // It might be worth serving it through a timeline instead of raw DB queries, like we do for Home feeds. -func (ps *postgresService) GetFavedTimelineForAccount(accountID string, maxID string, minID string, limit int) ([]*gtsmodel.Status, string, string, error) { +func (t *timelineDB) GetFavedTimeline(accountID string, maxID string, minID string, limit int) ([]*gtsmodel.Status, string, string, db.Error) {  	faves := []*gtsmodel.StatusFave{} -	fq := ps.conn.Model(&faves). +	fq := t.conn.Model(&faves).  		Where("account_id = ?", accountID).  		Order("id DESC") @@ -156,13 +166,13 @@ func (ps *postgresService) GetFavedTimelineForAccount(accountID string, maxID st  	err := fq.Select()  	if err != nil {  		if err == pg.ErrNoRows { -			return nil, "", "", db.ErrNoEntries{} +			return nil, "", "", db.ErrNoEntries  		}  		return nil, "", "", err  	}  	if len(faves) == 0 { -		return nil, "", "", db.ErrNoEntries{} +		return nil, "", "", db.ErrNoEntries  	}  	// map[statusID]faveID -- we need this to sort statuses by fave ID rather than their own ID @@ -175,16 +185,16 @@ func (ps *postgresService) GetFavedTimelineForAccount(accountID string, maxID st  	}  	statuses := []*gtsmodel.Status{} -	err = ps.conn.Model(&statuses).Where("id IN (?)", pg.In(in)).Select() +	err = t.conn.Model(&statuses).Where("id IN (?)", pg.In(in)).Select()  	if err != nil {  		if err == pg.ErrNoRows { -			return nil, "", "", db.ErrNoEntries{} +			return nil, "", "", db.ErrNoEntries  		}  		return nil, "", "", err  	}  	if len(statuses) == 0 { -		return nil, "", "", db.ErrNoEntries{} +		return nil, "", "", db.ErrNoEntries  	}  	// arrange statuses by fave ID diff --git a/internal/db/pg/update.go b/internal/db/pg/update.go deleted file mode 100644 index f6bc70ad9..000000000 --- a/internal/db/pg/update.go +++ /dev/null @@ -1,73 +0,0 @@ -/* -   GoToSocial -   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org - -   This program is free software: you can redistribute it and/or modify -   it under the terms of the GNU Affero General Public License as published by -   the Free Software Foundation, either version 3 of the License, or -   (at your option) any later version. - -   This program is distributed in the hope that it will be useful, -   but WITHOUT ANY WARRANTY; without even the implied warranty of -   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the -   GNU Affero General Public License for more details. - -   You should have received a copy of the GNU Affero General Public License -   along with this program.  If not, see <http://www.gnu.org/licenses/>. -*/ - -package pg - -import ( -	"fmt" - -	"github.com/go-pg/pg/v10" -	"github.com/superseriousbusiness/gotosocial/internal/db" -) - -func (ps *postgresService) Upsert(i interface{}, conflictColumn string) error { -	if _, err := ps.conn.Model(i).OnConflict(fmt.Sprintf("(%s) DO UPDATE", conflictColumn)).Insert(); err != nil { -		if err == pg.ErrNoRows { -			return db.ErrNoEntries{} -		} -		return err -	} -	return nil -} - -func (ps *postgresService) UpdateByID(id string, i interface{}) error { -	if _, err := ps.conn.Model(i).Where("id = ?", id).OnConflict("(id) DO UPDATE").Insert(); err != nil { -		if err == pg.ErrNoRows { -			return db.ErrNoEntries{} -		} -		return err -	} -	return nil -} - -func (ps *postgresService) UpdateOneByID(id string, key string, value interface{}, i interface{}) error { -	_, err := ps.conn.Model(i).Set("? = ?", pg.Safe(key), value).Where("id = ?", id).Update() -	return err -} - -func (ps *postgresService) UpdateWhere(where []db.Where, key string, value interface{}, i interface{}) error { -	q := ps.conn.Model(i) - -	for _, w := range where { -		if w.Value == nil { -			q = q.Where("? IS NULL", pg.Ident(w.Key)) -		} else { -			if w.CaseInsensitive { -				q = q.Where("LOWER(?) = LOWER(?)", pg.Safe(w.Key), w.Value) -			} else { -				q = q.Where("? = ?", pg.Safe(w.Key), w.Value) -			} -		} -	} - -	q = q.Set("? = ?", pg.Safe(key), value) - -	_, err := q.Update() - -	return err -} diff --git a/internal/db/pg/util.go b/internal/db/pg/util.go new file mode 100644 index 000000000..17c09b720 --- /dev/null +++ b/internal/db/pg/util.go @@ -0,0 +1,25 @@ +package pg + +import ( +	"strings" + +	"github.com/go-pg/pg/v10" +	"github.com/superseriousbusiness/gotosocial/internal/db" +) + +// processErrorResponse parses the given error and returns an appropriate DBError. +func processErrorResponse(err error) db.Error { +	switch err { +	case nil: +		return nil +	case pg.ErrNoRows: +		return db.ErrNoEntries +	case pg.ErrMultiRows: +		return db.ErrMultipleEntries +	default: +		if strings.Contains(err.Error(), "duplicate key value violates unique constraint") { +			return db.ErrAlreadyExists +		} +		return err +	} +} diff --git a/internal/db/relationship.go b/internal/db/relationship.go new file mode 100644 index 000000000..85f64d72b --- /dev/null +++ b/internal/db/relationship.go @@ -0,0 +1,71 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package db + +import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + +// Relationship contains functions for getting or modifying the relationship between two accounts. +type Relationship interface { +	// IsBlocked checks whether account 1 has a block in place against block2. +	// If eitherDirection is true, then the function returns true if account1 blocks account2, OR if account2 blocks account1. +	IsBlocked(account1 string, account2 string, eitherDirection bool) (bool, Error) + +	// GetBlock returns the block from account1 targeting account2, if it exists, or an error if it doesn't. +	// +	// Because this is slower than Blocked, only use it if you need the actual Block struct for some reason, +	// not if you're just checking for the existence of a block. +	GetBlock(account1 string, account2 string) (*gtsmodel.Block, Error) + +	// GetRelationship retrieves the relationship of the targetAccount to the requestingAccount. +	GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, Error) + +	// IsFollowing returns true if sourceAccount follows target account, or an error if something goes wrong while finding out. +	IsFollowing(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, Error) + +	// IsFollowRequested returns true if sourceAccount has requested to follow target account, or an error if something goes wrong while finding out. +	IsFollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, Error) + +	// IsMutualFollowing returns true if account1 and account2 both follow each other, or an error if something goes wrong while finding out. +	IsMutualFollowing(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, Error) + +	// AcceptFollowRequest moves a follow request in the database from the follow_requests table to the follows table. +	// In other words, it should create the follow, and delete the existing follow request. +	// +	// It will return the newly created follow for further processing. +	AcceptFollowRequest(originAccountID string, targetAccountID string) (*gtsmodel.Follow, Error) + +	// GetAccountFollowRequests returns all follow requests targeting the given account. +	GetAccountFollowRequests(accountID string) ([]*gtsmodel.FollowRequest, Error) + +	// GetAccountFollows returns a slice of follows owned by the given accountID. +	GetAccountFollows(accountID string) ([]*gtsmodel.Follow, Error) + +	// CountAccountFollows returns the amount of accounts that the given accountID is following. +	// +	// If localOnly is set to true, then only follows from *this instance* will be returned. +	CountAccountFollows(accountID string, localOnly bool) (int, Error) + +	// GetAccountFollowedBy fetches follows that target given accountID. +	// +	// If localOnly is set to true, then only follows from *this instance* will be returned. +	GetAccountFollowedBy(accountID string, localOnly bool) ([]*gtsmodel.Follow, Error) + +	// CountAccountFollowedBy returns the amounts that the given ID is followed by. +	CountAccountFollowedBy(accountID string, localOnly bool) (int, Error) +} diff --git a/internal/db/status.go b/internal/db/status.go new file mode 100644 index 000000000..9d206c198 --- /dev/null +++ b/internal/db/status.go @@ -0,0 +1,75 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package db + +import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + +// Status contains functions for getting statuses, creating statuses, and checking various other fields on statuses. +type Status interface { +	// GetStatusByID returns one status from the database, with all rel fields populated (if possible). +	GetStatusByID(id string) (*gtsmodel.Status, Error) + +	// GetStatusByURI returns one status from the database, with all rel fields populated (if possible). +	GetStatusByURI(uri string) (*gtsmodel.Status, Error) + +	// GetStatusByURL returns one status from the database, with all rel fields populated (if possible). +	GetStatusByURL(uri string) (*gtsmodel.Status, Error) + +	// PutStatus stores one status in the database. +	PutStatus(status *gtsmodel.Status) Error + +	// CountStatusReplies returns the amount of replies recorded for a status, or an error if something goes wrong +	CountStatusReplies(status *gtsmodel.Status) (int, Error) + +	// CountStatusReblogs returns the amount of reblogs/boosts recorded for a status, or an error if something goes wrong +	CountStatusReblogs(status *gtsmodel.Status) (int, Error) + +	// CountStatusFaves returns the amount of faves/likes recorded for a status, or an error if something goes wrong +	CountStatusFaves(status *gtsmodel.Status) (int, Error) + +	// GetStatusParents gets the parent statuses of a given status. +	// +	// If onlyDirect is true, only the immediate parent will be returned. +	GetStatusParents(status *gtsmodel.Status, onlyDirect bool) ([]*gtsmodel.Status, Error) + +	// GetStatusChildren gets the child statuses of a given status. +	// +	// If onlyDirect is true, only the immediate children will be returned. +	GetStatusChildren(status *gtsmodel.Status, onlyDirect bool, minID string) ([]*gtsmodel.Status, Error) + +	// IsStatusFavedBy checks if a given status has been faved by a given account ID +	IsStatusFavedBy(status *gtsmodel.Status, accountID string) (bool, Error) + +	// IsStatusRebloggedBy checks if a given status has been reblogged/boosted by a given account ID +	IsStatusRebloggedBy(status *gtsmodel.Status, accountID string) (bool, Error) + +	// IsStatusMutedBy checks if a given status has been muted by a given account ID +	IsStatusMutedBy(status *gtsmodel.Status, accountID string) (bool, Error) + +	// IsStatusBookmarkedBy checks if a given status has been bookmarked by a given account ID +	IsStatusBookmarkedBy(status *gtsmodel.Status, accountID string) (bool, Error) + +	// GetStatusFaves returns a slice of faves/likes of the given status. +	// This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user. +	GetStatusFaves(status *gtsmodel.Status) ([]*gtsmodel.StatusFave, Error) + +	// GetStatusReblogs returns a slice of statuses that are a boost/reblog of the given status. +	// This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user. +	GetStatusReblogs(status *gtsmodel.Status) ([]*gtsmodel.Status, Error) +} diff --git a/internal/db/timeline.go b/internal/db/timeline.go new file mode 100644 index 000000000..74aa5c781 --- /dev/null +++ b/internal/db/timeline.go @@ -0,0 +1,44 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package db + +import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + +// Timeline contains functionality for retrieving home/public/faved etc timelines for an account. +type Timeline interface { +	// GetHomeTimeline returns a slice of statuses from accounts that are followed by the given account id. +	// +	// Statuses should be returned in descending order of when they were created (newest first). +	GetHomeTimeline(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, Error) + +	// GetPublicTimeline fetches the account's PUBLIC timeline -- ie., posts and replies that are public. +	// It will use the given filters and try to return as many statuses as possible up to the limit. +	// +	// Statuses should be returned in descending order of when they were created (newest first). +	GetPublicTimeline(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, Error) + +	// GetFavedTimeline fetches the account's FAVED timeline -- ie., posts and replies that the requesting account has faved. +	// It will use the given filters and try to return as many statuses as possible up to the limit. +	// +	// Note that unlike the other GetTimeline functions, the returned statuses will be arranged by their FAVE id, not the STATUS id. +	// In other words, they'll be returned in descending order of when they were faved by the requesting user, not when they were created. +	// +	// Also note the extra return values, which correspond to the nextMaxID and prevMinID for building Link headers. +	GetFavedTimeline(accountID string, maxID string, minID string, limit int) ([]*gtsmodel.Status, string, string, Error) +} diff --git a/internal/federation/dereference.go b/internal/federation/dereference.go index 07901d5b1..96a662e32 100644 --- a/internal/federation/dereference.go +++ b/internal/federation/dereference.go @@ -1,3 +1,21 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ +  package federation  import ( diff --git a/internal/federation/dereferencing/account.go b/internal/federation/dereferencing/account.go index 72d2e44d7..ba6766061 100644 --- a/internal/federation/dereferencing/account.go +++ b/internal/federation/dereferencing/account.go @@ -29,7 +29,6 @@ import (  	"github.com/go-fed/activity/streams/vocab"  	"github.com/sirupsen/logrus"  	"github.com/superseriousbusiness/gotosocial/internal/ap" -	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/id"  	"github.com/superseriousbusiness/gotosocial/internal/transport" @@ -65,8 +64,8 @@ func (d *deref) GetRemoteAccount(username string, remoteAccountID *url.URL, refr  	new := true  	// check if we already have the account in our db -	maybeAccount := >smodel.Account{} -	if err := d.db.GetWhere([]db.Where{{Key: "uri", Value: remoteAccountID.String()}}, maybeAccount); err == nil { +	maybeAccount, err := d.db.GetAccountByURI(remoteAccountID.String()) +	if err == nil {  		// we've seen this account before so it's not new  		new = false  		if !refresh { diff --git a/internal/federation/dereferencing/announce.go b/internal/federation/dereferencing/announce.go index 2522a4034..6773db425 100644 --- a/internal/federation/dereferencing/announce.go +++ b/internal/federation/dereferencing/announce.go @@ -27,14 +27,14 @@ import (  )  func (d *deref) DereferenceAnnounce(announce *gtsmodel.Status, requestingUsername string) error { -	if announce.GTSBoostedStatus == nil || announce.GTSBoostedStatus.URI == "" { +	if announce.BoostOf == nil || announce.BoostOf.URI == "" {  		// we can't do anything unfortunately  		return errors.New("DereferenceAnnounce: no URI to dereference")  	} -	boostedStatusURI, err := url.Parse(announce.GTSBoostedStatus.URI) +	boostedStatusURI, err := url.Parse(announce.BoostOf.URI)  	if err != nil { -		return fmt.Errorf("DereferenceAnnounce: couldn't parse boosted status URI %s: %s", announce.GTSBoostedStatus.URI, err) +		return fmt.Errorf("DereferenceAnnounce: couldn't parse boosted status URI %s: %s", announce.BoostOf.URI, err)  	}  	if blocked, err := d.blockedDomain(boostedStatusURI.Host); blocked || err != nil {  		return fmt.Errorf("DereferenceAnnounce: domain %s is blocked", boostedStatusURI.Host) @@ -47,7 +47,7 @@ func (d *deref) DereferenceAnnounce(announce *gtsmodel.Status, requestingUsernam  	boostedStatus, _, _, err := d.GetRemoteStatus(requestingUsername, boostedStatusURI, false)  	if err != nil { -		return fmt.Errorf("DereferenceAnnounce: error dereferencing remote status with id %s: %s", announce.GTSBoostedStatus.URI, err) +		return fmt.Errorf("DereferenceAnnounce: error dereferencing remote status with id %s: %s", announce.BoostOf.URI, err)  	}  	announce.Content = boostedStatus.Content @@ -60,6 +60,6 @@ func (d *deref) DereferenceAnnounce(announce *gtsmodel.Status, requestingUsernam  	announce.BoostOfAccountID = boostedStatus.AccountID  	announce.Visibility = boostedStatus.Visibility  	announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced -	announce.GTSBoostedStatus = boostedStatus +	announce.BoostOf = boostedStatus  	return nil  } diff --git a/internal/federation/dereferencing/blocked.go b/internal/federation/dereferencing/blocked.go index a66afbb60..c8a4c6ade 100644 --- a/internal/federation/dereferencing/blocked.go +++ b/internal/federation/dereferencing/blocked.go @@ -31,7 +31,7 @@ func (d *deref) blockedDomain(host string) (bool, error) {  		return true, nil  	} -	if _, ok := err.(db.ErrNoEntries); ok { +	if err == db.ErrNoEntries {  		// there are no entries so there's no block  		return false, nil  	} diff --git a/internal/federation/dereferencing/status.go b/internal/federation/dereferencing/status.go index b05f6e72c..68693c021 100644 --- a/internal/federation/dereferencing/status.go +++ b/internal/federation/dereferencing/status.go @@ -66,8 +66,8 @@ func (d *deref) GetRemoteStatus(username string, remoteStatusID *url.URL, refres  	new := true  	// check if we already have the status in our db -	maybeStatus := >smodel.Status{} -	if err := d.db.GetWhere([]db.Where{{Key: "uri", Value: remoteStatusID.String()}}, maybeStatus); err == nil { +	maybeStatus, err := d.db.GetStatusByURI(remoteStatusID.String()) +	if err == nil {  		// we've seen this status before so it's not new  		new = false @@ -109,7 +109,7 @@ func (d *deref) GetRemoteStatus(username string, remoteStatusID *url.URL, refres  			return nil, statusable, new, fmt.Errorf("GetRemoteStatus: error populating status fields: %s", err)  		} -		if err := d.db.Put(gtsStatus); err != nil { +		if err := d.db.PutStatus(gtsStatus); err != nil {  			return nil, statusable, new, fmt.Errorf("GetRemoteStatus: error putting new status: %s", err)  		}  	} else { @@ -276,7 +276,7 @@ func (d *deref) populateStatusFields(status *gtsmodel.Status, requestingUsername  	// * the remote URL (a.RemoteURL)  	// This should be enough to pass along to the media processor.  	attachmentIDs := []string{} -	for _, a := range status.GTSMediaAttachments { +	for _, a := range status.Attachments {  		l.Tracef("dereferencing attachment: %+v", a)  		// it might have been processed elsewhere so check first if it's already in the database or not @@ -288,7 +288,7 @@ func (d *deref) populateStatusFields(status *gtsmodel.Status, requestingUsername  			attachmentIDs = append(attachmentIDs, maybeAttachment.ID)  			continue  		} -		if _, ok := err.(db.ErrNoEntries); !ok { +		if err != db.ErrNoEntries {  			// we have a real error  			return fmt.Errorf("error checking db for existence of attachment with remote url %s: %s", a.RemoteURL, err)  		} @@ -307,7 +307,7 @@ func (d *deref) populateStatusFields(status *gtsmodel.Status, requestingUsername  		}  		attachmentIDs = append(attachmentIDs, deferencedAttachment.ID)  	} -	status.Attachments = attachmentIDs +	status.AttachmentIDs = attachmentIDs  	// 2. Hashtags @@ -317,53 +317,84 @@ func (d *deref) populateStatusFields(status *gtsmodel.Status, requestingUsername  	// At this point, mentions should have the namestring and mentionedAccountURI set on them.  	//  	// We should dereference any accounts mentioned here which we don't have in our db yet, by their URI. -	mentions := []string{} -	for _, m := range status.GTSMentions { - +	mentionIDs := []string{} +	for _, m := range status.Mentions {  		if m.ID != "" { -			continue  			// we've already populated this mention, since it has an ID +			l.Debug("mention already populated") +			continue  		} -		mID, err := id.NewRandomULID() -		if err != nil { -			return err +		if m.TargetAccountURI == "" { +			// can't do anything with this mention +			l.Debug("target URI not set on mention") +			continue  		} -		m.ID = mID -		uri, err := url.Parse(m.MentionedAccountURI) +		targetAccountURI, err := url.Parse(m.TargetAccountURI)  		if err != nil { -			l.Debugf("error parsing mentioned account uri %s: %s", m.MentionedAccountURI, err) +			l.Debugf("error parsing mentioned account uri %s: %s", m.TargetAccountURI, err)  			continue  		} -		m.StatusID = status.ID -		m.OriginAccountID = status.GTSAuthorAccount.ID -		m.OriginAccountURI = status.GTSAuthorAccount.URI +		var targetAccount *gtsmodel.Account +		if a, err := d.db.GetAccountByURL(targetAccountURI.String()); err == nil { +			targetAccount = a +		} else if a, _, err := d.GetRemoteAccount(requestingUsername, targetAccountURI, false); err == nil { +			targetAccount = a +		} else { +			// we can't find the target account so bail +			l.Debug("can't retrieve account targeted by mention") +			continue +		} -		targetAccount, _, err := d.GetRemoteAccount(requestingUsername, uri, false) +		mID, err := id.NewRandomULID()  		if err != nil { -			continue +			return err +		} + +		m = >smodel.Mention{ +			ID:               mID, +			StatusID:         status.ID, +			Status:           m.Status, +			CreatedAt:        status.CreatedAt, +			UpdatedAt:        status.UpdatedAt, +			OriginAccountID:  status.Account.ID, +			OriginAccountURI: status.AccountURI, +			OriginAccount:    status.Account, +			TargetAccountID:  targetAccount.ID, +			TargetAccount:    targetAccount, +			NameString:       m.NameString, +			TargetAccountURI: targetAccount.URI, +			TargetAccountURL: targetAccount.URL,  		} -		// by this point, we know the targetAccount exists in our database with an ID :) -		m.TargetAccountID = targetAccount.ID  		if err := d.db.Put(m); err != nil {  			return fmt.Errorf("error creating mention: %s", err)  		} -		mentions = append(mentions, m.ID) +		mentionIDs = append(mentionIDs, m.ID)  	} -	status.Mentions = mentions +	status.MentionIDs = mentionIDs  	// status has replyToURI but we don't have an ID yet for the status it replies to  	if status.InReplyToURI != "" && status.InReplyToID == "" { -		replyToStatus := >smodel.Status{} -		if err := d.db.GetWhere([]db.Where{{Key: "uri", Value: status.InReplyToURI}}, replyToStatus); err == nil { +		statusURI, err := url.Parse(status.InReplyToURI) +		if err != nil { +			return err +		} +		if replyToStatus, err := d.db.GetStatusByURI(status.InReplyToURI); err == nil {  			// we have the status  			status.InReplyToID = replyToStatus.ID +			status.InReplyTo = replyToStatus  			status.InReplyToAccountID = replyToStatus.AccountID +			status.InReplyToAccount = replyToStatus.Account +		} else if replyToStatus, _, _, err := d.GetRemoteStatus(requestingUsername, statusURI, false); err == nil { +			// we got the status +			status.InReplyToID = replyToStatus.ID +			status.InReplyTo = replyToStatus +			status.InReplyToAccountID = replyToStatus.AccountID +			status.InReplyToAccount = replyToStatus.Account  		}  	} -  	return nil  } diff --git a/internal/federation/federatingdb/accept.go b/internal/federation/federatingdb/accept.go index 4d11ea62a..91d9df86f 100644 --- a/internal/federation/federatingdb/accept.go +++ b/internal/federation/federatingdb/accept.go @@ -1,3 +1,21 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ +  package federatingdb  import ( diff --git a/internal/federation/federatingdb/announce.go b/internal/federation/federatingdb/announce.go index a70c0c3a6..981eaf1ef 100644 --- a/internal/federation/federatingdb/announce.go +++ b/internal/federation/federatingdb/announce.go @@ -1,3 +1,21 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ +  package federatingdb  import ( diff --git a/internal/federation/federatingdb/create.go b/internal/federation/federatingdb/create.go index 2ac4890e8..fb4353cd4 100644 --- a/internal/federation/federatingdb/create.go +++ b/internal/federation/federatingdb/create.go @@ -112,8 +112,8 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {  				}  				status.ID = statusID -				if err := f.db.Put(status); err != nil { -					if _, ok := err.(db.ErrAlreadyExists); ok { +				if err := f.db.PutStatus(status); err != nil { +					if err == db.ErrAlreadyExists {  						// the status already exists in the database, which means we've already handled everything else,  						// so we can just return nil here and be done with it.  						return nil diff --git a/internal/federation/federatingdb/delete.go b/internal/federation/federatingdb/delete.go index 02ce43620..ee9310789 100644 --- a/internal/federation/federatingdb/delete.go +++ b/internal/federation/federatingdb/delete.go @@ -1,3 +1,21 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ +  package federatingdb  import ( @@ -6,7 +24,6 @@ import (  	"net/url"  	"github.com/sirupsen/logrus" -	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/util"  ) @@ -52,10 +69,8 @@ func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error {  	// in a delete we only get the URI, we can't know if we have a status or a profile or something else,  	// so we have to try a few different things... -	where := []db.Where{{Key: "uri", Value: id.String()}} - -	s := >smodel.Status{} -	if err := f.db.GetWhere(where, s); err == nil { +	s, err := f.db.GetStatusByURI(id.String()) +	if err == nil {  		// it's a status  		l.Debugf("uri is for status with id: %s", s.ID)  		if err := f.db.DeleteByID(s.ID, >smodel.Status{}); err != nil { @@ -69,8 +84,8 @@ func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error {  		}  	} -	a := >smodel.Account{} -	if err := f.db.GetWhere(where, a); err == nil { +	a, err := f.db.GetAccountByURI(id.String()) +	if err == nil {  		// it's an account  		l.Debugf("uri is for an account with id: %s", s.ID)  		if err := f.db.DeleteByID(a.ID, >smodel.Account{}); err != nil { diff --git a/internal/federation/federatingdb/exists.go b/internal/federation/federatingdb/exists.go index b5c10b895..0e13c1196 100644 --- a/internal/federation/federatingdb/exists.go +++ b/internal/federation/federatingdb/exists.go @@ -1,3 +1,21 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ +  package federatingdb  import ( diff --git a/internal/federation/federatingdb/followers.go b/internal/federation/federatingdb/followers.go index 1f111dd34..241362fc1 100644 --- a/internal/federation/federatingdb/followers.go +++ b/internal/federation/federatingdb/followers.go @@ -31,7 +31,8 @@ func (f *federatingDB) Followers(c context.Context, actorIRI *url.URL) (follower  	acct := >smodel.Account{}  	if util.IsUserPath(actorIRI) { -		if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: actorIRI.String()}}, acct); err != nil { +		acct, err = f.db.GetAccountByURI(actorIRI.String()) +		if err != nil {  			return nil, fmt.Errorf("FOLLOWERS: db error getting account with uri %s: %s", actorIRI.String(), err)  		}  	} else if util.IsFollowersPath(actorIRI) { @@ -42,8 +43,8 @@ func (f *federatingDB) Followers(c context.Context, actorIRI *url.URL) (follower  		return nil, fmt.Errorf("FOLLOWERS: could not parse actor IRI %s as users or followers path", actorIRI.String())  	} -	acctFollowers := []gtsmodel.Follow{} -	if err := f.db.GetFollowersByAccountID(acct.ID, &acctFollowers, false); err != nil { +	acctFollowers, err := f.db.GetAccountFollowedBy(acct.ID, false) +	if err != nil {  		return nil, fmt.Errorf("FOLLOWERS: db error getting followers for account id %s: %s", acct.ID, err)  	} diff --git a/internal/federation/federatingdb/following.go b/internal/federation/federatingdb/following.go index f92041e1e..45785c671 100644 --- a/internal/federation/federatingdb/following.go +++ b/internal/federation/federatingdb/following.go @@ -8,7 +8,6 @@ import (  	"github.com/go-fed/activity/streams"  	"github.com/go-fed/activity/streams/vocab"  	"github.com/sirupsen/logrus" -	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/util"  ) @@ -28,21 +27,37 @@ func (f *federatingDB) Following(c context.Context, actorIRI *url.URL) (followin  	)  	l.Debugf("entering FOLLOWING function with actorIRI %s", actorIRI.String()) -	acct := >smodel.Account{} +	var acct *gtsmodel.Account  	if util.IsUserPath(actorIRI) { -		if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: actorIRI.String()}}, acct); err != nil { +		username, err := util.ParseUserPath(actorIRI) +		if err != nil { +			return nil, fmt.Errorf("FOLLOWING: error parsing user path: %s", err) +		} + +		a, err := f.db.GetLocalAccountByUsername(username) +		if err != nil {  			return nil, fmt.Errorf("FOLLOWING: db error getting account with uri %s: %s", actorIRI.String(), err)  		} + +		acct = a  	} else if util.IsFollowingPath(actorIRI) { -		if err := f.db.GetWhere([]db.Where{{Key: "following_uri", Value: actorIRI.String()}}, acct); err != nil { +		username, err := util.ParseFollowingPath(actorIRI) +		if err != nil { +			return nil, fmt.Errorf("FOLLOWING: error parsing following path: %s", err) +		} + +		a, err := f.db.GetLocalAccountByUsername(username) +		if err != nil {  			return nil, fmt.Errorf("FOLLOWING: db error getting account with following uri %s: %s", actorIRI.String(), err)  		} + +		acct = a  	} else {  		return nil, fmt.Errorf("FOLLOWING: could not parse actor IRI %s as users or following path", actorIRI.String())  	} -	acctFollowing := []gtsmodel.Follow{} -	if err := f.db.GetFollowingByAccountID(acct.ID, &acctFollowing); err != nil { +	acctFollowing, err := f.db.GetAccountFollows(acct.ID) +	if err != nil {  		return nil, fmt.Errorf("FOLLOWING: db error getting following for account id %s: %s", acct.ID, err)  	} diff --git a/internal/federation/federatingdb/get.go b/internal/federation/federatingdb/get.go index 77a24bf43..0265080f9 100644 --- a/internal/federation/federatingdb/get.go +++ b/internal/federation/federatingdb/get.go @@ -43,8 +43,8 @@ func (f *federatingDB) Get(c context.Context, id *url.URL) (value vocab.Type, er  	l.Debug("entering GET function")  	if util.IsUserPath(id) { -		acct := >smodel.Account{} -		if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: id.String()}}, acct); err != nil { +		acct, err := f.db.GetAccountByURI(id.String()) +		if err != nil {  			return nil, err  		}  		l.Debug("is user path! returning account") diff --git a/internal/federation/federatingdb/inbox.go b/internal/federation/federatingdb/inbox.go index 25ef2cda5..4390a8b4b 100644 --- a/internal/federation/federatingdb/inbox.go +++ b/internal/federation/federatingdb/inbox.go @@ -1,3 +1,21 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ +  package federatingdb  import ( diff --git a/internal/federation/federatingdb/liked.go b/internal/federation/federatingdb/liked.go index 5645d6f1e..b85398fef 100644 --- a/internal/federation/federatingdb/liked.go +++ b/internal/federation/federatingdb/liked.go @@ -1,3 +1,21 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ +  package federatingdb  import ( diff --git a/internal/federation/federatingdb/outbox.go b/internal/federation/federatingdb/outbox.go index 1568e0017..849014432 100644 --- a/internal/federation/federatingdb/outbox.go +++ b/internal/federation/federatingdb/outbox.go @@ -1,3 +1,21 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ +  package federatingdb  import ( @@ -62,7 +80,7 @@ func (f *federatingDB) OutboxForInbox(c context.Context, inboxIRI *url.URL) (out  	}  	acct := >smodel.Account{}  	if err := f.db.GetWhere([]db.Where{{Key: "inbox_uri", Value: inboxIRI.String()}}, acct); err != nil { -		if _, ok := err.(db.ErrNoEntries); ok { +		if err == db.ErrNoEntries {  			return nil, fmt.Errorf("no actor found that corresponds to inbox %s", inboxIRI.String())  		}  		return nil, fmt.Errorf("db error searching for actor with inbox %s", inboxIRI.String()) diff --git a/internal/federation/federatingdb/owns.go b/internal/federation/federatingdb/owns.go index 51b20151a..0a65397ff 100644 --- a/internal/federation/federatingdb/owns.go +++ b/internal/federation/federatingdb/owns.go @@ -54,16 +54,16 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {  		if err != nil {  			return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err)  		} -		if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uid}}, >smodel.Status{}); err != nil { -			if _, ok := err.(db.ErrNoEntries); ok { +		status, err := f.db.GetStatusByURI(uid) +		if err != nil { +			if err == db.ErrNoEntries {  				// there are no entries for this status  				return false, nil  			}  			// an actual error happened  			return false, fmt.Errorf("database error fetching status with id %s: %s", uid, err)  		} -		l.Debugf("we own url %s", id.String()) -		return true, nil +		return status.Local, nil  	}  	if util.IsUserPath(id) { @@ -71,8 +71,8 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {  		if err != nil {  			return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err)  		} -		if err := f.db.GetLocalAccountByUsername(username, >smodel.Account{}); err != nil { -			if _, ok := err.(db.ErrNoEntries); ok { +		if _, err := f.db.GetLocalAccountByUsername(username); err != nil { +			if err == db.ErrNoEntries {  				// there are no entries for this username  				return false, nil  			} @@ -88,8 +88,8 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {  		if err != nil {  			return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err)  		} -		if err := f.db.GetLocalAccountByUsername(username, >smodel.Account{}); err != nil { -			if _, ok := err.(db.ErrNoEntries); ok { +		if _, err := f.db.GetLocalAccountByUsername(username); err != nil { +			if err == db.ErrNoEntries {  				// there are no entries for this username  				return false, nil  			} @@ -105,8 +105,8 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {  		if err != nil {  			return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err)  		} -		if err := f.db.GetLocalAccountByUsername(username, >smodel.Account{}); err != nil { -			if _, ok := err.(db.ErrNoEntries); ok { +		if _, err := f.db.GetLocalAccountByUsername(username); err != nil { +			if err == db.ErrNoEntries {  				// there are no entries for this username  				return false, nil  			} @@ -122,8 +122,8 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {  		if err != nil {  			return false, fmt.Errorf("error parsing like path for url %s: %s", id.String(), err)  		} -		if err := f.db.GetLocalAccountByUsername(username, >smodel.Account{}); err != nil { -			if _, ok := err.(db.ErrNoEntries); ok { +		if _, err := f.db.GetLocalAccountByUsername(username); err != nil { +			if err == db.ErrNoEntries {  				// there are no entries for this username  				return false, nil  			} @@ -131,7 +131,7 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {  			return false, fmt.Errorf("database error fetching account with username %s: %s", username, err)  		}  		if err := f.db.GetByID(likeID, >smodel.StatusFave{}); err != nil { -			if _, ok := err.(db.ErrNoEntries); ok { +			if err == db.ErrNoEntries {  				// there are no entries  				return false, nil  			} @@ -147,8 +147,8 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {  		if err != nil {  			return false, fmt.Errorf("error parsing block path for url %s: %s", id.String(), err)  		} -		if err := f.db.GetLocalAccountByUsername(username, >smodel.Account{}); err != nil { -			if _, ok := err.(db.ErrNoEntries); ok { +		if _, err := f.db.GetLocalAccountByUsername(username); err != nil { +			if err == db.ErrNoEntries {  				// there are no entries for this username  				return false, nil  			} @@ -156,7 +156,7 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {  			return false, fmt.Errorf("database error fetching account with username %s: %s", username, err)  		}  		if err := f.db.GetByID(blockID, >smodel.Block{}); err != nil { -			if _, ok := err.(db.ErrNoEntries); ok { +			if err == db.ErrNoEntries {  				// there are no entries  				return false, nil  			} diff --git a/internal/federation/federatingdb/undo.go b/internal/federation/federatingdb/undo.go index dd82e7bac..c527833b4 100644 --- a/internal/federation/federatingdb/undo.go +++ b/internal/federation/federatingdb/undo.go @@ -1,3 +1,21 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ +  package federatingdb  import ( diff --git a/internal/federation/federatingdb/update.go b/internal/federation/federatingdb/update.go index 3f4e3e413..88ffc23b4 100644 --- a/internal/federation/federatingdb/update.go +++ b/internal/federation/federatingdb/update.go @@ -1,3 +1,21 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ +  package federatingdb  import ( diff --git a/internal/federation/federatingdb/util.go b/internal/federation/federatingdb/util.go index 28f4c5a21..eac70d85c 100644 --- a/internal/federation/federatingdb/util.go +++ b/internal/federation/federatingdb/util.go @@ -97,8 +97,8 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (idURL *url.URL, e  			for iter := actorProp.Begin(); iter != actorProp.End(); iter = iter.Next() {  				// take the IRI of the first actor we can find (there should only be one)  				if iter.IsIRI() { -					actorAccount := >smodel.Account{} -					if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: iter.GetIRI().String()}}, actorAccount); err == nil { // if there's an error here, just use the fallback behavior -- we don't need to return an error here +					// if there's an error here, just use the fallback behavior -- we don't need to return an error here +					if actorAccount, err := f.db.GetAccountByURI(iter.GetIRI().String()); err == nil {  						newID, err := id.NewRandomULID()  						if err != nil {  							return nil, err @@ -213,7 +213,7 @@ func (f *federatingDB) ActorForOutbox(c context.Context, outboxIRI *url.URL) (ac  	}  	acct := >smodel.Account{}  	if err := f.db.GetWhere([]db.Where{{Key: "outbox_uri", Value: outboxIRI.String()}}, acct); err != nil { -		if _, ok := err.(db.ErrNoEntries); ok { +		if err == db.ErrNoEntries {  			return nil, fmt.Errorf("no actor found that corresponds to outbox %s", outboxIRI.String())  		}  		return nil, fmt.Errorf("db error searching for actor with outbox %s", outboxIRI.String()) @@ -238,7 +238,7 @@ func (f *federatingDB) ActorForInbox(c context.Context, inboxIRI *url.URL) (acto  	}  	acct := >smodel.Account{}  	if err := f.db.GetWhere([]db.Where{{Key: "inbox_uri", Value: inboxIRI.String()}}, acct); err != nil { -		if _, ok := err.(db.ErrNoEntries); ok { +		if err == db.ErrNoEntries {  			return nil, fmt.Errorf("no actor found that corresponds to inbox %s", inboxIRI.String())  		}  		return nil, fmt.Errorf("db error searching for actor with inbox %s", inboxIRI.String()) diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go index 9e21b43bf..5da68afd3 100644 --- a/internal/federation/federatingprotocol.go +++ b/internal/federation/federatingprotocol.go @@ -113,8 +113,8 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr  		return nil, false, errors.New("username was empty")  	} -	requestedAccount := >smodel.Account{} -	if err := f.db.GetLocalAccountByUsername(username, requestedAccount); err != nil { +	requestedAccount, err := f.db.GetLocalAccountByUsername(username) +	if err != nil {  		return nil, false, fmt.Errorf("could not fetch requested account with username %s: %s", username, err)  	} @@ -132,7 +132,7 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr  	// authentication has passed, so add an instance entry for this instance if it hasn't been done already  	i := >smodel.Instance{}  	if err := f.db.GetWhere([]db.Where{{Key: "domain", Value: publicKeyOwnerURI.Host, CaseInsensitive: true}}, i); err != nil { -		if _, ok := err.(db.ErrNoEntries); !ok { +		if err != db.ErrNoEntries {  			// there's been an actual error  			return ctx, false, fmt.Errorf("error getting requesting account with public key id %s: %s", publicKeyOwnerURI.String(), err)  		} @@ -176,8 +176,6 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr  // Finally, if the authentication and authorization succeeds, then  // blocked must be false and error nil. The request will continue  // to be processed. -// -// TODO: implement domain block checking here as well  func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, error) {  	l := f.log.WithFields(logrus.Fields{  		"func": "Blocked", @@ -191,19 +189,18 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er  		return false, errors.New("requested account not set on request context, so couldn't determine blocks")  	} +	blocked, err := f.db.AreURIsBlocked(actorIRIs) +	if err != nil { +		return false, fmt.Errorf("error checking domain blocks: %s", err) +	} +	if blocked { +		return blocked, nil +	} +  	for _, uri := range actorIRIs { -		blockedDomain, err := f.blockedDomain(uri.Host) +		requestingAccount, err := f.db.GetAccountByURI(uri.String())  		if err != nil { -			return false, fmt.Errorf("error checking domain block: %s", err) -		} -		if blockedDomain { -			return true, nil -		} - -		requestingAccount := >smodel.Account{} -		if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, requestingAccount); err != nil { -			_, ok := err.(db.ErrNoEntries) -			if ok { +			if err == db.ErrNoEntries {  				// we don't have an entry for this account so it's not blocked  				// TODO: allow a different default to be set for this behavior  				continue @@ -211,12 +208,11 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er  			return false, fmt.Errorf("error getting account with uri %s: %s", uri.String(), err)  		} -		// check if requested account blocks requesting account -		if err := f.db.GetWhere([]db.Where{ -			{Key: "account_id", Value: requestedAccount.ID}, -			{Key: "target_account_id", Value: requestingAccount.ID}, -		}, >smodel.Block{}); err == nil { -			// a block exists +		blocked, err = f.db.IsBlocked(requestedAccount.ID, requestingAccount.ID, true) +		if err != nil { +			return false, fmt.Errorf("error checking account block: %s", err) +		} +		if blocked {  			return true, nil  		}  	} diff --git a/internal/federation/finger.go b/internal/federation/finger.go index 0ffc60e5a..a5a4fa0e7 100644 --- a/internal/federation/finger.go +++ b/internal/federation/finger.go @@ -30,7 +30,7 @@ import (  )  func (f *federator) FingerRemoteAccount(requestingUsername string, targetUsername string, targetDomain string) (*url.URL, error) { -	if blocked, err := f.blockedDomain(targetDomain); blocked || err != nil { +	if blocked, err := f.db.IsDomainBlocked(targetDomain); blocked || err != nil {  		return nil, fmt.Errorf("FingerRemoteAccount: domain %s is blocked", targetDomain)  	} diff --git a/internal/federation/handshake.go b/internal/federation/handshake.go index 47c8a6c84..0671e78a9 100644 --- a/internal/federation/handshake.go +++ b/internal/federation/handshake.go @@ -1,3 +1,21 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ +  package federation  import "net/url" diff --git a/internal/federation/transport.go b/internal/federation/transport.go index ed28749a1..20aee964b 100644 --- a/internal/federation/transport.go +++ b/internal/federation/transport.go @@ -1,3 +1,21 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ +  package federation  import ( diff --git a/internal/federation/util.go b/internal/federation/util.go deleted file mode 100644 index de8654d32..000000000 --- a/internal/federation/util.go +++ /dev/null @@ -1,23 +0,0 @@ -package federation - -import ( -	"github.com/superseriousbusiness/gotosocial/internal/db" -	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func (f *federator) blockedDomain(host string) (bool, error) { -	b := >smodel.DomainBlock{} -	err := f.db.GetWhere([]db.Where{{Key: "domain", Value: host, CaseInsensitive: true}}, b) -	if err == nil { -		// block exists -		return true, nil -	} - -	if _, ok := err.(db.ErrNoEntries); ok { -		// there are no entries so there's no block -		return false, nil -	} - -	// there's an actual error -	return false, err -} diff --git a/internal/gtsmodel/account.go b/internal/gtsmodel/account.go index e560601b8..435caea6d 100644 --- a/internal/gtsmodel/account.go +++ b/internal/gtsmodel/account.go @@ -45,11 +45,13 @@ type Account struct {  	*/  	// ID of the avatar as a media attachment -	AvatarMediaAttachmentID string `pg:"type:CHAR(26)"` +	AvatarMediaAttachmentID string           `pg:"type:CHAR(26)"` +	AvatarMediaAttachment   *MediaAttachment `pg:"rel:has-one"`  	// For a non-local account, where can the header be fetched?  	AvatarRemoteURL string  	// ID of the header as a media attachment -	HeaderMediaAttachmentID string `pg:"type:CHAR(26)"` +	HeaderMediaAttachmentID string           `pg:"type:CHAR(26)"` +	HeaderMediaAttachment   *MediaAttachment `pg:"rel:has-one"`  	// For a non-local account, where can the header be fetched?  	HeaderRemoteURL string  	// DisplayName for this account. Can be empty, then just the Username will be used for display purposes. diff --git a/internal/gtsmodel/domainblock.go b/internal/gtsmodel/domainblock.go index b32984e95..1bed86d8f 100644 --- a/internal/gtsmodel/domainblock.go +++ b/internal/gtsmodel/domainblock.go @@ -31,7 +31,8 @@ type DomainBlock struct {  	// When was this block updated  	UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`  	// Account ID of the creator of this block -	CreatedByAccountID string `pg:"type:CHAR(26),notnull"` +	CreatedByAccountID string   `pg:"type:CHAR(26),notnull"` +	CreatedByAccount   *Account `pg:"rel:belongs-to"`  	// Private comment on this block, viewable to admins  	PrivateComment string  	// Public comment on this block, viewable (optionally) by everyone diff --git a/internal/gtsmodel/emaildomainblock.go b/internal/gtsmodel/emaildomainblock.go index 51558550a..374454374 100644 --- a/internal/gtsmodel/emaildomainblock.go +++ b/internal/gtsmodel/emaildomainblock.go @@ -31,5 +31,6 @@ type EmailDomainBlock struct {  	// When was this block updated  	UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`  	// Account ID of the creator of this block -	CreatedByAccountID string `pg:"type:CHAR(26),notnull"` +	CreatedByAccountID string   `pg:"type:CHAR(26),notnull"` +	CreatedByAccount   *Account `pg:"rel:belongs-to"`  } diff --git a/internal/gtsmodel/emoji.go b/internal/gtsmodel/emoji.go index 2fa3b7565..f0996d1a3 100644 --- a/internal/gtsmodel/emoji.go +++ b/internal/gtsmodel/emoji.go @@ -73,5 +73,6 @@ type Emoji struct {  	// Is this emoji visible in the admin emoji picker?  	VisibleInPicker bool `pg:",notnull,default:true"`  	// In which emoji category is this emoji visible? -	CategoryID string `pg:"type:CHAR(26)"` +	CategoryID string  `pg:"type:CHAR(26)"` +	Status     *Status `pg:"rel:belongs-to"`  } diff --git a/internal/gtsmodel/follow.go b/internal/gtsmodel/follow.go index f5a170ca8..8f169f8c4 100644 --- a/internal/gtsmodel/follow.go +++ b/internal/gtsmodel/follow.go @@ -29,9 +29,11 @@ type Follow struct {  	// When was this follow last updated?  	UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`  	// Who does this follow belong to? -	AccountID string `pg:"type:CHAR(26),unique:srctarget,notnull"` +	AccountID string   `pg:"type:CHAR(26),unique:srctarget,notnull"` +	Account   *Account `pg:"rel:belongs-to"`  	// Who does AccountID follow? -	TargetAccountID string `pg:"type:CHAR(26),unique:srctarget,notnull"` +	TargetAccountID string   `pg:"type:CHAR(26),unique:srctarget,notnull"` +	TargetAccount   *Account `pg:"rel:has-one"`  	// Does this follow also want to see reblogs and not just posts?  	ShowReblogs bool `pg:"default:true"`  	// What is the activitypub URI of this follow? diff --git a/internal/gtsmodel/followrequest.go b/internal/gtsmodel/followrequest.go index aabb785d2..752c7d0a2 100644 --- a/internal/gtsmodel/followrequest.go +++ b/internal/gtsmodel/followrequest.go @@ -29,9 +29,11 @@ type FollowRequest struct {  	// When was this follow request last updated?  	UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`  	// Who does this follow request originate from? -	AccountID string `pg:"type:CHAR(26),unique:srctarget,notnull"` +	AccountID string  `pg:"type:CHAR(26),unique:srctarget,notnull"` +	Account   Account `pg:"rel:has-one"`  	// Who is the target of this follow request? -	TargetAccountID string `pg:"type:CHAR(26),unique:srctarget,notnull"` +	TargetAccountID string  `pg:"type:CHAR(26),unique:srctarget,notnull"` +	TargetAccount   Account `pg:"rel:has-one"`  	// Does this follow also want to see reblogs and not just posts?  	ShowReblogs bool `pg:"default:true"`  	// What is the activitypub URI of this follow request? diff --git a/internal/gtsmodel/instance.go b/internal/gtsmodel/instance.go index 857831ba3..7b453a0b3 100644 --- a/internal/gtsmodel/instance.go +++ b/internal/gtsmodel/instance.go @@ -19,7 +19,8 @@ type Instance struct {  	// When was this instance suspended, if at all?  	SuspendedAt time.Time  	// ID of any existing domain block for this instance in the database -	DomainBlockID string `pg:"type:CHAR(26)"` +	DomainBlockID string       `pg:"type:CHAR(26)"` +	DomainBlock   *DomainBlock `pg:"rel:has-one"`  	// Short description of this instance  	ShortDescription string  	// Longer description of this instance @@ -31,7 +32,8 @@ type Instance struct {  	// Username of the contact account for this instance  	ContactAccountUsername string  	// Contact account ID in the database for this instance -	ContactAccountID string `pg:"type:CHAR(26)"` +	ContactAccountID string   `pg:"type:CHAR(26)"` +	ContactAccount   *Account `pg:"rel:has-one"`  	// Reputation score of this instance  	Reputation int64 `pg:",notnull,default:0"`  	// Version of the software used on this instance diff --git a/internal/gtsmodel/mediaattachment.go b/internal/gtsmodel/mediaattachment.go index 2aeeee962..0f12caaad 100644 --- a/internal/gtsmodel/mediaattachment.go +++ b/internal/gtsmodel/mediaattachment.go @@ -42,7 +42,8 @@ type MediaAttachment struct {  	// Metadata about the file  	FileMeta FileMeta  	// To which account does this attachment belong -	AccountID string `pg:"type:CHAR(26),notnull"` +	AccountID string   `pg:"type:CHAR(26),notnull"` +	Account   *Account `pg:"rel:belongs-to"`  	// Description of the attachment (for screenreaders)  	Description string  	// To which scheduled status does this attachment belong diff --git a/internal/gtsmodel/mention.go b/internal/gtsmodel/mention.go index 47c780521..931e681db 100644 --- a/internal/gtsmodel/mention.go +++ b/internal/gtsmodel/mention.go @@ -25,17 +25,20 @@ type Mention struct {  	// ID of this mention in the database  	ID string `pg:"type:CHAR(26),pk,notnull,unique"`  	// ID of the status this mention originates from -	StatusID string `pg:"type:CHAR(26),notnull"` +	StatusID string  `pg:"type:CHAR(26),notnull"` +	Status   *Status `pg:"rel:belongs-to"`  	// When was this mention created?  	CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`  	// When was this mention last updated?  	UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`  	// What's the internal account ID of the originator of the mention? -	OriginAccountID string `pg:"type:CHAR(26),notnull"` +	OriginAccountID string   `pg:"type:CHAR(26),notnull"` +	OriginAccount   *Account `pg:"rel:has-one"`  	// What's the AP URI of the originator of the mention?  	OriginAccountURI string `pg:",notnull"`  	// What's the internal account ID of the mention target? -	TargetAccountID string `pg:"type:CHAR(26),notnull"` +	TargetAccountID string   `pg:"type:CHAR(26),notnull"` +	TargetAccount   *Account `pg:"rel:has-one"`  	// Prevent this mention from generating a notification?  	Silent bool @@ -52,14 +55,14 @@ type Mention struct {  	//  	// This will not be put in the database, it's just for convenience.  	NameString string `pg:"-"` -	// MentionedAccountURI is the AP ID (uri) of the user mentioned. +	// TargetAccountURI is the AP ID (uri) of the user mentioned.  	//  	// This will not be put in the database, it's just for convenience. -	MentionedAccountURI string `pg:"-"` -	// MentionedAccountURL is the web url of the user mentioned. +	TargetAccountURI string `pg:"-"` +	// TargetAccountURL is the web url of the user mentioned.  	//  	// This will not be put in the database, it's just for convenience. -	MentionedAccountURL string `pg:"-"` +	TargetAccountURL string `pg:"-"`  	// A pointer to the gtsmodel account of the mentioned account. -	GTSAccount *Account `pg:"-"` +  } diff --git a/internal/gtsmodel/notification.go b/internal/gtsmodel/notification.go index efd4fe484..b85bc969e 100644 --- a/internal/gtsmodel/notification.go +++ b/internal/gtsmodel/notification.go @@ -29,24 +29,16 @@ type Notification struct {  	// Creation time of this notification  	CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`  	// Which account does this notification target (ie., who will receive the notification?) -	TargetAccountID string `pg:"type:CHAR(26),notnull"` +	TargetAccountID string   `pg:"type:CHAR(26),notnull"` +	TargetAccount   *Account `pg:"rel:has-one"`  	// Which account performed the action that created this notification? -	OriginAccountID string `pg:"type:CHAR(26),notnull"` +	OriginAccountID string   `pg:"type:CHAR(26),notnull"` +	OriginAccount   *Account `pg:"rel:has-one"`  	// If the notification pertains to a status, what is the database ID of that status? -	StatusID string `pg:"type:CHAR(26)"` +	StatusID string  `pg:"type:CHAR(26)"` +	Status   *Status `pg:"rel:has-one"`  	// Has this notification been read already?  	Read bool - -	/* -		NON-DATABASE fields -	*/ - -	// gts model of the target account, won't be put in the database, it's just for convenience when passing the notification around. -	GTSTargetAccount *Account `pg:"-"` -	// gts model of the origin account, won't be put in the database, it's just for convenience when passing the notification around. -	GTSOriginAccount *Account `pg:"-"` -	// gts model of the relevant status, won't be put in the database, it's just for convenience when passing the notification around. -	GTSStatus *Status `pg:"-"`  }  // NotificationType describes the reason/type of this notification. diff --git a/internal/gtsmodel/status.go b/internal/gtsmodel/status.go index 106298bcd..354f37e04 100644 --- a/internal/gtsmodel/status.go +++ b/internal/gtsmodel/status.go @@ -33,13 +33,17 @@ type Status struct {  	// the html-formatted content of this status  	Content string  	// Database IDs of any media attachments associated with this status -	Attachments []string `pg:",array"` +	AttachmentIDs []string           `pg:"attachments,array"` +	Attachments   []*MediaAttachment `pg:"attached_media,rel:has-many"`  	// Database IDs of any tags used in this status -	Tags []string `pg:",array"` +	TagIDs []string `pg:"tags,array"` +	Tags   []*Tag   `pg:"attached_tags,many2many:status_to_tags"` // https://pg.uptrace.dev/orm/many-to-many-relation/  	// Database IDs of any mentions in this status -	Mentions []string `pg:",array"` +	MentionIDs []string   `pg:"mentions,array"` +	Mentions   []*Mention `pg:"attached_mentions,rel:has-many"`  	// Database IDs of any emojis used in this status -	Emojis []string `pg:",array"` +	EmojiIDs []string `pg:"emojis,array"` +	Emojis   []*Emoji `pg:"attached_emojis,many2many:status_to_emojis"` // https://pg.uptrace.dev/orm/many-to-many-relation/  	// when was this status created?  	CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`  	// when was this status updated? @@ -47,19 +51,24 @@ type Status struct {  	// is this status from a local account?  	Local bool  	// which account posted this status? -	AccountID string `pg:"type:CHAR(26),notnull"` +	AccountID string   `pg:"type:CHAR(26),notnull"` +	Account   *Account `pg:"rel:has-one"`  	// AP uri of the owner of this status  	AccountURI string  	// id of the status this status is a reply to -	InReplyToID string `pg:"type:CHAR(26)"` +	InReplyToID string  `pg:"type:CHAR(26)"` +	InReplyTo   *Status `pg:"rel:has-one"`  	// AP uri of the status this status is a reply to  	InReplyToURI string  	// id of the account that this status replies to -	InReplyToAccountID string `pg:"type:CHAR(26)"` +	InReplyToAccountID string   `pg:"type:CHAR(26)"` +	InReplyToAccount   *Account `pg:"rel:has-one"`  	// id of the status this status is a boost of -	BoostOfID string `pg:"type:CHAR(26)"` +	BoostOfID string  `pg:"type:CHAR(26)"` +	BoostOf   *Status `pg:"rel:has-one"`  	// id of the account that owns the boosted status -	BoostOfAccountID string `pg:"type:CHAR(26)"` +	BoostOfAccountID string   `pg:"type:CHAR(26)"` +	BoostOfAccount   *Account `pg:"rel:has-one"`  	// cw string for this status  	ContentWarning string  	// visibility entry for this status @@ -69,7 +78,8 @@ type Status struct {  	// what language is this status written in?  	Language string  	// Which application was used to create this status? -	CreatedWithApplicationID string `pg:"type:CHAR(26)"` +	CreatedWithApplicationID string       `pg:"type:CHAR(26)"` +	CreatedWithApplication   *Application `pg:"rel:has-one"`  	// advanced visibility for this status  	VisibilityAdvanced *VisibilityAdvanced  	// What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types @@ -79,32 +89,18 @@ type Status struct {  	Text string  	// Has this status been pinned by its owner?  	Pinned bool +} -	/* -		INTERNAL MODEL NON-DATABASE FIELDS - -		These are for convenience while passing the status around internally, -		but these fields should *never* be put in the db. -	*/ +// StatusToTag is an intermediate struct to facilitate the many2many relationship between a status and one or more tags. +type StatusToTag struct { +	StatusID string `pg:"unique:statustag"` +	TagID    string `pg:"unique:statustag"` +} -	// Account that created this status -	GTSAuthorAccount *Account `pg:"-"` -	// Mentions created in this status -	GTSMentions []*Mention `pg:"-"` -	// Hashtags used in this status -	GTSTags []*Tag `pg:"-"` -	// Emojis used in this status -	GTSEmojis []*Emoji `pg:"-"` -	// MediaAttachments used in this status -	GTSMediaAttachments []*MediaAttachment `pg:"-"` -	// Status being replied to -	GTSReplyToStatus *Status `pg:"-"` -	// Account being replied to -	GTSReplyToAccount *Account `pg:"-"` -	// Status being boosted -	GTSBoostedStatus *Status `pg:"-"` -	// Account of the boosted status -	GTSBoostedAccount *Account `pg:"-"` +// StatusToEmoji is an intermediate struct to facilitate the many2many relationship between a status and one or more emojis. +type StatusToEmoji struct { +	StatusID string `pg:"unique:statusemoji"` +	EmojiID  string `pg:"unique:statusemoji"`  }  // Visibility represents the visibility granularity of a status. diff --git a/internal/gtsmodel/statusbookmark.go b/internal/gtsmodel/statusbookmark.go index 7d95067cc..468939bae 100644 --- a/internal/gtsmodel/statusbookmark.go +++ b/internal/gtsmodel/statusbookmark.go @@ -27,9 +27,11 @@ type StatusBookmark struct {  	// when was this bookmark created  	CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`  	// id of the account that created ('did') the bookmarking -	AccountID string `pg:"type:CHAR(26),notnull"` +	AccountID string   `pg:"type:CHAR(26),notnull"` +	Account   *Account `pg:"rel:belongs-to"`  	// id the account owning the bookmarked status -	TargetAccountID string `pg:"type:CHAR(26),notnull"` +	TargetAccountID string   `pg:"type:CHAR(26),notnull"` +	TargetAccount   *Account `pg:"rel:has-one"`  	// database id of the status that has been bookmarked  	StatusID string `pg:"type:CHAR(26),notnull"`  } diff --git a/internal/gtsmodel/statusfave.go b/internal/gtsmodel/statusfave.go index 7152db37a..17952673a 100644 --- a/internal/gtsmodel/statusfave.go +++ b/internal/gtsmodel/statusfave.go @@ -27,18 +27,14 @@ type StatusFave struct {  	// when was this fave created  	CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`  	// id of the account that created ('did') the fave -	AccountID string `pg:"type:CHAR(26),notnull"` +	AccountID string   `pg:"type:CHAR(26),notnull"` +	Account   *Account `pg:"rel:has-one"`  	// id the account owning the faved status -	TargetAccountID string `pg:"type:CHAR(26),notnull"` +	TargetAccountID string   `pg:"type:CHAR(26),notnull"` +	TargetAccount   *Account `pg:"rel:has-one"`  	// database id of the status that has been 'faved' -	StatusID string `pg:"type:CHAR(26),notnull"` +	StatusID string  `pg:"type:CHAR(26),notnull"` +	Status   *Status `pg:"rel:has-one"`  	// ActivityPub URI of this fave  	URI string `pg:",notnull"` - -	// GTSStatus is the status being interacted with. It won't be put or retrieved from the db, it's just for conveniently passing a pointer around. -	GTSStatus *Status `pg:"-"` -	// GTSTargetAccount is the account being interacted with. It won't be put or retrieved from the db, it's just for conveniently passing a pointer around. -	GTSTargetAccount *Account `pg:"-"` -	// GTSFavingAccount is the account doing the faving. It won't be put or retrieved from the db, it's just for conveniently passing a pointer around. -	GTSFavingAccount *Account `pg:"-"`  } diff --git a/internal/gtsmodel/statusmute.go b/internal/gtsmodel/statusmute.go index 6cd2b732f..472a5ec09 100644 --- a/internal/gtsmodel/statusmute.go +++ b/internal/gtsmodel/statusmute.go @@ -27,9 +27,12 @@ type StatusMute struct {  	// when was this mute created  	CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`  	// id of the account that created ('did') the mute -	AccountID string `pg:"type:CHAR(26),notnull"` +	AccountID string   `pg:"type:CHAR(26),notnull"` +	Account   *Account `pg:"rel:belongs-to"`  	// id the account owning the muted status (can be the same as accountID) -	TargetAccountID string `pg:"type:CHAR(26),notnull"` +	TargetAccountID string   `pg:"type:CHAR(26),notnull"` +	TargetAccount   *Account `pg:"rel:has-one"`  	// database id of the status that has been muted -	StatusID string `pg:"type:CHAR(26),notnull"` +	StatusID string  `pg:"type:CHAR(26),notnull"` +	Status   *Status `pg:"rel:has-one"`  } diff --git a/internal/gtsmodel/tag.go b/internal/gtsmodel/tag.go index c151e348f..27cce1c8b 100644 --- a/internal/gtsmodel/tag.go +++ b/internal/gtsmodel/tag.go @@ -27,7 +27,7 @@ type Tag struct {  	// Href of this tag, eg https://example.org/tags/somehashtag  	URL string  	// name of this tag -- the tag without the hash part -	Name string `pg:",unique,pk,notnull"` +	Name string `pg:",unique,notnull"`  	// Which account ID is the first one we saw using this tag?  	FirstSeenFromAccountID string `pg:"type:CHAR(26)"`  	// when was this tag created diff --git a/internal/gtsmodel/user.go b/internal/gtsmodel/user.go index a1e912e99..fe8ebcabe 100644 --- a/internal/gtsmodel/user.go +++ b/internal/gtsmodel/user.go @@ -35,7 +35,8 @@ type User struct {  	// confirmed email address for this user, this should be unique -- only one email address registered per instance, multiple users per email are not supported  	Email string `pg:"default:null,unique"`  	// The id of the local gtsmodel.Account entry for this user, if it exists (unconfirmed users don't have an account yet) -	AccountID string `pg:"type:CHAR(26),unique"` +	AccountID string   `pg:"type:CHAR(26),unique"` +	Account   *Account `pg:"rel:has-one"`  	// The encrypted password of this user, generated using https://pkg.go.dev/golang.org/x/crypto/bcrypt#GenerateFromPassword. A salt is included so we're safe against 🌈 tables  	EncryptedPassword string `pg:",notnull"` @@ -68,7 +69,8 @@ type User struct {  	// In what timezone/locale is this user located?  	Locale string  	// Which application id created this user? See gtsmodel.Application -	CreatedByApplicationID string `pg:"type:CHAR(26)"` +	CreatedByApplicationID string       `pg:"type:CHAR(26)"` +	CreatedByApplication   *Application `pg:"rel:has-one"`  	// When did we last contact this user  	LastEmailedAt time.Time `pg:"type:timestamp"` diff --git a/internal/media/handler.go b/internal/media/handler.go index 0bcf46488..c383a922e 100644 --- a/internal/media/handler.go +++ b/internal/media/handler.go @@ -142,7 +142,7 @@ func (mh *mediaHandler) ProcessHeaderOrAvatar(attachment []byte, accountID strin  	}  	// set it in the database -	if err := mh.db.SetHeaderOrAvatarForAccountID(ma, accountID); err != nil { +	if err := mh.db.SetAccountHeaderOrAvatar(ma, accountID); err != nil {  		return nil, fmt.Errorf("error putting %s in database: %s", mediaType, err)  	} @@ -231,8 +231,8 @@ func (mh *mediaHandler) ProcessLocalEmoji(emojiBytes []byte, shortcode string) (  	// since emoji aren't 'owned' by an account, but we still want to use the same pattern for serving them through the filserver,  	// (ie., fileserver/ACCOUNT_ID/etc etc) we need to fetch the INSTANCE ACCOUNT from the database. That is, the account that's created  	// with the same username as the instance hostname, which doesn't belong to any particular user. -	instanceAccount := >smodel.Account{} -	if err := mh.db.GetLocalAccountByUsername(mh.config.Host, instanceAccount); err != nil { +	instanceAccount, err := mh.db.GetInstanceAccount("") +	if err != nil {  		return nil, fmt.Errorf("error fetching instance account: %s", err)  	} diff --git a/internal/oauth/clientstore.go b/internal/oauth/clientstore.go index 998f6784e..2e7e0ae88 100644 --- a/internal/oauth/clientstore.go +++ b/internal/oauth/clientstore.go @@ -27,11 +27,11 @@ import (  )  type clientStore struct { -	db db.DB +	db db.Basic  }  // NewClientStore returns an implementation of the oauth2 ClientStore interface, using the given db as a storage backend. -func NewClientStore(db db.DB) oauth2.ClientStore { +func NewClientStore(db db.Basic) oauth2.ClientStore {  	pts := &clientStore{  		db: db,  	} diff --git a/internal/oauth/clientstore_test.go b/internal/oauth/clientstore_test.go index c515ff513..fd3452405 100644 --- a/internal/oauth/clientstore_test.go +++ b/internal/oauth/clientstore_test.go @@ -99,7 +99,7 @@ func (suite *PgClientStoreTestSuite) TestClientSetAndDelete() {  	// try to get the deleted client; we should get an error  	deletedClient, err := cs.GetByID(context.Background(), suite.testClientID)  	suite.Assert().Nil(deletedClient) -	suite.Assert().EqualValues(db.ErrNoEntries{}, err) +	suite.Assert().EqualValues(db.ErrNoEntries, err)  }  func TestPgClientStoreTestSuite(t *testing.T) { diff --git a/internal/oauth/server.go b/internal/oauth/server.go index 1289b18af..6d8f50064 100644 --- a/internal/oauth/server.go +++ b/internal/oauth/server.go @@ -66,7 +66,7 @@ type s struct {  }  // New returns a new oauth server that implements the Server interface -func New(database db.DB, log *logrus.Logger) Server { +func New(database db.Basic, log *logrus.Logger) Server {  	ts := newTokenStore(context.Background(), database, log)  	cs := NewClientStore(database) diff --git a/internal/oauth/tokenstore.go b/internal/oauth/tokenstore.go index 5f8e07882..4fd3183fc 100644 --- a/internal/oauth/tokenstore.go +++ b/internal/oauth/tokenstore.go @@ -34,7 +34,7 @@ import (  // tokenStore is an implementation of oauth2.TokenStore, which uses our db interface as a storage backend.  type tokenStore struct {  	oauth2.TokenStore -	db  db.DB +	db  db.Basic  	log *logrus.Logger  } @@ -42,7 +42,7 @@ type tokenStore struct {  //  // In order to allow tokens to 'expire', it will also set off a goroutine that iterates through  // the tokens in the DB once per minute and deletes any that have expired. -func newTokenStore(ctx context.Context, db db.DB, log *logrus.Logger) oauth2.TokenStore { +func newTokenStore(ctx context.Context, db db.Basic, log *logrus.Logger) oauth2.TokenStore {  	pts := &tokenStore{  		db:  db,  		log: log, diff --git a/internal/processing/account/createblock.go b/internal/processing/account/createblock.go index 79ce03805..f10a2efa3 100644 --- a/internal/processing/account/createblock.go +++ b/internal/processing/account/createblock.go @@ -31,24 +31,20 @@ import (  func (p *processor) BlockCreate(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {  	// make sure the target account actually exists in our db -	targetAcct := >smodel.Account{} -	if err := p.db.GetByID(targetAccountID, targetAcct); err != nil { -		if _, ok := err.(db.ErrNoEntries); ok { -			return nil, gtserror.NewErrorNotFound(fmt.Errorf("BlockCreate: account %s not found in the db: %s", targetAccountID, err)) -		} +	targetAccount, err := p.db.GetAccountByID(targetAccountID) +	if err != nil { +		return nil, gtserror.NewErrorNotFound(fmt.Errorf("BlockCreate: error getting account %s from the db: %s", targetAccountID, err))  	}  	// if requestingAccount already blocks target account, we don't need to do anything -	block := >smodel.Block{} -	if err := p.db.GetWhere([]db.Where{ -		{Key: "account_id", Value: requestingAccount.ID}, -		{Key: "target_account_id", Value: targetAccountID}, -	}, block); err == nil { -		// block already exists, just return relationship +	if blocked, err := p.db.IsBlocked(requestingAccount.ID, targetAccountID, false); err != nil { +		return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error checking existence of block: %s", err)) +	} else if blocked {  		return p.RelationshipGet(requestingAccount, targetAccountID)  	}  	// make the block +	block := >smodel.Block{}  	newBlockID, err := id.NewULID()  	if err != nil {  		return nil, gtserror.NewErrorInternalError(err) @@ -57,7 +53,7 @@ func (p *processor) BlockCreate(requestingAccount *gtsmodel.Account, targetAccou  	block.AccountID = requestingAccount.ID  	block.Account = requestingAccount  	block.TargetAccountID = targetAccountID -	block.TargetAccount = targetAcct +	block.TargetAccount = targetAccount  	block.URI = util.GenerateURIForBlock(requestingAccount.Username, p.config.Protocol, p.config.Host, newBlockID)  	// whack it in the database @@ -123,7 +119,7 @@ func (p *processor) BlockCreate(requestingAccount *gtsmodel.Account, targetAccou  				URI:             frURI,  			},  			OriginAccount: requestingAccount, -			TargetAccount: targetAcct, +			TargetAccount: targetAccount,  		}  	} @@ -138,7 +134,7 @@ func (p *processor) BlockCreate(requestingAccount *gtsmodel.Account, targetAccou  				URI:             fURI,  			},  			OriginAccount: requestingAccount, -			TargetAccount: targetAcct, +			TargetAccount: targetAccount,  		}  	} @@ -148,7 +144,7 @@ func (p *processor) BlockCreate(requestingAccount *gtsmodel.Account, targetAccou  		APActivityType: gtsmodel.ActivityStreamsCreate,  		GTSModel:       block,  		OriginAccount:  requestingAccount, -		TargetAccount:  targetAcct, +		TargetAccount:  targetAccount,  	}  	return p.RelationshipGet(requestingAccount, targetAccountID) diff --git a/internal/processing/account/createfollow.go b/internal/processing/account/createfollow.go index e89db9d47..8c856a50e 100644 --- a/internal/processing/account/createfollow.go +++ b/internal/processing/account/createfollow.go @@ -31,38 +31,33 @@ import (  func (p *processor) FollowCreate(requestingAccount *gtsmodel.Account, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) {  	// if there's a block between the accounts we shouldn't create the request ofc -	blocked, err := p.db.Blocked(requestingAccount.ID, form.ID) -	if err != nil { +	if blocked, err := p.db.IsBlocked(requestingAccount.ID, form.ID, true); err != nil {  		return nil, gtserror.NewErrorInternalError(err) -	} -	if blocked { -		return nil, gtserror.NewErrorNotFound(fmt.Errorf("accountfollowcreate: block exists between accounts")) +	} else if blocked { +		return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))  	}  	// make sure the target account actually exists in our db -	targetAcct := >smodel.Account{} -	if err := p.db.GetByID(form.ID, targetAcct); err != nil { -		if _, ok := err.(db.ErrNoEntries); ok { +	targetAcct, err := p.db.GetAccountByID(form.ID) +	if err != nil { +		if err == db.ErrNoEntries {  			return nil, gtserror.NewErrorNotFound(fmt.Errorf("accountfollowcreate: account %s not found in the db: %s", form.ID, err))  		} +		return nil, gtserror.NewErrorInternalError(err)  	}  	// check if a follow exists already -	follows, err := p.db.Follows(requestingAccount, targetAcct) -	if err != nil { +	if follows, err := p.db.IsFollowing(requestingAccount, targetAcct); err != nil {  		return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow in db: %s", err)) -	} -	if follows { +	} else if follows {  		// already follows so just return the relationship  		return p.RelationshipGet(requestingAccount, form.ID)  	} -	// check if a follow exists already -	followRequested, err := p.db.FollowRequested(requestingAccount, targetAcct) -	if err != nil { +	// check if a follow request exists already +	if followRequested, err := p.db.IsFollowRequested(requestingAccount, targetAcct); err != nil {  		return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow request in db: %s", err)) -	} -	if followRequested { +	} else if followRequested {  		// already follow requested so just return the relationship  		return p.RelationshipGet(requestingAccount, form.ID)  	} diff --git a/internal/processing/account/delete.go b/internal/processing/account/delete.go index 65ac02291..e8840abae 100644 --- a/internal/processing/account/delete.go +++ b/internal/processing/account/delete.go @@ -133,9 +133,9 @@ func (p *processor) Delete(account *gtsmodel.Account, origin string) error {  	var maxID string  selectStatusesLoop:  	for { -		statuses, err := p.db.GetStatusesForAccount(account.ID, 20, false, maxID, false, false) +		statuses, err := p.db.GetAccountStatuses(account.ID, 20, false, maxID, false, false)  		if err != nil { -			if _, ok := err.(db.ErrNoEntries); ok { +			if err == db.ErrNoEntries {  				// no statuses left for this instance so we're done  				l.Infof("Delete: done iterating through statuses for account %s", account.Username)  				break selectStatusesLoop @@ -147,7 +147,7 @@ selectStatusesLoop:  		for i, s := range statuses {  			// pass the status delete through the client api channel for processing -			s.GTSAuthorAccount = account +			s.Account = account  			l.Debug("putting status in the client api channel")  			p.fromClientAPI <- gtsmodel.FromClientAPI{  				APObjectType:   gtsmodel.ActivityStreamsNote, @@ -158,7 +158,7 @@ selectStatusesLoop:  			}  			if err := p.db.DeleteByID(s.ID, s); err != nil { -				if _, ok := err.(db.ErrNoEntries); !ok { +				if err != db.ErrNoEntries {  					// actual error has occurred  					l.Errorf("Delete: db error status %s for account %s: %s", s.ID, account.Username, err)  					break selectStatusesLoop @@ -168,7 +168,7 @@ selectStatusesLoop:  			// if there are any boosts of this status, delete them as well  			boosts := []*gtsmodel.Status{}  			if err := p.db.GetWhere([]db.Where{{Key: "boost_of_id", Value: s.ID}}, &boosts); err != nil { -				if _, ok := err.(db.ErrNoEntries); !ok { +				if err != db.ErrNoEntries {  					// an actual error has occurred  					l.Errorf("Delete: db error selecting boosts of status %s for account %s: %s", s.ID, account.Username, err)  					break selectStatusesLoop @@ -190,7 +190,7 @@ selectStatusesLoop:  				}  				if err := p.db.DeleteByID(b.ID, b); err != nil { -					if _, ok := err.(db.ErrNoEntries); !ok { +					if err != db.ErrNoEntries {  						// actual error has occurred  						l.Errorf("Delete: db error deleting boost with id %s: %s", b.ID, err)  						break selectStatusesLoop diff --git a/internal/processing/account/get.go b/internal/processing/account/get.go index a70bf02bd..3dfc54b51 100644 --- a/internal/processing/account/get.go +++ b/internal/processing/account/get.go @@ -30,7 +30,7 @@ import (  func (p *processor) Get(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, error) {  	targetAccount := >smodel.Account{}  	if err := p.db.GetByID(targetAccountID, targetAccount); err != nil { -		if _, ok := err.(db.ErrNoEntries); ok { +		if err == db.ErrNoEntries {  			return nil, errors.New("account not found")  		}  		return nil, fmt.Errorf("db error: %s", err) @@ -39,7 +39,7 @@ func (p *processor) Get(requestingAccount *gtsmodel.Account, targetAccountID str  	var blocked bool  	var err error  	if requestingAccount != nil { -		blocked, err = p.db.Blocked(requestingAccount.ID, targetAccountID) +		blocked, err = p.db.IsBlocked(requestingAccount.ID, targetAccountID, true)  		if err != nil {  			return nil, fmt.Errorf("error checking account block: %s", err)  		} diff --git a/internal/processing/account/getfollowers.go b/internal/processing/account/getfollowers.go index 0806a82c0..4f66b40ee 100644 --- a/internal/processing/account/getfollowers.go +++ b/internal/processing/account/getfollowers.go @@ -28,26 +28,23 @@ import (  )  func (p *processor) FollowersGet(requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) { -	blocked, err := p.db.Blocked(requestingAccount.ID, targetAccountID) -	if err != nil { +	if blocked, err := p.db.IsBlocked(requestingAccount.ID, targetAccountID, true); err != nil {  		return nil, gtserror.NewErrorInternalError(err) -	} - -	if blocked { +	} else if blocked {  		return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))  	} -	followers := []gtsmodel.Follow{}  	accounts := []apimodel.Account{} -	if err := p.db.GetFollowersByAccountID(targetAccountID, &followers, false); err != nil { -		if _, ok := err.(db.ErrNoEntries); ok { +	follows, err := p.db.GetAccountFollowedBy(targetAccountID, false) +	if err != nil { +		if err == db.ErrNoEntries {  			return accounts, nil  		}  		return nil, gtserror.NewErrorInternalError(err)  	} -	for _, f := range followers { -		blocked, err := p.db.Blocked(requestingAccount.ID, f.AccountID) +	for _, f := range follows { +		blocked, err := p.db.IsBlocked(requestingAccount.ID, f.AccountID, true)  		if err != nil {  			return nil, gtserror.NewErrorInternalError(err)  		} @@ -55,15 +52,18 @@ func (p *processor) FollowersGet(requestingAccount *gtsmodel.Account, targetAcco  			continue  		} -		a := >smodel.Account{} -		if err := p.db.GetByID(f.AccountID, a); err != nil { -			if _, ok := err.(db.ErrNoEntries); ok { -				continue +		if f.Account == nil { +			a, err := p.db.GetAccountByID(f.AccountID) +			if err != nil { +				if err == db.ErrNoEntries { +					continue +				} +				return nil, gtserror.NewErrorInternalError(err)  			} -			return nil, gtserror.NewErrorInternalError(err) +			f.Account = a  		} -		account, err := p.tc.AccountToMastoPublic(a) +		account, err := p.tc.AccountToMastoPublic(f.Account)  		if err != nil {  			return nil, gtserror.NewErrorInternalError(err)  		} diff --git a/internal/processing/account/getfollowing.go b/internal/processing/account/getfollowing.go index 75e89dacb..c7fb426f9 100644 --- a/internal/processing/account/getfollowing.go +++ b/internal/processing/account/getfollowing.go @@ -28,26 +28,23 @@ import (  )  func (p *processor) FollowingGet(requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) { -	blocked, err := p.db.Blocked(requestingAccount.ID, targetAccountID) -	if err != nil { +	if blocked, err := p.db.IsBlocked(requestingAccount.ID, targetAccountID, true); err != nil {  		return nil, gtserror.NewErrorInternalError(err) -	} - -	if blocked { +	} else if blocked {  		return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))  	} -	following := []gtsmodel.Follow{}  	accounts := []apimodel.Account{} -	if err := p.db.GetFollowingByAccountID(targetAccountID, &following); err != nil { -		if _, ok := err.(db.ErrNoEntries); ok { +	follows, err := p.db.GetAccountFollows(targetAccountID) +	if err != nil { +		if err == db.ErrNoEntries {  			return accounts, nil  		}  		return nil, gtserror.NewErrorInternalError(err)  	} -	for _, f := range following { -		blocked, err := p.db.Blocked(requestingAccount.ID, f.AccountID) +	for _, f := range follows { +		blocked, err := p.db.IsBlocked(requestingAccount.ID, f.AccountID, true)  		if err != nil {  			return nil, gtserror.NewErrorInternalError(err)  		} @@ -55,15 +52,18 @@ func (p *processor) FollowingGet(requestingAccount *gtsmodel.Account, targetAcco  			continue  		} -		a := >smodel.Account{} -		if err := p.db.GetByID(f.TargetAccountID, a); err != nil { -			if _, ok := err.(db.ErrNoEntries); ok { -				continue +		if f.TargetAccount == nil { +			a, err := p.db.GetAccountByID(f.TargetAccountID) +			if err != nil { +				if err == db.ErrNoEntries { +					continue +				} +				return nil, gtserror.NewErrorInternalError(err)  			} -			return nil, gtserror.NewErrorInternalError(err) +			f.TargetAccount = a  		} -		account, err := p.tc.AccountToMastoPublic(a) +		account, err := p.tc.AccountToMastoPublic(f.TargetAccount)  		if err != nil {  			return nil, gtserror.NewErrorInternalError(err)  		} diff --git a/internal/processing/account/getstatuses.go b/internal/processing/account/getstatuses.go index b8ccbc528..dc21e7006 100644 --- a/internal/processing/account/getstatuses.go +++ b/internal/processing/account/getstatuses.go @@ -28,18 +28,17 @@ import (  )  func (p *processor) StatusesGet(requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode) { -	targetAccount := >smodel.Account{} -	if err := p.db.GetByID(targetAccountID, targetAccount); err != nil { -		if _, ok := err.(db.ErrNoEntries); ok { -			return nil, gtserror.NewErrorNotFound(fmt.Errorf("no entry found for account id %s", targetAccountID)) -		} +	if blocked, err := p.db.IsBlocked(requestingAccount.ID, targetAccountID, true); err != nil {  		return nil, gtserror.NewErrorInternalError(err) +	} else if blocked { +		return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))  	}  	apiStatuses := []apimodel.Status{} -	statuses, err := p.db.GetStatusesForAccount(targetAccountID, limit, excludeReplies, maxID, pinnedOnly, mediaOnly) + +	statuses, err := p.db.GetAccountStatuses(targetAccountID, limit, excludeReplies, maxID, pinnedOnly, mediaOnly)  	if err != nil { -		if _, ok := err.(db.ErrNoEntries); ok { +		if err == db.ErrNoEntries {  			return apiStatuses, nil  		}  		return nil, gtserror.NewErrorInternalError(err) diff --git a/internal/processing/account/removeblock.go b/internal/processing/account/removeblock.go index 03b0c6750..7c1f2bc17 100644 --- a/internal/processing/account/removeblock.go +++ b/internal/processing/account/removeblock.go @@ -29,11 +29,9 @@ import (  func (p *processor) BlockRemove(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {  	// make sure the target account actually exists in our db -	targetAcct := >smodel.Account{} -	if err := p.db.GetByID(targetAccountID, targetAcct); err != nil { -		if _, ok := err.(db.ErrNoEntries); ok { -			return nil, gtserror.NewErrorNotFound(fmt.Errorf("BlockRemove: account %s not found in the db: %s", targetAccountID, err)) -		} +	targetAccount, err := p.db.GetAccountByID(targetAccountID) +	if err != nil { +		return nil, gtserror.NewErrorNotFound(fmt.Errorf("BlockCreate: error getting account %s from the db: %s", targetAccountID, err))  	}  	// check if a block exists, and remove it if it does (storing the URI for later) @@ -44,7 +42,7 @@ func (p *processor) BlockRemove(requestingAccount *gtsmodel.Account, targetAccou  		{Key: "target_account_id", Value: targetAccountID},  	}, block); err == nil {  		block.Account = requestingAccount -		block.TargetAccount = targetAcct +		block.TargetAccount = targetAccount  		if err := p.db.DeleteByID(block.ID, >smodel.Block{}); err != nil {  			return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockRemove: error removing block from db: %s", err))  		} @@ -58,7 +56,7 @@ func (p *processor) BlockRemove(requestingAccount *gtsmodel.Account, targetAccou  			APActivityType: gtsmodel.ActivityStreamsUndo,  			GTSModel:       block,  			OriginAccount:  requestingAccount, -			TargetAccount:  targetAcct, +			TargetAccount:  targetAccount,  		}  	} diff --git a/internal/processing/account/removefollow.go b/internal/processing/account/removefollow.go index ef8994893..6646d694e 100644 --- a/internal/processing/account/removefollow.go +++ b/internal/processing/account/removefollow.go @@ -29,7 +29,7 @@ import (  func (p *processor) FollowRemove(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {  	// if there's a block between the accounts we shouldn't do anything -	blocked, err := p.db.Blocked(requestingAccount.ID, targetAccountID) +	blocked, err := p.db.IsBlocked(requestingAccount.ID, targetAccountID, true)  	if err != nil {  		return nil, gtserror.NewErrorInternalError(err)  	} @@ -40,7 +40,7 @@ func (p *processor) FollowRemove(requestingAccount *gtsmodel.Account, targetAcco  	// make sure the target account actually exists in our db  	targetAcct := >smodel.Account{}  	if err := p.db.GetByID(targetAccountID, targetAcct); err != nil { -		if _, ok := err.(db.ErrNoEntries); ok { +		if err == db.ErrNoEntries {  			return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: account %s not found in the db: %s", targetAccountID, err))  		}  	} diff --git a/internal/processing/admin/createdomainblock.go b/internal/processing/admin/createdomainblock.go index df02cef94..624f632dc 100644 --- a/internal/processing/admin/createdomainblock.go +++ b/internal/processing/admin/createdomainblock.go @@ -36,7 +36,7 @@ func (p *processor) DomainBlockCreate(account *gtsmodel.Account, domain string,  	domainBlock := >smodel.DomainBlock{}  	err := p.db.GetWhere([]db.Where{{Key: "domain", Value: domain, CaseInsensitive: true}}, domainBlock)  	if err != nil { -		if _, ok := err.(db.ErrNoEntries); !ok { +		if err != db.ErrNoEntries {  			// something went wrong in the DB  			return nil, gtserror.NewErrorInternalError(fmt.Errorf("DomainBlockCreate: db error checking for existence of domain block %s: %s", domain, err))  		} @@ -60,7 +60,7 @@ func (p *processor) DomainBlockCreate(account *gtsmodel.Account, domain string,  		// put the new block in the database  		if err := p.db.Put(domainBlock); err != nil { -			if _, ok := err.(db.ErrAlreadyExists); !ok { +			if err != db.ErrNoEntries {  				// there's a real error creating the block  				return nil, gtserror.NewErrorInternalError(fmt.Errorf("DomainBlockCreate: db error putting new domain block %s: %s", domain, err))  			} @@ -123,9 +123,9 @@ func (p *processor) initiateDomainBlockSideEffects(account *gtsmodel.Account, bl  selectAccountsLoop:  	for { -		accounts, err := p.db.GetAccountsForInstance(block.Domain, maxID, limit) +		accounts, err := p.db.GetInstanceAccounts(block.Domain, maxID, limit)  		if err != nil { -			if _, ok := err.(db.ErrNoEntries); ok { +			if err == db.ErrNoEntries {  				// no accounts left for this instance so we're done  				l.Infof("domainBlockProcessSideEffects: done iterating through accounts for domain %s", block.Domain)  				break selectAccountsLoop diff --git a/internal/processing/admin/deletedomainblock.go b/internal/processing/admin/deletedomainblock.go index b41fedd92..edb0a58f9 100644 --- a/internal/processing/admin/deletedomainblock.go +++ b/internal/processing/admin/deletedomainblock.go @@ -32,7 +32,7 @@ func (p *processor) DomainBlockDelete(account *gtsmodel.Account, id string) (*ap  	domainBlock := >smodel.DomainBlock{}  	if err := p.db.GetByID(id, domainBlock); err != nil { -		if _, ok := err.(db.ErrNoEntries); !ok { +		if err != db.ErrNoEntries {  			// something has gone really wrong  			return nil, gtserror.NewErrorInternalError(err)  		} diff --git a/internal/processing/admin/getdomainblock.go b/internal/processing/admin/getdomainblock.go index 7d1f9e2ab..f74010627 100644 --- a/internal/processing/admin/getdomainblock.go +++ b/internal/processing/admin/getdomainblock.go @@ -31,7 +31,7 @@ func (p *processor) DomainBlockGet(account *gtsmodel.Account, id string, export  	domainBlock := >smodel.DomainBlock{}  	if err := p.db.GetByID(id, domainBlock); err != nil { -		if _, ok := err.(db.ErrNoEntries); !ok { +		if err != db.ErrNoEntries {  			// something has gone really wrong  			return nil, gtserror.NewErrorInternalError(err)  		} diff --git a/internal/processing/admin/getdomainblocks.go b/internal/processing/admin/getdomainblocks.go index 5e2241412..f827d03fc 100644 --- a/internal/processing/admin/getdomainblocks.go +++ b/internal/processing/admin/getdomainblocks.go @@ -29,7 +29,7 @@ func (p *processor) DomainBlocksGet(account *gtsmodel.Account, export bool) ([]*  	domainBlocks := []*gtsmodel.DomainBlock{}  	if err := p.db.GetAll(&domainBlocks); err != nil { -		if _, ok := err.(db.ErrNoEntries); !ok { +		if err != db.ErrNoEntries {  			// something has gone really wrong  			return nil, gtserror.NewErrorInternalError(err)  		} diff --git a/internal/processing/blocks.go b/internal/processing/blocks.go index 509600ca6..809cbde8e 100644 --- a/internal/processing/blocks.go +++ b/internal/processing/blocks.go @@ -29,9 +29,9 @@ import (  )  func (p *processor) BlocksGet(authed *oauth.Auth, maxID string, sinceID string, limit int) (*apimodel.BlocksResponse, gtserror.WithCode) { -	accounts, nextMaxID, prevMinID, err := p.db.GetBlocksForAccount(authed.Account.ID, maxID, sinceID, limit) +	accounts, nextMaxID, prevMinID, err := p.db.GetAccountBlocks(authed.Account.ID, maxID, sinceID, limit)  	if err != nil { -		if _, ok := err.(db.ErrNoEntries); ok { +		if err == db.ErrNoEntries {  			// there are just no entries  			return &apimodel.BlocksResponse{  				Accounts: []*apimodel.Account{}, diff --git a/internal/processing/federation.go b/internal/processing/federation.go index 765fdf862..cea14b4de 100644 --- a/internal/processing/federation.go +++ b/internal/processing/federation.go @@ -36,13 +36,12 @@ import (  func (p *processor) GetFediUser(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {  	// get the account the request is referring to -	requestedAccount := >smodel.Account{} -	if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { +	requestedAccount, err := p.db.GetLocalAccountByUsername(requestedUsername) +	if err != nil {  		return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))  	}  	var requestedPerson vocab.ActivityStreamsPerson -	var err error  	if util.IsPublicKeyPath(requestURL) {  		// if it's a public key path, we don't need to authenticate but we'll only serve the bare minimum user profile needed for the public key  		requestedPerson, err = p.tc.AccountToASMinimal(requestedAccount) @@ -63,7 +62,7 @@ func (p *processor) GetFediUser(ctx context.Context, requestedUsername string, r  				return nil, gtserror.NewErrorNotAuthorized(err)  			} -			blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID) +			blocked, err := p.db.IsBlocked(requestedAccount.ID, requestingAccount.ID, true)  			if err != nil {  				return nil, gtserror.NewErrorInternalError(err)  			} @@ -91,8 +90,8 @@ func (p *processor) GetFediUser(ctx context.Context, requestedUsername string, r  func (p *processor) GetFediFollowers(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {  	// get the account the request is referring to -	requestedAccount := >smodel.Account{} -	if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { +	requestedAccount, err := p.db.GetLocalAccountByUsername(requestedUsername) +	if err != nil {  		return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))  	} @@ -107,7 +106,7 @@ func (p *processor) GetFediFollowers(ctx context.Context, requestedUsername stri  		return nil, gtserror.NewErrorNotAuthorized(err)  	} -	blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID) +	blocked, err := p.db.IsBlocked(requestedAccount.ID, requestingAccount.ID, true)  	if err != nil {  		return nil, gtserror.NewErrorInternalError(err)  	} @@ -136,8 +135,8 @@ func (p *processor) GetFediFollowers(ctx context.Context, requestedUsername stri  func (p *processor) GetFediFollowing(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {  	// get the account the request is referring to -	requestedAccount := >smodel.Account{} -	if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { +	requestedAccount, err := p.db.GetLocalAccountByUsername(requestedUsername) +	if err != nil {  		return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))  	} @@ -152,7 +151,7 @@ func (p *processor) GetFediFollowing(ctx context.Context, requestedUsername stri  		return nil, gtserror.NewErrorNotAuthorized(err)  	} -	blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID) +	blocked, err := p.db.IsBlocked(requestedAccount.ID, requestingAccount.ID, true)  	if err != nil {  		return nil, gtserror.NewErrorInternalError(err)  	} @@ -181,8 +180,8 @@ func (p *processor) GetFediFollowing(ctx context.Context, requestedUsername stri  func (p *processor) GetFediStatus(ctx context.Context, requestedUsername string, requestedStatusID string, requestURL *url.URL) (interface{}, gtserror.WithCode) {  	// get the account the request is referring to -	requestedAccount := >smodel.Account{} -	if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { +	requestedAccount, err := p.db.GetLocalAccountByUsername(requestedUsername) +	if err != nil {  		return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))  	} @@ -199,7 +198,7 @@ func (p *processor) GetFediStatus(ctx context.Context, requestedUsername string,  	// authorize the request:  	// 1. check if a block exists between the requester and the requestee -	blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID) +	blocked, err := p.db.IsBlocked(requestedAccount.ID, requestingAccount.ID, true)  	if err != nil {  		return nil, gtserror.NewErrorInternalError(err)  	} @@ -241,8 +240,8 @@ func (p *processor) GetFediStatus(ctx context.Context, requestedUsername string,  func (p *processor) GetFediStatusReplies(ctx context.Context, requestedUsername string, requestedStatusID string, page bool, onlyOtherAccounts bool, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) {  	// get the account the request is referring to -	requestedAccount := >smodel.Account{} -	if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { +	requestedAccount, err := p.db.GetLocalAccountByUsername(requestedUsername) +	if err != nil {  		return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))  	} @@ -259,7 +258,7 @@ func (p *processor) GetFediStatusReplies(ctx context.Context, requestedUsername  	// authorize the request:  	// 1. check if a block exists between the requester and the requestee -	blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID) +	blocked, err := p.db.IsBlocked(requestedAccount.ID, requestingAccount.ID, true)  	if err != nil {  		return nil, gtserror.NewErrorInternalError(err)  	} @@ -321,7 +320,7 @@ func (p *processor) GetFediStatusReplies(ctx context.Context, requestedUsername  	} else {  		// scenario 3  		// get immediate children -		replies, err := p.db.StatusChildren(s, true, minID) +		replies, err := p.db.GetStatusChildren(s, true, minID)  		if err != nil {  			return nil, gtserror.NewErrorInternalError(err)  		} @@ -374,8 +373,8 @@ func (p *processor) GetFediStatusReplies(ctx context.Context, requestedUsername  func (p *processor) GetWebfingerAccount(ctx context.Context, requestedUsername string, requestURL *url.URL) (*apimodel.WellKnownResponse, gtserror.WithCode) {  	// get the account the request is referring to -	requestedAccount := >smodel.Account{} -	if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { +	requestedAccount, err := p.db.GetLocalAccountByUsername(requestedUsername) +	if err != nil {  		return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))  	} diff --git a/internal/processing/followrequest.go b/internal/processing/followrequest.go index 5eb9fd6ad..867725023 100644 --- a/internal/processing/followrequest.go +++ b/internal/processing/followrequest.go @@ -27,9 +27,9 @@ import (  )  func (p *processor) FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, gtserror.WithCode) { -	frs := []gtsmodel.FollowRequest{} -	if err := p.db.GetFollowRequestsForAccountID(auth.Account.ID, &frs); err != nil { -		if _, ok := err.(db.ErrNoEntries); !ok { +	frs, err := p.db.GetAccountFollowRequests(auth.Account.ID) +	if err != nil { +		if err != db.ErrNoEntries {  			return nil, gtserror.NewErrorInternalError(err)  		}  	} diff --git a/internal/processing/fromclientapi.go b/internal/processing/fromclientapi.go index 6755a9d82..beed283c1 100644 --- a/internal/processing/fromclientapi.go +++ b/internal/processing/fromclientapi.go @@ -187,19 +187,19 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error  				return errors.New("note was not parseable as *gtsmodel.Status")  			} -			if statusToDelete.GTSAuthorAccount == nil { -				statusToDelete.GTSAuthorAccount = clientMsg.OriginAccount +			if statusToDelete.Account == nil { +				statusToDelete.Account = clientMsg.OriginAccount  			}  			// delete all attachments for this status -			for _, a := range statusToDelete.Attachments { +			for _, a := range statusToDelete.AttachmentIDs {  				if err := p.mediaProcessor.Delete(a); err != nil {  					return err  				}  			}  			// delete all mentions for this status -			for _, m := range statusToDelete.Mentions { +			for _, m := range statusToDelete.MentionIDs {  				if err := p.db.DeleteByID(m, >smodel.Mention{}); err != nil {  					return err  				} @@ -237,16 +237,16 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error  // TODO: move all the below functions into federation.Federator  func (p *processor) federateStatus(status *gtsmodel.Status) error { -	if status.GTSAuthorAccount == nil { +	if status.Account == nil {  		a := >smodel.Account{}  		if err := p.db.GetByID(status.AccountID, a); err != nil {  			return fmt.Errorf("federateStatus: error fetching status author account: %s", err)  		} -		status.GTSAuthorAccount = a +		status.Account = a  	}  	// do nothing if this isn't our status -	if status.GTSAuthorAccount.Domain != "" { +	if status.Account.Domain != "" {  		return nil  	} @@ -255,9 +255,9 @@ func (p *processor) federateStatus(status *gtsmodel.Status) error {  		return fmt.Errorf("federateStatus: error converting status to as format: %s", err)  	} -	outboxIRI, err := url.Parse(status.GTSAuthorAccount.OutboxURI) +	outboxIRI, err := url.Parse(status.Account.OutboxURI)  	if err != nil { -		return fmt.Errorf("federateStatus: error parsing outboxURI %s: %s", status.GTSAuthorAccount.OutboxURI, err) +		return fmt.Errorf("federateStatus: error parsing outboxURI %s: %s", status.Account.OutboxURI, err)  	}  	_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, asStatus) @@ -265,16 +265,16 @@ func (p *processor) federateStatus(status *gtsmodel.Status) error {  }  func (p *processor) federateStatusDelete(status *gtsmodel.Status) error { -	if status.GTSAuthorAccount == nil { +	if status.Account == nil {  		a := >smodel.Account{}  		if err := p.db.GetByID(status.AccountID, a); err != nil {  			return fmt.Errorf("federateStatus: error fetching status author account: %s", err)  		} -		status.GTSAuthorAccount = a +		status.Account = a  	}  	// do nothing if this isn't our status -	if status.GTSAuthorAccount.Domain != "" { +	if status.Account.Domain != "" {  		return nil  	} @@ -283,14 +283,14 @@ func (p *processor) federateStatusDelete(status *gtsmodel.Status) error {  		return fmt.Errorf("federateStatusDelete: error converting status to as format: %s", err)  	} -	outboxIRI, err := url.Parse(status.GTSAuthorAccount.OutboxURI) +	outboxIRI, err := url.Parse(status.Account.OutboxURI)  	if err != nil { -		return fmt.Errorf("federateStatusDelete: error parsing outboxURI %s: %s", status.GTSAuthorAccount.OutboxURI, err) +		return fmt.Errorf("federateStatusDelete: error parsing outboxURI %s: %s", status.Account.OutboxURI, err)  	} -	actorIRI, err := url.Parse(status.GTSAuthorAccount.URI) +	actorIRI, err := url.Parse(status.Account.URI)  	if err != nil { -		return fmt.Errorf("federateStatusDelete: error parsing actorIRI %s: %s", status.GTSAuthorAccount.URI, err) +		return fmt.Errorf("federateStatusDelete: error parsing actorIRI %s: %s", status.Account.URI, err)  	}  	// create a delete and set the appropriate actor on it diff --git a/internal/processing/fromcommon.go b/internal/processing/fromcommon.go index d719b7f5f..2c2635175 100644 --- a/internal/processing/fromcommon.go +++ b/internal/processing/fromcommon.go @@ -30,35 +30,31 @@ import (  func (p *processor) notifyStatus(status *gtsmodel.Status) error {  	// if there are no mentions in this status then just bail -	if len(status.Mentions) == 0 { +	if len(status.MentionIDs) == 0 {  		return nil  	} -	if status.GTSMentions == nil { +	if status.Mentions == nil {  		// there are mentions but they're not fully populated on the status yet so do this -		menchies := []*gtsmodel.Mention{} -		for _, m := range status.Mentions { -			gtsm := >smodel.Mention{} -			if err := p.db.GetByID(m, gtsm); err != nil { -				return fmt.Errorf("notifyStatus: error getting mention with id %s from the db: %s", m, err) -			} -			menchies = append(menchies, gtsm) +		menchies, err := p.db.GetMentions(status.MentionIDs) +		if err != nil { +			return fmt.Errorf("notifyStatus: error getting mentions for status %s from the db: %s", status.ID, err)  		} -		status.GTSMentions = menchies +		status.Mentions = menchies  	}  	// now we have mentions as full gtsmodel.Mention structs on the status we can continue -	for _, m := range status.GTSMentions { +	for _, m := range status.Mentions {  		// make sure this is a local account, otherwise we don't need to create a notification for it -		if m.GTSAccount == nil { -			a := >smodel.Account{} -			if err := p.db.GetByID(m.TargetAccountID, a); err != nil { +		if m.TargetAccount == nil { +			a, err := p.db.GetAccountByID(m.TargetAccountID) +			if err != nil {  				// we don't have the account or there's been an error  				return fmt.Errorf("notifyStatus: error getting account with id %s from the db: %s", m.TargetAccountID, err)  			} -			m.GTSAccount = a +			m.TargetAccount = a  		} -		if m.GTSAccount.Domain != "" { +		if m.TargetAccount.Domain != "" {  			// not a local account so skip it  			continue  		} @@ -74,7 +70,7 @@ func (p *processor) notifyStatus(status *gtsmodel.Status) error {  			// notification exists already so just continue  			continue  		} -		if _, ok := err.(db.ErrNoEntries); !ok { +		if err != db.ErrNoEntries {  			// there's a real error in the db  			return fmt.Errorf("notifyStatus: error checking existence of notification for mention with id %s : %s", m.ID, err)  		} @@ -89,8 +85,11 @@ func (p *processor) notifyStatus(status *gtsmodel.Status) error {  			ID:               notifID,  			NotificationType: gtsmodel.NotificationMention,  			TargetAccountID:  m.TargetAccountID, +			TargetAccount:    m.TargetAccount,  			OriginAccountID:  status.AccountID, +			OriginAccount:    status.Account,  			StatusID:         status.ID, +			Status:           status,  		}  		if err := p.db.Put(notif); err != nil { @@ -103,7 +102,7 @@ func (p *processor) notifyStatus(status *gtsmodel.Status) error {  			return fmt.Errorf("notifyStatus: error converting notification to masto representation: %s", err)  		} -		if err := p.streamingProcessor.StreamNotificationToAccount(mastoNotif, m.GTSAccount); err != nil { +		if err := p.streamingProcessor.StreamNotificationToAccount(mastoNotif, m.TargetAccount); err != nil {  			return fmt.Errorf("notifyStatus: error streaming notification to account: %s", err)  		}  	} @@ -146,9 +145,9 @@ func (p *processor) notifyFollowRequest(followRequest *gtsmodel.FollowRequest, r  	return nil  } -func (p *processor) notifyFollow(follow *gtsmodel.Follow, receivingAccount *gtsmodel.Account) error { +func (p *processor) notifyFollow(follow *gtsmodel.Follow, targetAccount *gtsmodel.Account) error {  	// return if this isn't a local account -	if receivingAccount.Domain != "" { +	if targetAccount.Domain != "" {  		return nil  	} @@ -171,7 +170,9 @@ func (p *processor) notifyFollow(follow *gtsmodel.Follow, receivingAccount *gtsm  		ID:               notifID,  		NotificationType: gtsmodel.NotificationFollow,  		TargetAccountID:  follow.TargetAccountID, +		TargetAccount:    follow.TargetAccount,  		OriginAccountID:  follow.AccountID, +		OriginAccount:    follow.Account,  	}  	if err := p.db.Put(notif); err != nil {  		return fmt.Errorf("notifyFollow: error putting notification in database: %s", err) @@ -183,16 +184,16 @@ func (p *processor) notifyFollow(follow *gtsmodel.Follow, receivingAccount *gtsm  		return fmt.Errorf("notifyStatus: error converting notification to masto representation: %s", err)  	} -	if err := p.streamingProcessor.StreamNotificationToAccount(mastoNotif, receivingAccount); err != nil { +	if err := p.streamingProcessor.StreamNotificationToAccount(mastoNotif, targetAccount); err != nil {  		return fmt.Errorf("notifyStatus: error streaming notification to account: %s", err)  	}  	return nil  } -func (p *processor) notifyFave(fave *gtsmodel.StatusFave, receivingAccount *gtsmodel.Account) error { +func (p *processor) notifyFave(fave *gtsmodel.StatusFave, targetAccount *gtsmodel.Account) error {  	// return if this isn't a local account -	if receivingAccount.Domain != "" { +	if targetAccount.Domain != "" {  		return nil  	} @@ -205,8 +206,11 @@ func (p *processor) notifyFave(fave *gtsmodel.StatusFave, receivingAccount *gtsm  		ID:               notifID,  		NotificationType: gtsmodel.NotificationFave,  		TargetAccountID:  fave.TargetAccountID, +		TargetAccount:    fave.TargetAccount,  		OriginAccountID:  fave.AccountID, +		OriginAccount:    fave.Account,  		StatusID:         fave.StatusID, +		Status:           fave.Status,  	}  	if err := p.db.Put(notif); err != nil { @@ -219,7 +223,7 @@ func (p *processor) notifyFave(fave *gtsmodel.StatusFave, receivingAccount *gtsm  		return fmt.Errorf("notifyStatus: error converting notification to masto representation: %s", err)  	} -	if err := p.streamingProcessor.StreamNotificationToAccount(mastoNotif, receivingAccount); err != nil { +	if err := p.streamingProcessor.StreamNotificationToAccount(mastoNotif, targetAccount); err != nil {  		return fmt.Errorf("notifyStatus: error streaming notification to account: %s", err)  	} @@ -232,22 +236,29 @@ func (p *processor) notifyAnnounce(status *gtsmodel.Status) error {  		return nil  	} -	boostedStatus := >smodel.Status{} -	if err := p.db.GetByID(status.BoostOfID, boostedStatus); err != nil { -		return fmt.Errorf("notifyAnnounce: error getting status with id %s: %s", status.BoostOfID, err) +	if status.BoostOf == nil { +		boostedStatus, err := p.db.GetStatusByID(status.BoostOfID) +		if err != nil { +			return fmt.Errorf("notifyAnnounce: error getting status with id %s: %s", status.BoostOfID, err) +		} +		status.BoostOf = boostedStatus  	} -	boostedAcct := >smodel.Account{} -	if err := p.db.GetByID(boostedStatus.AccountID, boostedAcct); err != nil { -		return fmt.Errorf("notifyAnnounce: error getting account with id %s: %s", boostedStatus.AccountID, err) +	if status.BoostOfAccount == nil { +		boostedAcct, err := p.db.GetAccountByID(status.BoostOfAccountID) +		if err != nil { +			return fmt.Errorf("notifyAnnounce: error getting account with id %s: %s", status.BoostOfAccountID, err) +		} +		status.BoostOf.Account = boostedAcct +		status.BoostOfAccount = boostedAcct  	} -	if boostedAcct.Domain != "" { +	if status.BoostOfAccount.Domain == "" {  		// remote account, nothing to do  		return nil  	} -	if boostedStatus.AccountID == status.AccountID { +	if status.BoostOfAccountID == status.AccountID {  		// it's a self boost, nothing to do  		return nil  	} @@ -255,7 +266,7 @@ func (p *processor) notifyAnnounce(status *gtsmodel.Status) error {  	// make sure a notif doesn't already exist for this announce  	err := p.db.GetWhere([]db.Where{  		{Key: "notification_type", Value: gtsmodel.NotificationReblog}, -		{Key: "target_account_id", Value: boostedAcct.ID}, +		{Key: "target_account_id", Value: status.BoostOfAccountID},  		{Key: "origin_account_id", Value: status.AccountID},  		{Key: "status_id", Value: status.ID},  	}, >smodel.Notification{}) @@ -273,9 +284,12 @@ func (p *processor) notifyAnnounce(status *gtsmodel.Status) error {  	notif := >smodel.Notification{  		ID:               notifID,  		NotificationType: gtsmodel.NotificationReblog, -		TargetAccountID:  boostedAcct.ID, +		TargetAccountID:  status.BoostOfAccountID, +		TargetAccount:    status.BoostOfAccount,  		OriginAccountID:  status.AccountID, +		OriginAccount:    status.Account,  		StatusID:         status.ID, +		Status:           status,  	}  	if err := p.db.Put(notif); err != nil { @@ -288,7 +302,7 @@ func (p *processor) notifyAnnounce(status *gtsmodel.Status) error {  		return fmt.Errorf("notifyStatus: error converting notification to masto representation: %s", err)  	} -	if err := p.streamingProcessor.StreamNotificationToAccount(mastoNotif, boostedAcct); err != nil { +	if err := p.streamingProcessor.StreamNotificationToAccount(mastoNotif, status.BoostOfAccount); err != nil {  		return fmt.Errorf("notifyStatus: error streaming notification to account: %s", err)  	} @@ -297,32 +311,33 @@ func (p *processor) notifyAnnounce(status *gtsmodel.Status) error {  func (p *processor) timelineStatus(status *gtsmodel.Status) error {  	// make sure the author account is pinned onto the status -	if status.GTSAuthorAccount == nil { -		a := >smodel.Account{} -		if err := p.db.GetByID(status.AccountID, a); err != nil { +	if status.Account == nil { +		a, err := p.db.GetAccountByID(status.AccountID) +		if err != nil {  			return fmt.Errorf("timelineStatus: error getting author account with id %s: %s", status.AccountID, err)  		} -		status.GTSAuthorAccount = a +		status.Account = a  	}  	// get local followers of the account that posted the status -	followers := []gtsmodel.Follow{} -	if err := p.db.GetFollowersByAccountID(status.AccountID, &followers, true); err != nil { +	follows, err := p.db.GetAccountFollowedBy(status.AccountID, true) +	if err != nil {  		return fmt.Errorf("timelineStatus: error getting followers for account id %s: %s", status.AccountID, err)  	}  	// if the poster is local, add a fake entry for them to the followers list so they can see their own status in their timeline -	if status.GTSAuthorAccount.Domain == "" { -		followers = append(followers, gtsmodel.Follow{ +	if status.Account.Domain == "" { +		follows = append(follows, >smodel.Follow{  			AccountID: status.AccountID, +			Account:   status.Account,  		})  	}  	wg := sync.WaitGroup{} -	wg.Add(len(followers)) -	errors := make(chan error, len(followers)) +	wg.Add(len(follows)) +	errors := make(chan error, len(follows)) -	for _, f := range followers { +	for _, f := range follows {  		go p.timelineStatusForAccount(status, f.AccountID, errors, &wg)  	} @@ -354,8 +369,8 @@ func (p *processor) timelineStatusForAccount(status *gtsmodel.Status, accountID  	defer wg.Done()  	// get the timeline owner account -	timelineAccount := >smodel.Account{} -	if err := p.db.GetByID(accountID, timelineAccount); err != nil { +	timelineAccount, err := p.db.GetAccountByID(accountID) +	if err != nil {  		errors <- fmt.Errorf("timelineStatusForAccount: error getting account for timeline with id %s: %s", accountID, err)  		return  	} diff --git a/internal/processing/fromfederator.go b/internal/processing/fromfederator.go index 949a734c7..c95c27778 100644 --- a/internal/processing/fromfederator.go +++ b/internal/processing/fromfederator.go @@ -100,8 +100,8 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er  			}  			incomingAnnounce.ID = incomingAnnounceID -			if err := p.db.Put(incomingAnnounce); err != nil { -				if _, ok := err.(db.ErrAlreadyExists); !ok { +			if err := p.db.PutStatus(incomingAnnounce); err != nil { +				if err != db.ErrNoEntries {  					return fmt.Errorf("error adding dereferenced announce to the db: %s", err)  				}  			} @@ -164,14 +164,14 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er  			}  			// delete all attachments for this status -			for _, a := range statusToDelete.Attachments { +			for _, a := range statusToDelete.AttachmentIDs {  				if err := p.mediaProcessor.Delete(a); err != nil {  					return err  				}  			}  			// delete all mentions for this status -			for _, m := range statusToDelete.Mentions { +			for _, m := range statusToDelete.MentionIDs {  				if err := p.db.DeleteByID(m, >smodel.Mention{}); err != nil {  					return err  				} diff --git a/internal/processing/instance.go b/internal/processing/instance.go index 89f60f5d4..b151744ef 100644 --- a/internal/processing/instance.go +++ b/internal/processing/instance.go @@ -51,8 +51,8 @@ func (p *processor) InstancePatch(form *apimodel.InstanceSettingsUpdateRequest)  	}  	// fetch the instance account from the db for processing -	ia := >smodel.Account{} -	if err := p.db.GetLocalAccountByUsername(p.config.Host, ia); err != nil { +	ia, err := p.db.GetInstanceAccount("") +	if err != nil {  		return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error fetching instance account %s: %s", p.config.Host, err))  	} @@ -67,8 +67,8 @@ func (p *processor) InstancePatch(form *apimodel.InstanceSettingsUpdateRequest)  	// validate & update site contact account if it's set on the form  	if form.ContactUsername != nil {  		// make sure the account with the given username exists in the db -		contactAccount := >smodel.Account{} -		if err := p.db.GetLocalAccountByUsername(*form.ContactUsername, contactAccount); err != nil { +		contactAccount, err := p.db.GetLocalAccountByUsername(*form.ContactUsername) +		if err != nil {  			return nil, gtserror.NewErrorBadRequest(err, fmt.Sprintf("account with username %s not retrievable", *form.ContactUsername))  		}  		// make sure it has a user associated with it diff --git a/internal/processing/media/delete.go b/internal/processing/media/delete.go index 694d78ac3..b5ea8c806 100644 --- a/internal/processing/media/delete.go +++ b/internal/processing/media/delete.go @@ -12,7 +12,7 @@ import (  func (p *processor) Delete(mediaAttachmentID string) gtserror.WithCode {  	a := >smodel.MediaAttachment{}  	if err := p.db.GetByID(mediaAttachmentID, a); err != nil { -		if _, ok := err.(db.ErrNoEntries); ok { +		if err == db.ErrNoEntries {  			// attachment already gone  			return nil  		} @@ -38,7 +38,7 @@ func (p *processor) Delete(mediaAttachmentID string) gtserror.WithCode {  	// delete the attachment  	if err := p.db.DeleteByID(mediaAttachmentID, a); err != nil { -		if _, ok := err.(db.ErrNoEntries); !ok { +		if err != db.ErrNoEntries {  			errs = append(errs, fmt.Sprintf("remove attachment: %s", err))  		}  	} diff --git a/internal/processing/media/getfile.go b/internal/processing/media/getfile.go index 1664306b8..01288c56d 100644 --- a/internal/processing/media/getfile.go +++ b/internal/processing/media/getfile.go @@ -57,7 +57,7 @@ func (p *processor) GetFile(account *gtsmodel.Account, form *apimodel.GetContent  	// make sure the requesting account and the media account don't block each other  	if account != nil { -		blocked, err := p.db.Blocked(account.ID, form.AccountID) +		blocked, err := p.db.IsBlocked(account.ID, form.AccountID, true)  		if err != nil {  			return nil, gtserror.NewErrorNotFound(fmt.Errorf("block status could not be established between accounts %s and %s: %s", form.AccountID, account.ID, err))  		} diff --git a/internal/processing/media/getmedia.go b/internal/processing/media/getmedia.go index c36370225..380a54cc2 100644 --- a/internal/processing/media/getmedia.go +++ b/internal/processing/media/getmedia.go @@ -31,7 +31,7 @@ import (  func (p *processor) GetMedia(account *gtsmodel.Account, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) {  	attachment := >smodel.MediaAttachment{}  	if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil { -		if _, ok := err.(db.ErrNoEntries); ok { +		if err == db.ErrNoEntries {  			// attachment doesn't exist  			return nil, gtserror.NewErrorNotFound(errors.New("attachment doesn't exist in the db"))  		} diff --git a/internal/processing/media/update.go b/internal/processing/media/update.go index 28f3a26f6..89ed08ac1 100644 --- a/internal/processing/media/update.go +++ b/internal/processing/media/update.go @@ -32,7 +32,7 @@ import (  func (p *processor) Update(account *gtsmodel.Account, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) {  	attachment := >smodel.MediaAttachment{}  	if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil { -		if _, ok := err.(db.ErrNoEntries); ok { +		if err == db.ErrNoEntries {  			// attachment doesn't exist  			return nil, gtserror.NewErrorNotFound(errors.New("attachment doesn't exist in the db"))  		} diff --git a/internal/processing/notification.go b/internal/processing/notification.go index 6ad974126..7af74b04f 100644 --- a/internal/processing/notification.go +++ b/internal/processing/notification.go @@ -27,7 +27,7 @@ import (  func (p *processor) NotificationsGet(authed *oauth.Auth, limit int, maxID string, sinceID string) ([]*apimodel.Notification, gtserror.WithCode) {  	l := p.log.WithField("func", "NotificationsGet") -	notifs, err := p.db.GetNotificationsForAccount(authed.Account.ID, limit, maxID, sinceID) +	notifs, err := p.db.GetNotifications(authed.Account.ID, limit, maxID, sinceID)  	if err != nil {  		return nil, gtserror.NewErrorInternalError(err)  	} diff --git a/internal/processing/search.go b/internal/processing/search.go index 737ad8f71..f2ae721ae 100644 --- a/internal/processing/search.go +++ b/internal/processing/search.go @@ -90,7 +90,7 @@ func (p *processor) SearchGet(authed *oauth.Auth, searchQuery *apimodel.SearchQu  	*/  	for _, foundAccount := range foundAccounts {  		// make sure there's no block in either direction between the account and the requester -		if blocked, err := p.db.Blocked(authed.Account.ID, foundAccount.ID); err == nil && !blocked { +		if blocked, err := p.db.IsBlocked(authed.Account.ID, foundAccount.ID, true); err == nil && !blocked {  			// all good, convert it and add it to the results  			if acctMasto, err := p.tc.AccountToMastoPublic(foundAccount); err == nil && acctMasto != nil {  				results.Accounts = append(results.Accounts, *acctMasto) @@ -99,11 +99,6 @@ func (p *processor) SearchGet(authed *oauth.Auth, searchQuery *apimodel.SearchQu  	}  	for _, foundStatus := range foundStatuses { -		statusOwner := >smodel.Account{} -		if err := p.db.GetByID(foundStatus.AccountID, statusOwner); err != nil { -			continue -		} -  		if visible, err := p.filter.StatusVisible(foundStatus, authed.Account); !visible || err != nil {  			continue  		} @@ -126,12 +121,9 @@ func (p *processor) searchStatusByURI(authed *oauth.Auth, uri *url.URL, resolve  		"resolve": resolve,  	}) -	maybeStatus := >smodel.Status{} -	if err := p.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String(), CaseInsensitive: true}}, maybeStatus); err == nil { -		// we have it and it's a status +	if maybeStatus, err := p.db.GetStatusByURI(uri.String()); err == nil {  		return maybeStatus, nil -	} else if err := p.db.GetWhere([]db.Where{{Key: "url", Value: uri.String(), CaseInsensitive: true}}, maybeStatus); err == nil { -		// we have it and it's a status +	} else if maybeStatus, err := p.db.GetStatusByURL(uri.String()); err == nil {  		return maybeStatus, nil  	} @@ -150,14 +142,12 @@ func (p *processor) searchStatusByURI(authed *oauth.Auth, uri *url.URL, resolve  }  func (p *processor) searchAccountByURI(authed *oauth.Auth, uri *url.URL, resolve bool) (*gtsmodel.Account, error) { -	maybeAccount := >smodel.Account{} -	if err := p.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String(), CaseInsensitive: true}}, maybeAccount); err == nil { -		// we have it and it's an account +	if maybeAccount, err := p.db.GetAccountByURI(uri.String()); err == nil {  		return maybeAccount, nil -	} else if err = p.db.GetWhere([]db.Where{{Key: "url", Value: uri.String(), CaseInsensitive: true}}, maybeAccount); err == nil { -		// we have it and it's an account +	} else if maybeAccount, err := p.db.GetAccountByURL(uri.String()); err == nil {  		return maybeAccount, nil  	} +  	if resolve {  		// we don't have it locally so try and dereference it  		account, _, err := p.federator.GetRemoteAccount(authed.Account.Username, uri, true) @@ -179,7 +169,8 @@ func (p *processor) searchAccountByMention(authed *oauth.Auth, mention string, r  	// if it's a local account we can skip a whole bunch of stuff  	maybeAcct := >smodel.Account{}  	if domain == p.config.Host { -		if err = p.db.GetLocalAccountByUsername(username, maybeAcct); err != nil { +		maybeAcct, err = p.db.GetLocalAccountByUsername(username) +		if err != nil {  			return nil, fmt.Errorf("searchAccountByMention: error getting local account by username: %s", err)  		}  		return maybeAcct, nil @@ -196,7 +187,7 @@ func (p *processor) searchAccountByMention(authed *oauth.Auth, mention string, r  		return maybeAcct, nil  	} -	if _, ok := err.(db.ErrNoEntries); !ok { +	if err != db.ErrNoEntries {  		// if it's  not errNoEntries there's been a real database error so bail at this point  		return nil, fmt.Errorf("searchAccountByMention: database error: %s", err)  	} diff --git a/internal/processing/status/boost.go b/internal/processing/status/boost.go index 93d0f19de..d7a62beb1 100644 --- a/internal/processing/status/boost.go +++ b/internal/processing/status/boost.go @@ -9,31 +9,22 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  ) -func (p *processor) Boost(account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { -	l := p.log.WithField("func", "StatusBoost") - -	l.Tracef("going to search for target status %s", targetStatusID) -	targetStatus := >smodel.Status{} -	if err := p.db.GetByID(targetStatusID, targetStatus); err != nil { +func (p *processor) Boost(requestingAccount *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { +	targetStatus, err := p.db.GetStatusByID(targetStatusID) +	if err != nil {  		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))  	} - -	l.Tracef("going to search for target account %s", targetStatus.AccountID) -	targetAccount := >smodel.Account{} -	if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil { -		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)) +	if targetStatus.Account == nil { +		return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))  	} -	l.Trace("going to see if status is visible") -	visible, err := p.filter.StatusVisible(targetStatus, account) +	visible, err := p.filter.StatusVisible(targetStatus, requestingAccount)  	if err != nil {  		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))  	} -  	if !visible {  		return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))  	} -  	if targetStatus.VisibilityAdvanced != nil {  		if !targetStatus.VisibilityAdvanced.Boostable {  			return nil, gtserror.NewErrorForbidden(errors.New("status is not boostable")) @@ -41,16 +32,16 @@ func (p *processor) Boost(account *gtsmodel.Account, application *gtsmodel.Appli  	}  	// it's visible! it's boostable! so let's boost the FUCK out of it -	boostWrapperStatus, err := p.tc.StatusToBoost(targetStatus, account) +	boostWrapperStatus, err := p.tc.StatusToBoost(targetStatus, requestingAccount)  	if err != nil {  		return nil, gtserror.NewErrorInternalError(err)  	}  	boostWrapperStatus.CreatedWithApplicationID = application.ID -	boostWrapperStatus.GTSBoostedAccount = targetAccount +	boostWrapperStatus.BoostOfAccount = targetStatus.Account  	// put the boost in the database -	if err := p.db.Put(boostWrapperStatus); err != nil { +	if err := p.db.PutStatus(boostWrapperStatus); err != nil {  		return nil, gtserror.NewErrorInternalError(err)  	} @@ -59,12 +50,12 @@ func (p *processor) Boost(account *gtsmodel.Account, application *gtsmodel.Appli  		APObjectType:   gtsmodel.ActivityStreamsAnnounce,  		APActivityType: gtsmodel.ActivityStreamsCreate,  		GTSModel:       boostWrapperStatus, -		OriginAccount:  account, -		TargetAccount:  targetAccount, +		OriginAccount:  requestingAccount, +		TargetAccount:  targetStatus.Account,  	}  	// return the frontend representation of the new status to the submitter -	mastoStatus, err := p.tc.StatusToMasto(boostWrapperStatus, account) +	mastoStatus, err := p.tc.StatusToMasto(boostWrapperStatus, requestingAccount)  	if err != nil {  		return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))  	} diff --git a/internal/processing/status/boostedby.go b/internal/processing/status/boostedby.go index b352178e3..1bde6b5ae 100644 --- a/internal/processing/status/boostedby.go +++ b/internal/processing/status/boostedby.go @@ -9,46 +9,37 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  ) -func (p *processor) BoostedBy(account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) { -	l := p.log.WithField("func", "StatusBoostedBy") - -	l.Tracef("going to search for target status %s", targetStatusID) -	targetStatus := >smodel.Status{} -	if err := p.db.GetByID(targetStatusID, targetStatus); err != nil { -		return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error fetching status %s: %s", targetStatusID, err)) +func (p *processor) BoostedBy(requestingAccount *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) { +	targetStatus, err := p.db.GetStatusByID(targetStatusID) +	if err != nil { +		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))  	} - -	l.Tracef("going to search for target account %s", targetStatus.AccountID) -	targetAccount := >smodel.Account{} -	if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil { -		return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error fetching target account %s: %s", targetStatus.AccountID, err)) +	if targetStatus.Account == nil { +		return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))  	} -	l.Trace("going to see if status is visible") -	visible, err := p.filter.StatusVisible(targetStatus, account) +	visible, err := p.filter.StatusVisible(targetStatus, requestingAccount)  	if err != nil { -		return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error seeing if status %s is visible: %s", targetStatus.ID, err)) +		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))  	} -  	if !visible { -		return nil, gtserror.NewErrorNotFound(errors.New("StatusBoostedBy: status is not visible")) +		return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))  	} -	// get ALL accounts that faved a status -- doesn't take account of blocks and mutes and stuff -	favingAccounts, err := p.db.WhoBoostedStatus(targetStatus) +	statusReblogs, err := p.db.GetStatusReblogs(targetStatus)  	if err != nil {  		return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error seeing who boosted status: %s", err))  	}  	// filter the list so the user doesn't see accounts they blocked or which blocked them  	filteredAccounts := []*gtsmodel.Account{} -	for _, acc := range favingAccounts { -		blocked, err := p.db.Blocked(account.ID, acc.ID) +	for _, s := range statusReblogs { +		blocked, err := p.db.IsBlocked(requestingAccount.ID, s.AccountID, true)  		if err != nil {  			return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error checking blocks: %s", err))  		}  		if !blocked { -			filteredAccounts = append(filteredAccounts, acc) +			filteredAccounts = append(filteredAccounts, s.Account)  		}  	} diff --git a/internal/processing/status/context.go b/internal/processing/status/context.go index 32c528296..43002545e 100644 --- a/internal/processing/status/context.go +++ b/internal/processing/status/context.go @@ -1,46 +1,45 @@  package status  import ( +	"errors"  	"fmt"  	"sort"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" -	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  ) -func (p *processor) Context(account *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode) { - -	context := &apimodel.Context{ -		Ancestors:   []apimodel.Status{}, -		Descendants: []apimodel.Status{}, +func (p *processor) Context(requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode) { +	targetStatus, err := p.db.GetStatusByID(targetStatusID) +	if err != nil { +		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))  	} - -	targetStatus := >smodel.Status{} -	if err := p.db.GetByID(targetStatusID, targetStatus); err != nil { -		if _, ok := err.(db.ErrNoEntries); ok { -			return nil, gtserror.NewErrorNotFound(err) -		} -		return nil, gtserror.NewErrorInternalError(err) +	if targetStatus.Account == nil { +		return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))  	} -	visible, err := p.filter.StatusVisible(targetStatus, account) +	visible, err := p.filter.StatusVisible(targetStatus, requestingAccount)  	if err != nil { -		return nil, gtserror.NewErrorNotFound(err) +		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))  	}  	if !visible { -		return nil, gtserror.NewErrorForbidden(fmt.Errorf("account with id %s does not have permission to view status %s", account.ID, targetStatusID)) +		return nil, gtserror.NewErrorNotFound(errors.New("status is not visible")) +	} + +	context := &apimodel.Context{ +		Ancestors:   []apimodel.Status{}, +		Descendants: []apimodel.Status{},  	} -	parents, err := p.db.StatusParents(targetStatus, false) +	parents, err := p.db.GetStatusParents(targetStatus, false)  	if err != nil {  		return nil, gtserror.NewErrorInternalError(err)  	}  	for _, status := range parents { -		if v, err := p.filter.StatusVisible(status, account); err == nil && v { -			mastoStatus, err := p.tc.StatusToMasto(status, account) +		if v, err := p.filter.StatusVisible(status, requestingAccount); err == nil && v { +			mastoStatus, err := p.tc.StatusToMasto(status, requestingAccount)  			if err == nil {  				context.Ancestors = append(context.Ancestors, *mastoStatus)  			} @@ -51,14 +50,14 @@ func (p *processor) Context(account *gtsmodel.Account, targetStatusID string) (*  		return context.Ancestors[i].ID < context.Ancestors[j].ID  	}) -	children, err := p.db.StatusChildren(targetStatus, false, "") +	children, err := p.db.GetStatusChildren(targetStatus, false, "")  	if err != nil {  		return nil, gtserror.NewErrorInternalError(err)  	}  	for _, status := range children { -		if v, err := p.filter.StatusVisible(status, account); err == nil && v { -			mastoStatus, err := p.tc.StatusToMasto(status, account) +		if v, err := p.filter.StatusVisible(status, requestingAccount); err == nil && v { +			mastoStatus, err := p.tc.StatusToMasto(status, requestingAccount)  			if err == nil {  				context.Descendants = append(context.Descendants, *mastoStatus)  			} diff --git a/internal/processing/status/create.go b/internal/processing/status/create.go index 0e99b5f4a..fc112ed8b 100644 --- a/internal/processing/status/create.go +++ b/internal/processing/status/create.go @@ -38,27 +38,22 @@ func (p *processor) Create(account *gtsmodel.Account, application *gtsmodel.Appl  		Text:                     form.Status,  	} -	// check if replyToID is ok  	if err := p.ProcessReplyToID(form, account.ID, newStatus); err != nil {  		return nil, gtserror.NewErrorInternalError(err)  	} -	// check if mediaIDs are ok  	if err := p.ProcessMediaIDs(form, account.ID, newStatus); err != nil {  		return nil, gtserror.NewErrorInternalError(err)  	} -	// check if visibility settings are ok  	if err := p.ProcessVisibility(form, account.Privacy, newStatus); err != nil {  		return nil, gtserror.NewErrorInternalError(err)  	} -	// handle language settings  	if err := p.ProcessLanguage(form, account.Language, newStatus); err != nil {  		return nil, gtserror.NewErrorInternalError(err)  	} -	// handle mentions  	if err := p.ProcessMentions(form, account.ID, newStatus); err != nil {  		return nil, gtserror.NewErrorInternalError(err)  	} @@ -75,20 +70,11 @@ func (p *processor) Create(account *gtsmodel.Account, application *gtsmodel.Appl  		return nil, gtserror.NewErrorInternalError(err)  	} -	// put the new status in the database, generating an ID for it in the process -	if err := p.db.Put(newStatus); err != nil { +	// put the new status in the database +	if err := p.db.PutStatus(newStatus); err != nil {  		return nil, gtserror.NewErrorInternalError(err)  	} -	// change the status ID of the media attachments to the new status -	for _, a := range newStatus.GTSMediaAttachments { -		a.StatusID = newStatus.ID -		a.UpdatedAt = time.Now() -		if err := p.db.UpdateByID(a.ID, a); err != nil { -			return nil, gtserror.NewErrorInternalError(err) -		} -	} -  	// send it back to the processor for async processing  	p.fromClientAPI <- gtsmodel.FromClientAPI{  		APObjectType:   gtsmodel.ActivityStreamsNote, diff --git a/internal/processing/status/delete.go b/internal/processing/status/delete.go index 259038dee..4c5dfd744 100644 --- a/internal/processing/status/delete.go +++ b/internal/processing/status/delete.go @@ -5,36 +5,24 @@ import (  	"fmt"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" -	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  ) -func (p *processor) Delete(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { -	l := p.log.WithField("func", "StatusDelete") -	l.Tracef("going to search for target status %s", targetStatusID) -	targetStatus := >smodel.Status{} -	if err := p.db.GetByID(targetStatusID, targetStatus); err != nil { -		if _, ok := err.(db.ErrNoEntries); !ok { -			return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err)) -		} -		// status is already gone -		return nil, nil +func (p *processor) Delete(requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { +	targetStatus, err := p.db.GetStatusByID(targetStatusID) +	if err != nil { +		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))  	} - -	if targetStatus.AccountID != account.ID { -		return nil, gtserror.NewErrorForbidden(errors.New("status doesn't belong to requesting account")) +	if targetStatus.Account == nil { +		return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))  	} -	var boostOfStatus *gtsmodel.Status -	if targetStatus.BoostOfID != "" { -		boostOfStatus = >smodel.Status{} -		if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil { -			return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)) -		} +	if targetStatus.AccountID != requestingAccount.ID { +		return nil, gtserror.NewErrorForbidden(errors.New("status doesn't belong to requesting account"))  	} -	mastoStatus, err := p.tc.StatusToMasto(targetStatus, account) +	mastoStatus, err := p.tc.StatusToMasto(targetStatus, requestingAccount)  	if err != nil {  		return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))  	} @@ -48,8 +36,8 @@ func (p *processor) Delete(account *gtsmodel.Account, targetStatusID string) (*a  		APObjectType:   gtsmodel.ActivityStreamsNote,  		APActivityType: gtsmodel.ActivityStreamsDelete,  		GTSModel:       targetStatus, -		OriginAccount:  account, -		TargetAccount:  account, +		OriginAccount:  requestingAccount, +		TargetAccount:  requestingAccount,  	}  	return mastoStatus, nil diff --git a/internal/processing/status/fave.go b/internal/processing/status/fave.go index 0dfee6233..7ba8c8fe8 100644 --- a/internal/processing/status/fave.go +++ b/internal/processing/status/fave.go @@ -12,39 +12,22 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/util"  ) -func (p *processor) Fave(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { -	l := p.log.WithField("func", "StatusFave") -	l.Tracef("going to search for target status %s", targetStatusID) -	targetStatus := >smodel.Status{} -	if err := p.db.GetByID(targetStatusID, targetStatus); err != nil { +func (p *processor) Fave(requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { +	targetStatus, err := p.db.GetStatusByID(targetStatusID) +	if err != nil {  		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))  	} - -	l.Tracef("going to search for target account %s", targetStatus.AccountID) -	targetAccount := >smodel.Account{} -	if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil { -		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)) +	if targetStatus.Account == nil { +		return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))  	} -	var boostOfStatus *gtsmodel.Status -	if targetStatus.BoostOfID != "" { -		boostOfStatus = >smodel.Status{} -		if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil { -			return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)) -		} -	} - -	l.Trace("going to see if status is visible") -	visible, err := p.filter.StatusVisible(targetStatus, account) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that +	visible, err := p.filter.StatusVisible(targetStatus, requestingAccount)  	if err != nil {  		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))  	} -  	if !visible {  		return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))  	} - -	// is the status faveable?  	if targetStatus.VisibilityAdvanced != nil {  		if !targetStatus.VisibilityAdvanced.Likeable {  			return nil, gtserror.NewErrorForbidden(errors.New("status is not faveable")) @@ -54,7 +37,7 @@ func (p *processor) Fave(account *gtsmodel.Account, targetStatusID string) (*api  	// first check if the status is already faved, if so we don't need to do anything  	newFave := true  	gtsFave := >smodel.StatusFave{} -	if err := p.db.GetWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: account.ID}}, gtsFave); err == nil { +	if err := p.db.GetWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsFave); err == nil {  		// we already have a fave for this status  		newFave = false  	} @@ -67,14 +50,14 @@ func (p *processor) Fave(account *gtsmodel.Account, targetStatusID string) (*api  		// we need to create a new fave in the database  		gtsFave := >smodel.StatusFave{ -			ID:               thisFaveID, -			AccountID:        account.ID, -			TargetAccountID:  targetAccount.ID, -			StatusID:         targetStatus.ID, -			URI:              util.GenerateURIForLike(account.Username, p.config.Protocol, p.config.Host, thisFaveID), -			GTSStatus:        targetStatus, -			GTSTargetAccount: targetAccount, -			GTSFavingAccount: account, +			ID:              thisFaveID, +			AccountID:       requestingAccount.ID, +			Account:         requestingAccount, +			TargetAccountID: targetStatus.AccountID, +			TargetAccount:   targetStatus.Account, +			StatusID:        targetStatus.ID, +			Status:          targetStatus, +			URI:             util.GenerateURIForLike(requestingAccount.Username, p.config.Protocol, p.config.Host, thisFaveID),  		}  		if err := p.db.Put(gtsFave); err != nil { @@ -86,13 +69,13 @@ func (p *processor) Fave(account *gtsmodel.Account, targetStatusID string) (*api  			APObjectType:   gtsmodel.ActivityStreamsLike,  			APActivityType: gtsmodel.ActivityStreamsCreate,  			GTSModel:       gtsFave, -			OriginAccount:  account, -			TargetAccount:  targetAccount, +			OriginAccount:  requestingAccount, +			TargetAccount:  targetStatus.Account,  		}  	}  	// return the mastodon representation of the target status -	mastoStatus, err := p.tc.StatusToMasto(targetStatus, account) +	mastoStatus, err := p.tc.StatusToMasto(targetStatus, requestingAccount)  	if err != nil {  		return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))  	} diff --git a/internal/processing/status/favedby.go b/internal/processing/status/favedby.go index 5194cc258..dffe6bba9 100644 --- a/internal/processing/status/favedby.go +++ b/internal/processing/status/favedby.go @@ -9,51 +9,40 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  ) -func (p *processor) FavedBy(account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) { -	l := p.log.WithField("func", "StatusFavedBy") - -	l.Tracef("going to search for target status %s", targetStatusID) -	targetStatus := >smodel.Status{} -	if err := p.db.GetByID(targetStatusID, targetStatus); err != nil { +func (p *processor) FavedBy(requestingAccount *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) { +	targetStatus, err := p.db.GetStatusByID(targetStatusID) +	if err != nil {  		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))  	} - -	l.Tracef("going to search for target account %s", targetStatus.AccountID) -	targetAccount := >smodel.Account{} -	if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil { -		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)) +	if targetStatus.Account == nil { +		return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))  	} -	l.Trace("going to see if status is visible") -	visible, err := p.filter.StatusVisible(targetStatus, account) +	visible, err := p.filter.StatusVisible(targetStatus, requestingAccount)  	if err != nil {  		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))  	} -  	if !visible {  		return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))  	} -	// get ALL accounts that faved a status -- doesn't take account of blocks and mutes and stuff -	favingAccounts, err := p.db.WhoFavedStatus(targetStatus) +	statusFaves, err := p.db.GetStatusFaves(targetStatus)  	if err != nil {  		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing who faved status: %s", err))  	}  	// filter the list so the user doesn't see accounts they blocked or which blocked them  	filteredAccounts := []*gtsmodel.Account{} -	for _, acc := range favingAccounts { -		blocked, err := p.db.Blocked(account.ID, acc.ID) +	for _, fave := range statusFaves { +		blocked, err := p.db.IsBlocked(requestingAccount.ID, fave.AccountID, true)  		if err != nil {  			return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking blocks: %s", err))  		}  		if !blocked { -			filteredAccounts = append(filteredAccounts, acc) +			filteredAccounts = append(filteredAccounts, fave.Account)  		}  	} -	// TODO: filter other things here? suspended? muted? silenced? -  	// now we can return the masto representation of those accounts  	mastoAccounts := []*apimodel.Account{}  	for _, acc := range filteredAccounts { diff --git a/internal/processing/status/get.go b/internal/processing/status/get.go index 9a70185b0..9d403b901 100644 --- a/internal/processing/status/get.go +++ b/internal/processing/status/get.go @@ -9,44 +9,27 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  ) -func (p *processor) Get(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { -	l := p.log.WithField("func", "StatusGet") - -	l.Tracef("going to search for target status %s", targetStatusID) -	targetStatus := >smodel.Status{} -	if err := p.db.GetByID(targetStatusID, targetStatus); err != nil { +func (p *processor) Get(requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { +	targetStatus, err := p.db.GetStatusByID(targetStatusID) +	if err != nil {  		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))  	} - -	l.Tracef("going to search for target account %s", targetStatus.AccountID) -	targetAccount := >smodel.Account{} -	if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil { -		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)) +	if targetStatus.Account == nil { +		return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))  	} -	l.Trace("going to see if status is visible") -	visible, err := p.filter.StatusVisible(targetStatus, account) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that +	visible, err := p.filter.StatusVisible(targetStatus, requestingAccount)  	if err != nil {  		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))  	} -  	if !visible {  		return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))  	} -	var boostOfStatus *gtsmodel.Status -	if targetStatus.BoostOfID != "" { -		boostOfStatus = >smodel.Status{} -		if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil { -			return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)) -		} -	} - -	mastoStatus, err := p.tc.StatusToMasto(targetStatus, account) +	mastoStatus, err := p.tc.StatusToMasto(targetStatus, requestingAccount)  	if err != nil {  		return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))  	}  	return mastoStatus, nil -  } diff --git a/internal/processing/status/unboost.go b/internal/processing/status/unboost.go index 2a1394695..254cfe11f 100644 --- a/internal/processing/status/unboost.go +++ b/internal/processing/status/unboost.go @@ -10,27 +10,19 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  ) -func (p *processor) Unboost(account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { -	l := p.log.WithField("func", "Unboost") - -	l.Tracef("going to search for target status %s", targetStatusID) -	targetStatus := >smodel.Status{} -	if err := p.db.GetByID(targetStatusID, targetStatus); err != nil { +func (p *processor) Unboost(requestingAccount *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { +	targetStatus, err := p.db.GetStatusByID(targetStatusID) +	if err != nil {  		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))  	} - -	l.Tracef("going to search for target account %s", targetStatus.AccountID) -	targetAccount := >smodel.Account{} -	if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil { -		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)) +	if targetStatus.Account == nil { +		return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))  	} -	l.Trace("going to see if status is visible") -	visible, err := p.filter.StatusVisible(targetStatus, account) +	visible, err := p.filter.StatusVisible(targetStatus, requestingAccount)  	if err != nil {  		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))  	} -  	if !visible {  		return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))  	} @@ -46,7 +38,7 @@ func (p *processor) Unboost(account *gtsmodel.Account, application *gtsmodel.App  		},  		{  			Key:   "account_id", -			Value: account.ID, +			Value: requestingAccount.ID,  		},  	}  	err = p.db.GetWhere(where, gtsBoost) @@ -57,7 +49,7 @@ func (p *processor) Unboost(account *gtsmodel.Account, application *gtsmodel.App  	if err != nil {  		// something went wrong in the db finding the boost -		if _, ok := err.(db.ErrNoEntries); !ok { +		if err != db.ErrNoEntries {  			return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing boost from database: %s", err))  		}  		// we just don't have a boost @@ -71,22 +63,23 @@ func (p *processor) Unboost(account *gtsmodel.Account, application *gtsmodel.App  		}  		// pin some stuff onto the boost while we have it out of the db -		gtsBoost.GTSBoostedStatus = targetStatus -		gtsBoost.GTSBoostedStatus.GTSAuthorAccount = targetAccount -		gtsBoost.GTSBoostedAccount = targetAccount -		gtsBoost.GTSAuthorAccount = account +		gtsBoost.Account = requestingAccount + +		gtsBoost.BoostOf = targetStatus +		gtsBoost.BoostOfAccount = targetStatus.Account +		gtsBoost.BoostOf.Account = targetStatus.Account  		// send it back to the processor for async processing  		p.fromClientAPI <- gtsmodel.FromClientAPI{  			APObjectType:   gtsmodel.ActivityStreamsAnnounce,  			APActivityType: gtsmodel.ActivityStreamsUndo,  			GTSModel:       gtsBoost, -			OriginAccount:  account, -			TargetAccount:  targetAccount, +			OriginAccount:  requestingAccount, +			TargetAccount:  targetStatus.Account,  		}  	} -	mastoStatus, err := p.tc.StatusToMasto(targetStatus, account) +	mastoStatus, err := p.tc.StatusToMasto(targetStatus, requestingAccount)  	if err != nil {  		return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))  	} diff --git a/internal/processing/status/unfave.go b/internal/processing/status/unfave.go index b51daacb9..d6e5320db 100644 --- a/internal/processing/status/unfave.go +++ b/internal/processing/status/unfave.go @@ -10,26 +10,19 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  ) -func (p *processor) Unfave(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { -	l := p.log.WithField("func", "StatusUnfave") -	l.Tracef("going to search for target status %s", targetStatusID) -	targetStatus := >smodel.Status{} -	if err := p.db.GetByID(targetStatusID, targetStatus); err != nil { +func (p *processor) Unfave(requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { +	targetStatus, err := p.db.GetStatusByID(targetStatusID) +	if err != nil {  		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))  	} - -	l.Tracef("going to search for target account %s", targetStatus.AccountID) -	targetAccount := >smodel.Account{} -	if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil { -		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)) +	if targetStatus.Account == nil { +		return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))  	} -	l.Trace("going to see if status is visible") -	visible, err := p.filter.StatusVisible(targetStatus, account) +	visible, err := p.filter.StatusVisible(targetStatus, requestingAccount)  	if err != nil {  		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))  	} -  	if !visible {  		return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))  	} @@ -38,14 +31,14 @@ func (p *processor) Unfave(account *gtsmodel.Account, targetStatusID string) (*a  	var toUnfave bool  	gtsFave := >smodel.StatusFave{} -	err = p.db.GetWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: account.ID}}, gtsFave) +	err = p.db.GetWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsFave)  	if err == nil {  		// we have a fave  		toUnfave = true  	}  	if err != nil {  		// something went wrong in the db finding the fave -		if _, ok := err.(db.ErrNoEntries); !ok { +		if err != db.ErrNoEntries {  			return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing fave from database: %s", err))  		}  		// we just don't have a fave @@ -54,7 +47,7 @@ func (p *processor) Unfave(account *gtsmodel.Account, targetStatusID string) (*a  	if toUnfave {  		// we had a fave, so take some action to get rid of it -		if err := p.db.DeleteWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: account.ID}}, gtsFave); err != nil { +		if err := p.db.DeleteWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsFave); err != nil {  			return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unfaveing status: %s", err))  		} @@ -63,12 +56,12 @@ func (p *processor) Unfave(account *gtsmodel.Account, targetStatusID string) (*a  			APObjectType:   gtsmodel.ActivityStreamsLike,  			APActivityType: gtsmodel.ActivityStreamsUndo,  			GTSModel:       gtsFave, -			OriginAccount:  account, -			TargetAccount:  targetAccount, +			OriginAccount:  requestingAccount, +			TargetAccount:  targetStatus.Account,  		}  	} -	mastoStatus, err := p.tc.StatusToMasto(targetStatus, account) +	mastoStatus, err := p.tc.StatusToMasto(targetStatus, requestingAccount)  	if err != nil {  		return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))  	} diff --git a/internal/processing/status/util.go b/internal/processing/status/util.go index 3be53591b..025607f4a 100644 --- a/internal/processing/status/util.go +++ b/internal/processing/status/util.go @@ -99,7 +99,7 @@ func (p *processor) ProcessReplyToID(form *apimodel.AdvancedStatusCreateForm, th  	repliedAccount := >smodel.Account{}  	// check replied status exists + is replyable  	if err := p.db.GetByID(form.InReplyToID, repliedStatus); err != nil { -		if _, ok := err.(db.ErrNoEntries); ok { +		if err == db.ErrNoEntries {  			return fmt.Errorf("status with id %s not replyable because it doesn't exist", form.InReplyToID)  		}  		return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err) @@ -113,14 +113,14 @@ func (p *processor) ProcessReplyToID(form *apimodel.AdvancedStatusCreateForm, th  	// check replied account is known to us  	if err := p.db.GetByID(repliedStatus.AccountID, repliedAccount); err != nil { -		if _, ok := err.(db.ErrNoEntries); ok { +		if err == db.ErrNoEntries {  			return fmt.Errorf("status with id %s not replyable because account id %s is not known", form.InReplyToID, repliedStatus.AccountID)  		}  		return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)  	}  	// check if a block exists -	if blocked, err := p.db.Blocked(thisAccountID, repliedAccount.ID); err != nil { -		if _, ok := err.(db.ErrNoEntries); !ok { +	if blocked, err := p.db.IsBlocked(thisAccountID, repliedAccount.ID, true); err != nil { +		if err != db.ErrNoEntries {  			return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)  		}  	} else if blocked { @@ -156,8 +156,8 @@ func (p *processor) ProcessMediaIDs(form *apimodel.AdvancedStatusCreateForm, thi  		gtsMediaAttachments = append(gtsMediaAttachments, a)  		attachments = append(attachments, a.ID)  	} -	status.GTSMediaAttachments = gtsMediaAttachments -	status.Attachments = attachments +	status.Attachments = gtsMediaAttachments +	status.AttachmentIDs = attachments  	return nil  } @@ -192,9 +192,9 @@ func (p *processor) ProcessMentions(form *apimodel.AdvancedStatusCreateForm, acc  		menchies = append(menchies, menchie.ID)  	}  	// add full populated gts menchies to the status for passing them around conveniently -	status.GTSMentions = gtsMenchies +	status.Mentions = gtsMenchies  	// add just the ids of the mentioned accounts to the status for putting in the db -	status.Mentions = menchies +	status.MentionIDs = menchies  	return nil  } @@ -211,9 +211,9 @@ func (p *processor) ProcessTags(form *apimodel.AdvancedStatusCreateForm, account  		tags = append(tags, tag.ID)  	}  	// add full populated gts tags to the status for passing them around conveniently -	status.GTSTags = gtsTags +	status.Tags = gtsTags  	// add just the ids of the used tags to the status for putting in the db -	status.Tags = tags +	status.TagIDs = tags  	return nil  } @@ -227,9 +227,9 @@ func (p *processor) ProcessEmojis(form *apimodel.AdvancedStatusCreateForm, accou  		emojis = append(emojis, e.ID)  	}  	// add full populated gts emojis to the status for passing them around conveniently -	status.GTSEmojis = gtsEmojis +	status.Emojis = gtsEmojis  	// add just the ids of the used emojis to the status for putting in the db -	status.Emojis = emojis +	status.EmojiIDs = emojis  	return nil  } @@ -252,9 +252,9 @@ func (p *processor) ProcessContent(form *apimodel.AdvancedStatusCreateForm, acco  	var formatted string  	switch form.Format {  	case apimodel.StatusFormatPlain: -		formatted = p.formatter.FromPlain(content, status.GTSMentions, status.GTSTags) +		formatted = p.formatter.FromPlain(content, status.Mentions, status.Tags)  	case apimodel.StatusFormatMarkdown: -		formatted = p.formatter.FromMarkdown(content, status.GTSMentions, status.GTSTags) +		formatted = p.formatter.FromMarkdown(content, status.Mentions, status.Tags)  	default:  		return fmt.Errorf("format %s not recognised as a valid status format", form.Format)  	} diff --git a/internal/processing/status/util_test.go b/internal/processing/status/util_test.go index 4bf508848..9c282eb52 100644 --- a/internal/processing/status/util_test.go +++ b/internal/processing/status/util_test.go @@ -91,19 +91,19 @@ func (suite *UtilTestSuite) TestProcessMentions1() {  	err := suite.status.ProcessMentions(form, creatingAccount.ID, status)  	assert.NoError(suite.T(), err) -	assert.Len(suite.T(), status.GTSMentions, 1) -	newMention := status.GTSMentions[0] +	assert.Len(suite.T(), status.Mentions, 1) +	newMention := status.Mentions[0]  	assert.Equal(suite.T(), mentionedAccount.ID, newMention.TargetAccountID)  	assert.Equal(suite.T(), creatingAccount.ID, newMention.OriginAccountID)  	assert.Equal(suite.T(), creatingAccount.URI, newMention.OriginAccountURI)  	assert.Equal(suite.T(), status.ID, newMention.StatusID)  	assert.Equal(suite.T(), fmt.Sprintf("@%s@%s", mentionedAccount.Username, mentionedAccount.Domain), newMention.NameString) -	assert.Equal(suite.T(), mentionedAccount.URI, newMention.MentionedAccountURI) -	assert.Equal(suite.T(), mentionedAccount.URL, newMention.MentionedAccountURL) -	assert.NotNil(suite.T(), newMention.GTSAccount) +	assert.Equal(suite.T(), mentionedAccount.URI, newMention.TargetAccountURI) +	assert.Equal(suite.T(), mentionedAccount.URL, newMention.TargetAccountURL) +	assert.NotNil(suite.T(), newMention.OriginAccount) -	assert.Len(suite.T(), status.Mentions, 1) -	assert.Equal(suite.T(), newMention.ID, status.Mentions[0]) +	assert.Len(suite.T(), status.MentionIDs, 1) +	assert.Equal(suite.T(), newMention.ID, status.MentionIDs[0])  }  func (suite *UtilTestSuite) TestProcessContentFull1() { @@ -232,19 +232,19 @@ func (suite *UtilTestSuite) TestProcessMentions2() {  	err := suite.status.ProcessMentions(form, creatingAccount.ID, status)  	assert.NoError(suite.T(), err) -	assert.Len(suite.T(), status.GTSMentions, 1) -	newMention := status.GTSMentions[0] +	assert.Len(suite.T(), status.Mentions, 1) +	newMention := status.Mentions[0]  	assert.Equal(suite.T(), mentionedAccount.ID, newMention.TargetAccountID)  	assert.Equal(suite.T(), creatingAccount.ID, newMention.OriginAccountID)  	assert.Equal(suite.T(), creatingAccount.URI, newMention.OriginAccountURI)  	assert.Equal(suite.T(), status.ID, newMention.StatusID)  	assert.Equal(suite.T(), fmt.Sprintf("@%s@%s", mentionedAccount.Username, mentionedAccount.Domain), newMention.NameString) -	assert.Equal(suite.T(), mentionedAccount.URI, newMention.MentionedAccountURI) -	assert.Equal(suite.T(), mentionedAccount.URL, newMention.MentionedAccountURL) -	assert.NotNil(suite.T(), newMention.GTSAccount) +	assert.Equal(suite.T(), mentionedAccount.URI, newMention.TargetAccountURI) +	assert.Equal(suite.T(), mentionedAccount.URL, newMention.TargetAccountURL) +	assert.NotNil(suite.T(), newMention.OriginAccount) -	assert.Len(suite.T(), status.Mentions, 1) -	assert.Equal(suite.T(), newMention.ID, status.Mentions[0]) +	assert.Len(suite.T(), status.MentionIDs, 1) +	assert.Equal(suite.T(), newMention.ID, status.MentionIDs[0])  }  func (suite *UtilTestSuite) TestProcessContentFull2() { diff --git a/internal/processing/streaming.go b/internal/processing/streaming.go index 1e566da81..457db0576 100644 --- a/internal/processing/streaming.go +++ b/internal/processing/streaming.go @@ -1,3 +1,21 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ +  package processing  import ( diff --git a/internal/processing/timeline.go b/internal/processing/timeline.go index 18d0a6ac7..afddd3e6c 100644 --- a/internal/processing/timeline.go +++ b/internal/processing/timeline.go @@ -74,9 +74,9 @@ func (p *processor) HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID st  }  func (p *processor) PublicTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.StatusTimelineResponse, gtserror.WithCode) { -	statuses, err := p.db.GetPublicTimelineForAccount(authed.Account.ID, maxID, sinceID, minID, limit, local) +	statuses, err := p.db.GetPublicTimeline(authed.Account.ID, maxID, sinceID, minID, limit, local)  	if err != nil { -		if _, ok := err.(db.ErrNoEntries); ok { +		if err == db.ErrNoEntries {  			// there are just no entries left  			return &apimodel.StatusTimelineResponse{  				Statuses: []*apimodel.Status{}, @@ -95,9 +95,9 @@ func (p *processor) PublicTimelineGet(authed *oauth.Auth, maxID string, sinceID  }  func (p *processor) FavedTimelineGet(authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.StatusTimelineResponse, gtserror.WithCode) { -	statuses, nextMaxID, prevMinID, err := p.db.GetFavedTimelineForAccount(authed.Account.ID, maxID, minID, limit) +	statuses, nextMaxID, prevMinID, err := p.db.GetFavedTimeline(authed.Account.ID, maxID, minID, limit)  	if err != nil { -		if _, ok := err.(db.ErrNoEntries); ok { +		if err == db.ErrNoEntries {  			// there are just no entries left  			return &apimodel.StatusTimelineResponse{  				Statuses: []*apimodel.Status{}, @@ -122,7 +122,7 @@ func (p *processor) filterPublicStatuses(authed *oauth.Auth, statuses []*gtsmode  	for _, s := range statuses {  		targetAccount := >smodel.Account{}  		if err := p.db.GetByID(s.AccountID, targetAccount); err != nil { -			if _, ok := err.(db.ErrNoEntries); ok { +			if err == db.ErrNoEntries {  				l.Debugf("filterPublicStatuses: skipping status %s because account %s can't be found in the db", s.ID, s.AccountID)  				continue  			} @@ -157,7 +157,7 @@ func (p *processor) filterFavedStatuses(authed *oauth.Auth, statuses []*gtsmodel  	for _, s := range statuses {  		targetAccount := >smodel.Account{}  		if err := p.db.GetByID(s.AccountID, targetAccount); err != nil { -			if _, ok := err.(db.ErrNoEntries); ok { +			if err == db.ErrNoEntries {  				l.Debugf("filterFavedStatuses: skipping status %s because account %s can't be found in the db", s.ID, s.AccountID)  				continue  			} diff --git a/internal/router/session.go b/internal/router/session.go index 2b9be2f56..38810572f 100644 --- a/internal/router/session.go +++ b/internal/router/session.go @@ -49,7 +49,7 @@ func useSession(cfg *config.Config, dbService db.DB, engine *gin.Engine) error {  	// check if we have a saved router session already  	routerSessions := []*gtsmodel.RouterSession{}  	if err := dbService.GetAll(&routerSessions); err != nil { -		if _, ok := err.(db.ErrNoEntries); !ok { +		if err != db.ErrNoEntries {  			// proper error occurred  			return err  		} diff --git a/internal/text/common.go b/internal/text/common.go index f6a5ca5f5..af77521dd 100644 --- a/internal/text/common.go +++ b/internal/text/common.go @@ -93,9 +93,9 @@ func (f *formatter) ReplaceMentions(in string, mentions []*gtsmodel.Mention) str  		// make sure we have a target account, either by getting one pinned on the mention,  		// or by pulling it from the database  		var targetAccount *gtsmodel.Account -		if menchie.GTSAccount != nil { +		if menchie.OriginAccount != nil {  			// got it from the mention -			targetAccount = menchie.GTSAccount +			targetAccount = menchie.OriginAccount  		} else {  			a := >smodel.Account{}  			if err := f.db.GetByID(menchie.TargetAccountID, a); err == nil { diff --git a/internal/timeline/index.go b/internal/timeline/index.go index 1e1a9d7bb..7cffe7ab9 100644 --- a/internal/timeline/index.go +++ b/internal/timeline/index.go @@ -50,9 +50,9 @@ func (t *timeline) IndexBefore(statusID string, include bool, amount int) error  	i := 0  grabloop:  	for ; len(filtered) < amount && i < 5; i = i + 1 { // try the grabloop 5 times only -		statuses, err := t.db.GetHomeTimelineForAccount(t.accountID, "", "", offsetStatus, amount, false) +		statuses, err := t.db.GetHomeTimeline(t.accountID, "", "", offsetStatus, amount, false)  		if err != nil { -			if _, ok := err.(db.ErrNoEntries); ok { +			if err == db.ErrNoEntries {  				break grabloop // we just don't have enough statuses left in the db so index what we've got and then bail  			}  			return fmt.Errorf("IndexBefore: error getting statuses from db: %s", err) @@ -130,9 +130,9 @@ positionLoop:  grabloop:  	for ; len(filtered) < amount && i < 5; i = i + 1 { // try the grabloop 5 times only  		l.Tracef("entering grabloop; i is %d; len(filtered) is %d", i, len(filtered)) -		statuses, err := t.db.GetHomeTimelineForAccount(t.accountID, offsetStatus, "", "", amount, false) +		statuses, err := t.db.GetHomeTimeline(t.accountID, offsetStatus, "", "", amount, false)  		if err != nil { -			if _, ok := err.(db.ErrNoEntries); ok { +			if err == db.ErrNoEntries {  				break grabloop // we just don't have enough statuses left in the db so index what we've got and then bail  			}  			return fmt.Errorf("IndexBehind: error getting statuses from db: %s", err) diff --git a/internal/timeline/index_test.go b/internal/timeline/index_test.go index f48b2691c..4201a27dd 100644 --- a/internal/timeline/index_test.go +++ b/internal/timeline/index_test.go @@ -67,9 +67,8 @@ func (suite *IndexTestSuite) TestIndexBeforeLowID() {  	suite.NoError(err)  	suite.Equal("01F8MHAAY43M6RJ473VQFCVH37", postID) -	// indexLength should only be 9 because that's all this user has hometimelineable  	indexLength := suite.timeline.PostIndexLength() -	suite.Equal(9, indexLength) +	suite.Equal(10, indexLength)  }  func (suite *IndexTestSuite) TestIndexBeforeHighID() { @@ -97,9 +96,9 @@ func (suite *IndexTestSuite) TestIndexBehindHighID() {  	suite.NoError(err)  	suite.Equal("01FCTA44PW9H1TB328S9AQXKDS", postID) -	// indexLength should only be 11 because that's all this user has hometimelineable +	// indexLength should be 10 because that's all this user has hometimelineable  	indexLength := suite.timeline.PostIndexLength() -	suite.Equal(11, indexLength) +	suite.Equal(10, indexLength)  }  func (suite *IndexTestSuite) TestIndexBehindLowID() { diff --git a/internal/timeline/manager_test.go b/internal/timeline/manager_test.go index 9b975a5ce..00c6dcb4a 100644 --- a/internal/timeline/manager_test.go +++ b/internal/timeline/manager_test.go @@ -66,9 +66,9 @@ func (suite *ManagerTestSuite) TestManagerIntegration() {  	err = suite.manager.PrepareXFromTop(testAccount.ID, 20)  	suite.NoError(err) -	// local_account_1 can see 11 statuses out of the testrig statuses in its home timeline +	// local_account_1 can see 12 statuses out of the testrig statuses in its home timeline  	indexedLen = suite.manager.GetIndexedLength(testAccount.ID) -	suite.Equal(11, indexedLen) +	suite.Equal(12, indexedLen)  	// oldest should now be set  	oldestIndexed, err = suite.manager.GetOldestIndexedID(testAccount.ID) @@ -78,7 +78,7 @@ func (suite *ManagerTestSuite) TestManagerIntegration() {  	// get hometimeline  	statuses, err := suite.manager.HomeTimeline(testAccount.ID, "", "", "", 20, false)  	suite.NoError(err) -	suite.Len(statuses, 11) +	suite.Len(statuses, 12)  	// now wipe the last status from all timelines, as though it had been deleted by the owner  	err = suite.manager.WipeStatusFromAllTimelines("01F8MH75CBF9JFX4ZAD54N0W0R") @@ -86,7 +86,7 @@ func (suite *ManagerTestSuite) TestManagerIntegration() {  	// timeline should be shorter  	indexedLen = suite.manager.GetIndexedLength(testAccount.ID) -	suite.Equal(10, indexedLen) +	suite.Equal(11, indexedLen)  	// oldest should now be different  	oldestIndexed, err = suite.manager.GetOldestIndexedID(testAccount.ID) @@ -100,7 +100,7 @@ func (suite *ManagerTestSuite) TestManagerIntegration() {  	// timeline should be shorter  	indexedLen = suite.manager.GetIndexedLength(testAccount.ID) -	suite.Equal(9, indexedLen) +	suite.Equal(10, indexedLen)  	// oldest should now be different  	oldestIndexed, err = suite.manager.GetOldestIndexedID(testAccount.ID) diff --git a/internal/timeline/prepare.go b/internal/timeline/prepare.go index 51846c816..20000b4e9 100644 --- a/internal/timeline/prepare.go +++ b/internal/timeline/prepare.go @@ -95,7 +95,7 @@ prepareloop:  		if preparing {  			if err := t.prepare(entry.statusID); err != nil {  				// there's been an error -				if _, ok := err.(db.ErrNoEntries); !ok { +				if err != db.ErrNoEntries {  					// it's a real error  					return fmt.Errorf("PrepareBehind: error preparing status with id %s: %s", entry.statusID, err)  				} @@ -150,7 +150,7 @@ prepareloop:  		if preparing {  			if err := t.prepare(entry.statusID); err != nil {  				// there's been an error -				if _, ok := err.(db.ErrNoEntries); !ok { +				if err != db.ErrNoEntries {  					// it's a real error  					return fmt.Errorf("PrepareBefore: error preparing status with id %s: %s", entry.statusID, err)  				} @@ -205,7 +205,7 @@ prepareloop:  		if err := t.prepare(entry.statusID); err != nil {  			// there's been an error -			if _, ok := err.(db.ErrNoEntries); !ok { +			if err != db.ErrNoEntries {  				// it's a real error  				return fmt.Errorf("PrepareFromTop: error preparing status with id %s: %s", entry.statusID, err)  			} diff --git a/internal/transport/controller.go b/internal/transport/controller.go index 33eab2a3a..4eb6b5658 100644 --- a/internal/transport/controller.go +++ b/internal/transport/controller.go @@ -28,7 +28,6 @@ import (  	"github.com/sirupsen/logrus"  	"github.com/superseriousbusiness/gotosocial/internal/config"  	"github.com/superseriousbusiness/gotosocial/internal/db" -	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  )  // Controller generates transports for use in making federation requests to other servers. @@ -95,14 +94,15 @@ func (c *controller) NewTransportForUsername(username string) (Transport, error)  	// We need an account to use to create a transport for dereferecing something.  	// If a username has been given, we can fetch the account with that username and use it.  	// Otherwise, we can take the instance account and use those credentials to make the request. -	ourAccount := >smodel.Account{}  	var u string  	if username == "" {  		u = c.config.Host  	} else {  		u = username  	} -	if err := c.db.GetLocalAccountByUsername(u, ourAccount); err != nil { + +	ourAccount, err := c.db.GetLocalAccountByUsername(u) +	if err != nil {  		return nil, fmt.Errorf("error getting account %s from db: %s", username, err)  	} diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index f754d282a..a16318df8 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -37,21 +37,20 @@ func (c *converter) ASRepresentationToAccount(accountable ap.Accountable, update  	}  	uri := uriProp.GetIRI() -	acct := >smodel.Account{}  	if !update { -		err := c.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, acct) +		acct, err := c.db.GetAccountByURI(uri.String())  		if err == nil {  			// we already know this account so we can skip generating it  			return acct, nil  		} -		if _, ok := err.(db.ErrNoEntries); !ok { +		if err != db.ErrNoEntries {  			// we don't know the account and there's been a real error  			return nil, fmt.Errorf("error getting account with uri %s from the database: %s", uri.String(), err)  		}  	}  	// we don't know the account, or we're being told to update it, so we need to generate it from the person -- at least we already have the URI! -	acct = >smodel.Account{} +	acct := >smodel.Account{}  	acct.URI = uri.String()  	// Username aka preferredUsername @@ -188,22 +187,22 @@ func (c *converter) ASStatusToStatus(statusable ap.Statusable) (*gtsmodel.Status  	// attachments to dereference and fetch later on (we don't do that here)  	if attachments, err := ap.ExtractAttachments(statusable); err == nil { -		status.GTSMediaAttachments = attachments +		status.Attachments = attachments  	}  	// hashtags to dereference later on  	if hashtags, err := ap.ExtractHashtags(statusable); err == nil { -		status.GTSTags = hashtags +		status.Tags = hashtags  	}  	// emojis to dereference and fetch later on  	if emojis, err := ap.ExtractEmojis(statusable); err == nil { -		status.GTSEmojis = emojis +		status.Emojis = emojis  	}  	// mentions to dereference later on  	if mentions, err := ap.ExtractMentions(statusable); err == nil { -		status.GTSMentions = mentions +		status.Mentions = mentions  	}  	// cw string for this status @@ -225,13 +224,13 @@ func (c *converter) ASStatusToStatus(statusable ap.Statusable) (*gtsmodel.Status  	}  	status.AccountURI = attributedTo.String() -	statusOwner := >smodel.Account{} -	if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: attributedTo.String(), CaseInsensitive: true}}, statusOwner); err != nil { +	statusOwner, err := c.db.GetAccountByURI(attributedTo.String()) +	if err != nil {  		return nil, fmt.Errorf("couldn't get status owner from db: %s", err)  	}  	status.AccountID = statusOwner.ID  	status.AccountURI = statusOwner.URI -	status.GTSAuthorAccount = statusOwner +	status.Account = statusOwner  	// check if there's a post that this is a reply to  	inReplyToURI := ap.ExtractInReplyToURI(statusable) @@ -241,18 +240,16 @@ func (c *converter) ASStatusToStatus(statusable ap.Statusable) (*gtsmodel.Status  		status.InReplyToURI = inReplyToURI.String()  		// now we can check if we have the replied-to status in our db already -		inReplyToStatus := >smodel.Status{} -		if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: inReplyToURI.String()}}, inReplyToStatus); err == nil { +		if inReplyToStatus, err := c.db.GetStatusByURI(inReplyToURI.String()); err == nil {  			// we have the status in our database already -			// so we can set these fields here and then... +			// so we can set these fields here and now...  			status.InReplyToID = inReplyToStatus.ID  			status.InReplyToAccountID = inReplyToStatus.AccountID -			status.GTSReplyToStatus = inReplyToStatus - -			// ... check if we've seen the account already -			inReplyToAccount := >smodel.Account{} -			if err := c.db.GetByID(inReplyToStatus.AccountID, inReplyToAccount); err == nil { -				status.GTSReplyToAccount = inReplyToAccount +			status.InReplyTo = inReplyToStatus +			if status.InReplyToAccount == nil { +				if inReplyToAccount, err := c.db.GetAccountByID(inReplyToStatus.AccountID); err == nil { +					status.InReplyToAccount = inReplyToAccount +				}  			}  		}  	} @@ -328,8 +325,8 @@ func (c *converter) ASFollowToFollowRequest(followable ap.Followable) (*gtsmodel  	if err != nil {  		return nil, errors.New("error extracting actor property from follow")  	} -	originAccount := >smodel.Account{} -	if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: origin.String()}}, originAccount); err != nil { +	originAccount, err := c.db.GetAccountByURI(origin.String()) +	if err != nil {  		return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err)  	} @@ -337,8 +334,8 @@ func (c *converter) ASFollowToFollowRequest(followable ap.Followable) (*gtsmodel  	if err != nil {  		return nil, errors.New("error extracting object property from follow")  	} -	targetAccount := >smodel.Account{} -	if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: target.String()}}, targetAccount); err != nil { +	targetAccount, err := c.db.GetAccountByURI(target.String()) +	if err != nil {  		return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err)  	} @@ -362,8 +359,8 @@ func (c *converter) ASFollowToFollow(followable ap.Followable) (*gtsmodel.Follow  	if err != nil {  		return nil, errors.New("error extracting actor property from follow")  	} -	originAccount := >smodel.Account{} -	if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: origin.String()}}, originAccount); err != nil { +	originAccount, err := c.db.GetAccountByURI(origin.String()) +	if err != nil {  		return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err)  	} @@ -371,8 +368,8 @@ func (c *converter) ASFollowToFollow(followable ap.Followable) (*gtsmodel.Follow  	if err != nil {  		return nil, errors.New("error extracting object property from follow")  	} -	targetAccount := >smodel.Account{} -	if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: target.String()}}, targetAccount); err != nil { +	targetAccount, err := c.db.GetAccountByURI(target.String()) +	if err != nil {  		return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err)  	} @@ -396,8 +393,8 @@ func (c *converter) ASLikeToFave(likeable ap.Likeable) (*gtsmodel.StatusFave, er  	if err != nil {  		return nil, errors.New("error extracting actor property from like")  	} -	originAccount := >smodel.Account{} -	if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: origin.String()}}, originAccount); err != nil { +	originAccount, err := c.db.GetAccountByURI(origin.String()) +	if err != nil {  		return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err)  	} @@ -406,24 +403,30 @@ func (c *converter) ASLikeToFave(likeable ap.Likeable) (*gtsmodel.StatusFave, er  		return nil, errors.New("error extracting object property from like")  	} -	targetStatus := >smodel.Status{} -	if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: target.String()}}, targetStatus); err != nil { +	targetStatus, err := c.db.GetStatusByURI(target.String()) +	if err != nil {  		return nil, fmt.Errorf("error extracting status with uri %s from the database: %s", target.String(), err)  	} -	targetAccount := >smodel.Account{} -	if err := c.db.GetByID(targetStatus.AccountID, targetAccount); err != nil { -		return nil, fmt.Errorf("error extracting account with id %s from the database: %s", targetStatus.AccountID, err) +	var targetAccount *gtsmodel.Account +	if targetStatus.Account != nil { +		targetAccount = targetStatus.Account +	} else { +		a, err := c.db.GetAccountByID(targetStatus.AccountID) +		if err != nil { +			return nil, fmt.Errorf("error extracting account with id %s from the database: %s", targetStatus.AccountID, err) +		} +		targetAccount = a  	}  	return >smodel.StatusFave{ -		TargetAccountID:  targetAccount.ID, -		StatusID:         targetStatus.ID, -		AccountID:        originAccount.ID, -		URI:              uri, -		GTSStatus:        targetStatus, -		GTSTargetAccount: targetAccount, -		GTSFavingAccount: originAccount, +		AccountID:       originAccount.ID, +		Account:         originAccount, +		TargetAccountID: targetAccount.ID, +		TargetAccount:   targetAccount, +		StatusID:        targetStatus.ID, +		Status:          targetStatus, +		URI:             uri,  	}, nil  } @@ -438,9 +441,9 @@ func (c *converter) ASBlockToBlock(blockable ap.Blockable) (*gtsmodel.Block, err  	if err != nil {  		return nil, errors.New("ASBlockToBlock: error extracting actor property from block")  	} -	originAccount := >smodel.Account{} -	if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: origin.String()}}, originAccount); err != nil { -		return nil, fmt.Errorf("ASBlockToBlock: error extracting account with uri %s from the database: %s", origin.String(), err) +	originAccount, err := c.db.GetAccountByURI(origin.String()) +	if err != nil { +		return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err)  	}  	target, err := ap.ExtractObject(blockable) @@ -448,9 +451,9 @@ func (c *converter) ASBlockToBlock(blockable ap.Blockable) (*gtsmodel.Block, err  		return nil, errors.New("ASBlockToBlock: error extracting object property from block")  	} -	targetAccount := >smodel.Account{} -	if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: target.String(), CaseInsensitive: true}}, targetAccount); err != nil { -		return nil, fmt.Errorf("ASBlockToBlock: error extracting account with uri %s from the database: %s", target.String(), err) +	targetAccount, err := c.db.GetAccountByURI(target.String()) +	if err != nil { +		return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err)  	}  	return >smodel.Block{ @@ -473,7 +476,7 @@ func (c *converter) ASAnnounceToStatus(announceable ap.Announceable) (*gtsmodel.  	}  	uri := idProp.GetIRI().String() -	if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: uri}}, status); err == nil { +	if status, err := c.db.GetStatusByURI(uri); err == nil {  		// we already have it, great, just return it as-is :)  		isNew = false  		return status, isNew, nil @@ -487,7 +490,7 @@ func (c *converter) ASAnnounceToStatus(announceable ap.Announceable) (*gtsmodel.  	}  	// set the URI on the new status for dereferencing later -	status.GTSBoostedStatus = >smodel.Status{ +	status.BoostOf = >smodel.Status{  		URI: boostedStatusURI.String(),  	} @@ -507,18 +510,19 @@ func (c *converter) ASAnnounceToStatus(announceable ap.Announceable) (*gtsmodel.  	// get the boosting account based on the URI  	// this should have been dereferenced already before we hit this point so we can confidently error out if we don't have it -	boostingAccount := >smodel.Account{} -	if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: actor.String()}}, boostingAccount); err != nil { +	boostingAccount, err := c.db.GetAccountByURI(actor.String()) +	if err != nil {  		return nil, isNew, fmt.Errorf("ASAnnounceToStatus: error in db fetching account with uri %s: %s", actor.String(), err)  	}  	status.AccountID = boostingAccount.ID  	status.AccountURI = boostingAccount.URI +	status.Account = boostingAccount  	// these will all be wrapped in the boosted status so set them empty here -	status.Attachments = []string{} -	status.Tags = []string{} -	status.Mentions = []string{} -	status.Emojis = []string{} +	status.AttachmentIDs = []string{} +	status.TagIDs = []string{} +	status.MentionIDs = []string{} +	status.EmojiIDs = []string{}  	// parse the visibility from the To and CC entries  	var visibility gtsmodel.Visibility @@ -552,7 +556,6 @@ func (c *converter) ASAnnounceToStatus(announceable ap.Announceable) (*gtsmodel.  	status.Visibility = visibility  	// the rest of the fields will be taken from the target status, but it's not our job to do the dereferencing here -  	return status, isNew, nil  } diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go index 10d9a0f18..e477a6135 100644 --- a/internal/typeutils/converter.go +++ b/internal/typeutils/converter.go @@ -24,6 +24,7 @@ import (  	"github.com/go-fed/activity/streams/vocab"  	"github.com/superseriousbusiness/gotosocial/internal/ap"  	"github.com/superseriousbusiness/gotosocial/internal/api/model" +	"github.com/superseriousbusiness/gotosocial/internal/cache"  	"github.com/superseriousbusiness/gotosocial/internal/config"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" @@ -175,14 +176,18 @@ type TypeConverter interface {  }  type converter struct { -	config *config.Config -	db     db.DB +	config        *config.Config +	db            db.DB +	frontendCache cache.Cache +	asCache       cache.Cache  }  // NewConverter returns a new Converter  func NewConverter(config *config.Config, db db.DB) TypeConverter {  	return &converter{ -		config: config, -		db:     db, +		config:        config, +		db:            db, +		frontendCache: cache.New(), +		asCache:       cache.New(),  	}  } diff --git a/internal/typeutils/internal.go b/internal/typeutils/internal.go index a46ad7fbd..ad15ecbee 100644 --- a/internal/typeutils/internal.go +++ b/internal/typeutils/internal.go @@ -54,10 +54,10 @@ func (c *converter) StatusToBoost(s *gtsmodel.Status, boostingAccount *gtsmodel.  		InReplyToAccountID: "",  		// these will all be wrapped in the boosted status so set them empty here -		Attachments: []string{}, -		Tags:        []string{}, -		Mentions:    []string{}, -		Emojis:      []string{}, +		AttachmentIDs: []string{}, +		TagIDs:        []string{}, +		MentionIDs:    []string{}, +		EmojiIDs:      []string{},  		// the below fields will be taken from the target status  		Content:             s.Content, @@ -74,7 +74,7 @@ func (c *converter) StatusToBoost(s *gtsmodel.Status, boostingAccount *gtsmodel.  		// attach these here for convenience -- the boosted status/account won't go in the DB  		// but they're needed in the processor and for the frontend. Since we have them, we can  		// attach them so we don't need to fetch them again later (save some DB calls) -		GTSBoostedStatus: s, +		BoostOf: s,  	}  	return boostWrapperStatus, nil diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go index 333f131d4..11ace9dfa 100644 --- a/internal/typeutils/internaltoas.go +++ b/internal/typeutils/internaltoas.go @@ -34,6 +34,14 @@ import (  // Converts a gts model account into an Activity Streams person type, following  // the spec laid out for mastodon here: https://docs.joinmastodon.org/spec/activitypub/  func (c *converter) AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error) { +	// first check if we have this person in our asCache already +	if personI, err := c.asCache.Fetch(a.ID); err == nil { +		if person, ok := personI.(vocab.ActivityStreamsPerson); ok { +			// we have it, so just return it as-is +			return person, nil +		} +	} +  	person := streams.NewActivityStreamsPerson()  	// id should be the activitypub URI of this user @@ -256,6 +264,11 @@ func (c *converter) AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerso  		person.SetActivityStreamsImage(headerProperty)  	} +	// put the person in our cache in case we need it again soon +	if err := c.asCache.Store(a.ID, person); err != nil { +		return nil, err +	} +  	return person, nil  } @@ -326,16 +339,24 @@ func (c *converter) AccountToASMinimal(a *gtsmodel.Account) (vocab.ActivityStrea  }  func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, error) { +	// first check if we have this note in our asCache already +	if noteI, err := c.asCache.Fetch(s.ID); err == nil { +		if note, ok := noteI.(vocab.ActivityStreamsNote); ok { +			// we have it, so just return it as-is +			return note, nil +		} +	} +  	// ensure prerequisites here before we get stuck in  	// check if author account is already attached to status and attach it if not  	// if we can't retrieve this, bail here already because we can't attribute the status to anyone -	if s.GTSAuthorAccount == nil { -		a := >smodel.Account{} -		if err := c.db.GetByID(s.AccountID, a); err != nil { +	if s.Account == nil { +		a, err := c.db.GetAccountByID(s.AccountID) +		if err != nil {  			return nil, fmt.Errorf("StatusToAS: error retrieving author account from db: %s", err)  		} -		s.GTSAuthorAccount = a +		s.Account = a  	}  	// create the Note! @@ -361,16 +382,16 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e  	// inReplyTo  	if s.InReplyToID != "" {  		// fetch the replied status if we don't have it on hand already -		if s.GTSReplyToStatus == nil { +		if s.InReplyTo == nil {  			rs := >smodel.Status{}  			if err := c.db.GetByID(s.InReplyToID, rs); err != nil {  				return nil, fmt.Errorf("StatusToAS: error retrieving replied-to status from db: %s", err)  			} -			s.GTSReplyToStatus = rs +			s.InReplyTo = rs  		} -		rURI, err := url.Parse(s.GTSReplyToStatus.URI) +		rURI, err := url.Parse(s.InReplyTo.URI)  		if err != nil { -			return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.GTSReplyToStatus.URI, err) +			return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.InReplyTo.URI, err)  		}  		inReplyToProp := streams.NewActivityStreamsInReplyToProperty() @@ -396,9 +417,9 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e  	}  	// attributedTo -	authorAccountURI, err := url.Parse(s.GTSAuthorAccount.URI) +	authorAccountURI, err := url.Parse(s.Account.URI)  	if err != nil { -		return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.GTSAuthorAccount.URI, err) +		return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.Account.URI, err)  	}  	attributedToProp := streams.NewActivityStreamsAttributedToProperty()  	attributedToProp.AppendIRI(authorAccountURI) @@ -408,7 +429,7 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e  	tagProp := streams.NewActivityStreamsTagProperty()  	// tag -- mentions -	for _, m := range s.GTSMentions { +	for _, m := range s.Mentions {  		asMention, err := c.MentionToAS(m)  		if err != nil {  			return nil, fmt.Errorf("StatusToAS: error converting mention to AS mention: %s", err) @@ -425,9 +446,9 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e  	status.SetActivityStreamsTag(tagProp)  	// parse out some URIs we need here -	authorFollowersURI, err := url.Parse(s.GTSAuthorAccount.FollowersURI) +	authorFollowersURI, err := url.Parse(s.Account.FollowersURI)  	if err != nil { -		return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.GTSAuthorAccount.FollowersURI, err) +		return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.Account.FollowersURI, err)  	}  	publicURI, err := url.Parse(asPublicURI) @@ -441,10 +462,10 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e  	switch s.Visibility {  	case gtsmodel.VisibilityDirect:  		// if DIRECT, then only mentioned users should be added to TO, and nothing to CC -		for _, m := range s.GTSMentions { -			iri, err := url.Parse(m.GTSAccount.URI) +		for _, m := range s.Mentions { +			iri, err := url.Parse(m.OriginAccount.URI)  			if err != nil { -				return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.GTSAccount.URI, err) +				return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.OriginAccount.URI, err)  			}  			toProp.AppendIRI(iri)  		} @@ -453,10 +474,10 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e  	case gtsmodel.VisibilityFollowersOnly:  		// if FOLLOWERS ONLY then we want to add followers to TO, and mentions to CC  		toProp.AppendIRI(authorFollowersURI) -		for _, m := range s.GTSMentions { -			iri, err := url.Parse(m.GTSAccount.URI) +		for _, m := range s.Mentions { +			iri, err := url.Parse(m.OriginAccount.URI)  			if err != nil { -				return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.GTSAccount.URI, err) +				return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.OriginAccount.URI, err)  			}  			ccProp.AppendIRI(iri)  		} @@ -464,10 +485,10 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e  		// if UNLOCKED, we want to add followers to TO, and public and mentions to CC  		toProp.AppendIRI(authorFollowersURI)  		ccProp.AppendIRI(publicURI) -		for _, m := range s.GTSMentions { -			iri, err := url.Parse(m.GTSAccount.URI) +		for _, m := range s.Mentions { +			iri, err := url.Parse(m.OriginAccount.URI)  			if err != nil { -				return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.GTSAccount.URI, err) +				return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.OriginAccount.URI, err)  			}  			ccProp.AppendIRI(iri)  		} @@ -475,10 +496,10 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e  		// if PUBLIC, we want to add public to TO, and followers and mentions to CC  		toProp.AppendIRI(publicURI)  		ccProp.AppendIRI(authorFollowersURI) -		for _, m := range s.GTSMentions { -			iri, err := url.Parse(m.GTSAccount.URI) +		for _, m := range s.Mentions { +			iri, err := url.Parse(m.OriginAccount.URI)  			if err != nil { -				return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.GTSAccount.URI, err) +				return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.OriginAccount.URI, err)  			}  			ccProp.AppendIRI(iri)  		} @@ -496,7 +517,7 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e  	// attachment  	attachmentProp := streams.NewActivityStreamsAttachmentProperty() -	for _, a := range s.GTSMediaAttachments { +	for _, a := range s.Attachments {  		doc, err := c.AttachmentToAS(a)  		if err != nil {  			return nil, fmt.Errorf("StatusToAS: error converting attachment: %s", err) @@ -515,6 +536,11 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e  	repliesProp.SetActivityStreamsCollection(repliesCollection)  	status.SetActivityStreamsReplies(repliesProp) +	// put the note in our cache in case we need it again soon +	if err := c.asCache.Store(s.ID, status); err != nil { +		return nil, err +	} +  	return status, nil  } @@ -565,12 +591,12 @@ func (c *converter) FollowToAS(f *gtsmodel.Follow, originAccount *gtsmodel.Accou  }  func (c *converter) MentionToAS(m *gtsmodel.Mention) (vocab.ActivityStreamsMention, error) { -	if m.GTSAccount == nil { +	if m.OriginAccount == nil {  		a := >smodel.Account{}  		if err := c.db.GetWhere([]db.Where{{Key: "target_account_id", Value: m.TargetAccountID}}, a); err != nil {  			return nil, fmt.Errorf("MentionToAS: error getting target account from db: %s", err)  		} -		m.GTSAccount = a +		m.OriginAccount = a  	}  	// create the mention @@ -578,21 +604,21 @@ func (c *converter) MentionToAS(m *gtsmodel.Mention) (vocab.ActivityStreamsMenti  	// href -- this should be the URI of the mentioned user  	hrefProp := streams.NewActivityStreamsHrefProperty() -	hrefURI, err := url.Parse(m.GTSAccount.URI) +	hrefURI, err := url.Parse(m.OriginAccount.URI)  	if err != nil { -		return nil, fmt.Errorf("MentionToAS: error parsing uri %s: %s", m.GTSAccount.URI, err) +		return nil, fmt.Errorf("MentionToAS: error parsing uri %s: %s", m.OriginAccount.URI, err)  	}  	hrefProp.SetIRI(hrefURI)  	mention.SetActivityStreamsHref(hrefProp)  	// name -- this should be the namestring of the mentioned user, something like @whatever@example.org  	var domain string -	if m.GTSAccount.Domain == "" { +	if m.OriginAccount.Domain == "" {  		domain = c.config.AccountDomain  	} else { -		domain = m.GTSAccount.Domain +		domain = m.OriginAccount.Domain  	} -	username := m.GTSAccount.Username +	username := m.OriginAccount.Username  	nameString := fmt.Sprintf("@%s@%s", username, domain)  	nameProp := streams.NewActivityStreamsNameProperty()  	nameProp.AppendXMLSchemaString(nameString) @@ -648,30 +674,30 @@ func (c *converter) AttachmentToAS(a *gtsmodel.MediaAttachment) (vocab.ActivityS  */  func (c *converter) FaveToAS(f *gtsmodel.StatusFave) (vocab.ActivityStreamsLike, error) {  	// check if targetStatus is already pinned to this fave, and fetch it if not -	if f.GTSStatus == nil { +	if f.Status == nil {  		s := >smodel.Status{}  		if err := c.db.GetByID(f.StatusID, s); err != nil {  			return nil, fmt.Errorf("FaveToAS: error fetching target status from database: %s", err)  		} -		f.GTSStatus = s +		f.Status = s  	}  	// check if the targetAccount is already pinned to this fave, and fetch it if not -	if f.GTSTargetAccount == nil { +	if f.TargetAccount == nil {  		a := >smodel.Account{}  		if err := c.db.GetByID(f.TargetAccountID, a); err != nil {  			return nil, fmt.Errorf("FaveToAS: error fetching target account from database: %s", err)  		} -		f.GTSTargetAccount = a +		f.TargetAccount = a  	}  	// check if the faving account is already pinned to this fave, and fetch it if not -	if f.GTSFavingAccount == nil { +	if f.Account == nil {  		a := >smodel.Account{}  		if err := c.db.GetByID(f.AccountID, a); err != nil {  			return nil, fmt.Errorf("FaveToAS: error fetching faving account from database: %s", err)  		} -		f.GTSFavingAccount = a +		f.Account = a  	}  	// create the like @@ -679,9 +705,9 @@ func (c *converter) FaveToAS(f *gtsmodel.StatusFave) (vocab.ActivityStreamsLike,  	// set the actor property to the fave-ing account's URI  	actorProp := streams.NewActivityStreamsActorProperty() -	actorIRI, err := url.Parse(f.GTSFavingAccount.URI) +	actorIRI, err := url.Parse(f.Account.URI)  	if err != nil { -		return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.GTSFavingAccount.URI, err) +		return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.Account.URI, err)  	}  	actorProp.AppendIRI(actorIRI)  	like.SetActivityStreamsActor(actorProp) @@ -697,18 +723,18 @@ func (c *converter) FaveToAS(f *gtsmodel.StatusFave) (vocab.ActivityStreamsLike,  	// set the object property to the target status's URI  	objectProp := streams.NewActivityStreamsObjectProperty() -	statusIRI, err := url.Parse(f.GTSStatus.URI) +	statusIRI, err := url.Parse(f.Status.URI)  	if err != nil { -		return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.GTSStatus.URI, err) +		return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.Status.URI, err)  	}  	objectProp.AppendIRI(statusIRI)  	like.SetActivityStreamsObject(objectProp)  	// set the TO property to the target account's IRI  	toProp := streams.NewActivityStreamsToProperty() -	toIRI, err := url.Parse(f.GTSTargetAccount.URI) +	toIRI, err := url.Parse(f.TargetAccount.URI)  	if err != nil { -		return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.GTSTargetAccount.URI, err) +		return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.TargetAccount.URI, err)  	}  	toProp.AppendIRI(toIRI)  	like.SetActivityStreamsTo(toProp) @@ -718,12 +744,12 @@ func (c *converter) FaveToAS(f *gtsmodel.StatusFave) (vocab.ActivityStreamsLike,  func (c *converter) BoostToAS(boostWrapperStatus *gtsmodel.Status, boostingAccount *gtsmodel.Account, boostedAccount *gtsmodel.Account) (vocab.ActivityStreamsAnnounce, error) {  	// the boosted status is probably pinned to the boostWrapperStatus but double check to make sure -	if boostWrapperStatus.GTSBoostedStatus == nil { +	if boostWrapperStatus.BoostOf == nil {  		b := >smodel.Status{}  		if err := c.db.GetByID(boostWrapperStatus.BoostOfID, b); err != nil {  			return nil, fmt.Errorf("BoostToAS: error getting status with ID %s from the db: %s", boostWrapperStatus.BoostOfID, err)  		} -		boostWrapperStatus.GTSBoostedStatus = b +		boostWrapperStatus.BoostOf = b  	}  	// create the announce @@ -748,9 +774,9 @@ func (c *converter) BoostToAS(boostWrapperStatus *gtsmodel.Status, boostingAccou  	announce.SetJSONLDId(idProp)  	// set the object -	boostedStatusURI, err := url.Parse(boostWrapperStatus.GTSBoostedStatus.URI) +	boostedStatusURI, err := url.Parse(boostWrapperStatus.BoostOf.URI)  	if err != nil { -		return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", boostWrapperStatus.GTSBoostedStatus.URI, err) +		return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", boostWrapperStatus.BoostOf.URI, err)  	}  	objectProp := streams.NewActivityStreamsObjectProperty()  	objectProp.AppendIRI(boostedStatusURI) diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 1283e718a..caa14e211 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -38,15 +38,15 @@ func (c *converter) AccountToMastoSensitive(a *gtsmodel.Account) (*model.Account  	// then adding the Source object to it...  	// check pending follow requests aimed at this account -	fr := []gtsmodel.FollowRequest{} -	if err := c.db.GetFollowRequestsForAccountID(a.ID, &fr); err != nil { -		if _, ok := err.(db.ErrNoEntries); !ok { +	frs, err := c.db.GetAccountFollowRequests(a.ID) +	if err != nil { +		if err != db.ErrNoEntries {  			return nil, fmt.Errorf("error getting follow requests: %s", err)  		}  	}  	var frc int -	if fr != nil { -		frc = len(fr) +	if frs != nil { +		frc = len(frs)  	}  	mastoAccount.Source = &model.Source{ @@ -62,68 +62,69 @@ func (c *converter) AccountToMastoSensitive(a *gtsmodel.Account) (*model.Account  }  func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*model.Account, error) { -	// count followers -	followers := []gtsmodel.Follow{} -	if err := c.db.GetFollowersByAccountID(a.ID, &followers, false); err != nil { -		if _, ok := err.(db.ErrNoEntries); !ok { -			return nil, fmt.Errorf("error getting followers: %s", err) +	// first check if we have this account in our frontEnd cache +	if accountI, err := c.frontendCache.Fetch(a.ID); err == nil { +		if account, ok := accountI.(*model.Account); ok { +			// we have it, so just return it as-is +			return account, nil  		}  	} -	var followersCount int -	if followers != nil { -		followersCount = len(followers) + +	// count followers +	followersCount, err := c.db.CountAccountFollowedBy(a.ID, false) +	if err != nil { +		return nil, fmt.Errorf("error counting followers: %s", err)  	}  	// count following -	following := []gtsmodel.Follow{} -	if err := c.db.GetFollowingByAccountID(a.ID, &following); err != nil { -		if _, ok := err.(db.ErrNoEntries); !ok { -			return nil, fmt.Errorf("error getting following: %s", err) -		} -	} -	var followingCount int -	if following != nil { -		followingCount = len(following) +	followingCount, err := c.db.CountAccountFollows(a.ID, false) +	if err != nil { +		return nil, fmt.Errorf("error counting following: %s", err)  	}  	// count statuses -	statusesCount, err := c.db.CountStatusesByAccountID(a.ID) +	statusesCount, err := c.db.CountAccountStatuses(a.ID)  	if err != nil { -		if _, ok := err.(db.ErrNoEntries); !ok { -			return nil, fmt.Errorf("error getting last statuses: %s", err) -		} +		return nil, fmt.Errorf("error counting statuses: %s", err)  	}  	// check when the last status was -	lastStatus := >smodel.Status{} -	if err := c.db.GetLastStatusForAccountID(a.ID, lastStatus); err != nil { -		if _, ok := err.(db.ErrNoEntries); !ok { -			return nil, fmt.Errorf("error getting last status: %s", err) -		} -	}  	var lastStatusAt string -	if lastStatus != nil { -		lastStatusAt = lastStatus.CreatedAt.Format(time.RFC3339) +	lastPosted, err := c.db.GetAccountLastPosted(a.ID) +	if err == nil && !lastPosted.IsZero() { +		lastStatusAt = lastPosted.Format(time.RFC3339)  	}  	// build the avatar and header URLs -	avi := >smodel.MediaAttachment{} -	if err := c.db.GetAvatarForAccountID(avi, a.ID); err != nil { -		if _, ok := err.(db.ErrNoEntries); !ok { -			return nil, fmt.Errorf("error getting avatar: %s", err) +	var aviURL string +	var aviURLStatic string +	if a.AvatarMediaAttachmentID != "" { +		// make sure avi is pinned to this account +		if a.AvatarMediaAttachment == nil { +			avi, err := c.db.GetAttachmentByID(a.AvatarMediaAttachmentID) +			if err != nil { +				return nil, fmt.Errorf("error retrieving avatar: %s", err) +			} +			a.AvatarMediaAttachment = avi  		} +		aviURL = a.AvatarMediaAttachment.URL +		aviURLStatic = a.AvatarMediaAttachment.Thumbnail.URL  	} -	aviURL := avi.URL -	aviURLStatic := avi.Thumbnail.URL -	header := >smodel.MediaAttachment{} -	if err := c.db.GetHeaderForAccountID(header, a.ID); err != nil { -		if _, ok := err.(db.ErrNoEntries); !ok { -			return nil, fmt.Errorf("error getting header: %s", err) +	var headerURL string +	var headerURLStatic string +	if a.HeaderMediaAttachmentID != "" { +		// make sure header is pinned to this account +		if a.HeaderMediaAttachment == nil { +			avi, err := c.db.GetAttachmentByID(a.HeaderMediaAttachmentID) +			if err != nil { +				return nil, fmt.Errorf("error retrieving avatar: %s", err) +			} +			a.HeaderMediaAttachment = avi  		} +		headerURL = a.HeaderMediaAttachment.URL +		headerURLStatic = a.HeaderMediaAttachment.Thumbnail.URL  	} -	headerURL := header.URL -	headerURLStatic := header.Thumbnail.URL  	// get the fields set on this account  	fields := []model.Field{} @@ -155,7 +156,7 @@ func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*model.Account, e  		suspended = true  	} -	return &model.Account{ +	accountFrontend := &model.Account{  		ID:             a.ID,  		Username:       a.Username,  		Acct:           acct, @@ -176,7 +177,14 @@ func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*model.Account, e  		Emojis:         emojis, // TODO: implement this  		Fields:         fields,  		Suspended:      suspended, -	}, nil +	} + +	// put the account in our cache in case we need it again soon +	if err := c.frontendCache.Store(a.ID, accountFrontend); err != nil { +		return nil, err +	} + +	return accountFrontend, nil  }  func (c *converter) AccountToMastoBlocked(a *gtsmodel.Account) (*model.Account, error) { @@ -302,17 +310,17 @@ func (c *converter) TagToMasto(t *gtsmodel.Tag) (model.Tag, error) {  }  func (c *converter) StatusToMasto(s *gtsmodel.Status, requestingAccount *gtsmodel.Account) (*model.Status, error) { -	repliesCount, err := c.db.GetReplyCountForStatus(s) +	repliesCount, err := c.db.CountStatusReplies(s)  	if err != nil {  		return nil, fmt.Errorf("error counting replies: %s", err)  	} -	reblogsCount, err := c.db.GetReblogCountForStatus(s) +	reblogsCount, err := c.db.CountStatusReblogs(s)  	if err != nil {  		return nil, fmt.Errorf("error counting reblogs: %s", err)  	} -	favesCount, err := c.db.GetFaveCountForStatus(s) +	favesCount, err := c.db.CountStatusFaves(s)  	if err != nil {  		return nil, fmt.Errorf("error counting faves: %s", err)  	} @@ -320,27 +328,27 @@ func (c *converter) StatusToMasto(s *gtsmodel.Status, requestingAccount *gtsmode  	var mastoRebloggedStatus *model.Status  	if s.BoostOfID != "" {  		// the boosted status might have been set on this struct already so check first before doing db calls -		if s.GTSBoostedStatus == nil { +		if s.BoostOf == nil {  			// it's not set so fetch it from the db  			bs := >smodel.Status{}  			if err := c.db.GetByID(s.BoostOfID, bs); err != nil {  				return nil, fmt.Errorf("error getting boosted status with id %s: %s", s.BoostOfID, err)  			} -			s.GTSBoostedStatus = bs +			s.BoostOf = bs  		}  		// the boosted account might have been set on this struct already or passed as a param so check first before doing db calls -		if s.GTSBoostedAccount == nil { +		if s.BoostOfAccount == nil {  			// it's not set so fetch it from the db  			ba := >smodel.Account{} -			if err := c.db.GetByID(s.GTSBoostedStatus.AccountID, ba); err != nil { -				return nil, fmt.Errorf("error getting boosted account %s from status with id %s: %s", s.GTSBoostedStatus.AccountID, s.BoostOfID, err) +			if err := c.db.GetByID(s.BoostOf.AccountID, ba); err != nil { +				return nil, fmt.Errorf("error getting boosted account %s from status with id %s: %s", s.BoostOf.AccountID, s.BoostOfID, err)  			} -			s.GTSBoostedAccount = ba -			s.GTSBoostedStatus.GTSAuthorAccount = ba +			s.BoostOfAccount = ba +			s.BoostOf.Account = ba  		} -		mastoRebloggedStatus, err = c.StatusToMasto(s.GTSBoostedStatus, requestingAccount) +		mastoRebloggedStatus, err = c.StatusToMasto(s.BoostOf, requestingAccount)  		if err != nil {  			return nil, fmt.Errorf("error converting boosted status to mastotype: %s", err)  		} @@ -358,15 +366,15 @@ func (c *converter) StatusToMasto(s *gtsmodel.Status, requestingAccount *gtsmode  		}  	} -	if s.GTSAuthorAccount == nil { +	if s.Account == nil {  		a := >smodel.Account{}  		if err := c.db.GetByID(s.AccountID, a); err != nil {  			return nil, fmt.Errorf("error getting status author: %s", err)  		} -		s.GTSAuthorAccount = a +		s.Account = a  	} -	mastoAuthorAccount, err := c.AccountToMastoPublic(s.GTSAuthorAccount) +	mastoAuthorAccount, err := c.AccountToMastoPublic(s.Account)  	if err != nil {  		return nil, fmt.Errorf("error parsing account of status author: %s", err)  	} @@ -374,8 +382,8 @@ func (c *converter) StatusToMasto(s *gtsmodel.Status, requestingAccount *gtsmode  	mastoAttachments := []model.Attachment{}  	// the status might already have some gts attachments on it if it's not been pulled directly from the database  	// if so, we can directly convert the gts attachments into masto ones -	if s.GTSMediaAttachments != nil { -		for _, gtsAttachment := range s.GTSMediaAttachments { +	if s.Attachments != nil { +		for _, gtsAttachment := range s.Attachments {  			mastoAttachment, err := c.AttachmentToMasto(gtsAttachment)  			if err != nil {  				return nil, fmt.Errorf("error converting attachment with id %s: %s", gtsAttachment.ID, err) @@ -385,7 +393,7 @@ func (c *converter) StatusToMasto(s *gtsmodel.Status, requestingAccount *gtsmode  		// the status doesn't have gts attachments on it, but it does have attachment IDs  		// in this case, we need to pull the gts attachments from the db to convert them into masto ones  	} else { -		for _, a := range s.Attachments { +		for _, a := range s.AttachmentIDs {  			gtsAttachment := >smodel.MediaAttachment{}  			if err := c.db.GetByID(a, gtsAttachment); err != nil {  				return nil, fmt.Errorf("error getting attachment with id %s: %s", a, err) @@ -401,8 +409,8 @@ func (c *converter) StatusToMasto(s *gtsmodel.Status, requestingAccount *gtsmode  	mastoMentions := []model.Mention{}  	// the status might already have some gts mentions on it if it's not been pulled directly from the database  	// if so, we can directly convert the gts mentions into masto ones -	if s.GTSMentions != nil { -		for _, gtsMention := range s.GTSMentions { +	if s.Mentions != nil { +		for _, gtsMention := range s.Mentions {  			mastoMention, err := c.MentionToMasto(gtsMention)  			if err != nil {  				return nil, fmt.Errorf("error converting mention with id %s: %s", gtsMention.ID, err) @@ -412,7 +420,7 @@ func (c *converter) StatusToMasto(s *gtsmodel.Status, requestingAccount *gtsmode  		// the status doesn't have gts mentions on it, but it does have mention IDs  		// in this case, we need to pull the gts mentions from the db to convert them into masto ones  	} else { -		for _, m := range s.Mentions { +		for _, m := range s.MentionIDs {  			gtsMention := >smodel.Mention{}  			if err := c.db.GetByID(m, gtsMention); err != nil {  				return nil, fmt.Errorf("error getting mention with id %s: %s", m, err) @@ -428,8 +436,8 @@ func (c *converter) StatusToMasto(s *gtsmodel.Status, requestingAccount *gtsmode  	mastoTags := []model.Tag{}  	// the status might already have some gts tags on it if it's not been pulled directly from the database  	// if so, we can directly convert the gts tags into masto ones -	if s.GTSTags != nil { -		for _, gtsTag := range s.GTSTags { +	if s.Tags != nil { +		for _, gtsTag := range s.Tags {  			mastoTag, err := c.TagToMasto(gtsTag)  			if err != nil {  				return nil, fmt.Errorf("error converting tag with id %s: %s", gtsTag.ID, err) @@ -439,7 +447,7 @@ func (c *converter) StatusToMasto(s *gtsmodel.Status, requestingAccount *gtsmode  		// the status doesn't have gts tags on it, but it does have tag IDs  		// in this case, we need to pull the gts tags from the db to convert them into masto ones  	} else { -		for _, t := range s.Tags { +		for _, t := range s.TagIDs {  			gtsTag := >smodel.Tag{}  			if err := c.db.GetByID(t, gtsTag); err != nil {  				return nil, fmt.Errorf("error getting tag with id %s: %s", t, err) @@ -455,8 +463,8 @@ func (c *converter) StatusToMasto(s *gtsmodel.Status, requestingAccount *gtsmode  	mastoEmojis := []model.Emoji{}  	// the status might already have some gts emojis on it if it's not been pulled directly from the database  	// if so, we can directly convert the gts emojis into masto ones -	if s.GTSEmojis != nil { -		for _, gtsEmoji := range s.GTSEmojis { +	if s.Emojis != nil { +		for _, gtsEmoji := range s.Emojis {  			mastoEmoji, err := c.EmojiToMasto(gtsEmoji)  			if err != nil {  				return nil, fmt.Errorf("error converting emoji with id %s: %s", gtsEmoji.ID, err) @@ -466,7 +474,7 @@ func (c *converter) StatusToMasto(s *gtsmodel.Status, requestingAccount *gtsmode  		// the status doesn't have gts emojis on it, but it does have emoji IDs  		// in this case, we need to pull the gts emojis from the db to convert them into masto ones  	} else { -		for _, e := range s.Emojis { +		for _, e := range s.EmojiIDs {  			gtsEmoji := >smodel.Emoji{}  			if err := c.db.GetByID(e, gtsEmoji); err != nil {  				return nil, fmt.Errorf("error getting emoji with id %s: %s", e, err) @@ -559,17 +567,17 @@ func (c *converter) InstanceToMasto(i *gtsmodel.Instance) (*model.Instance, erro  		statusCountKey := "status_count"  		domainCountKey := "domain_count" -		userCount, err := c.db.GetUserCountForInstance(c.config.Host) +		userCount, err := c.db.CountInstanceUsers(c.config.Host)  		if err == nil {  			mi.Stats[userCountKey] = userCount  		} -		statusCount, err := c.db.GetStatusCountForInstance(c.config.Host) +		statusCount, err := c.db.CountInstanceStatuses(c.config.Host)  		if err == nil {  			mi.Stats[statusCountKey] = statusCount  		} -		domainCount, err := c.db.GetDomainCountForInstance(c.config.Host) +		domainCount, err := c.db.CountInstanceDomains(c.config.Host)  		if err == nil {  			mi.Stats[domainCountKey] = domainCount  		} @@ -585,13 +593,10 @@ func (c *converter) InstanceToMasto(i *gtsmodel.Instance) (*model.Instance, erro  	}  	// get the instance account if it exists and just skip if it doesn't -	ia := >smodel.Account{} -	if err := c.db.GetWhere([]db.Where{{Key: "username", Value: i.Domain}}, ia); err == nil { -		// instance account exists, get the header for the account if it exists -		attachment := >smodel.MediaAttachment{} -		if err := c.db.GetHeaderForAccountID(attachment, ia.ID); err == nil { -			// header exists, set it on the api model -			mi.Thumbnail = attachment.URL +	ia, err := c.db.GetInstanceAccount("") +	if err == nil { +		if ia.HeaderMediaAttachment != nil { +			mi.Thumbnail = ia.HeaderMediaAttachment.URL  		}  	} @@ -628,47 +633,47 @@ func (c *converter) RelationshipToMasto(r *gtsmodel.Relationship) (*model.Relati  }  func (c *converter) NotificationToMasto(n *gtsmodel.Notification) (*model.Notification, error) { - -	if n.GTSTargetAccount == nil { -		tAccount := >smodel.Account{} -		if err := c.db.GetByID(n.TargetAccountID, tAccount); err != nil { +	if n.TargetAccount == nil { +		tAccount, err := c.db.GetAccountByID(n.TargetAccountID) +		if err != nil {  			return nil, fmt.Errorf("NotificationToMasto: error getting target account with id %s from the db: %s", n.TargetAccountID, err)  		} -		n.GTSTargetAccount = tAccount +		n.TargetAccount = tAccount  	} -	if n.GTSOriginAccount == nil { -		ogAccount := >smodel.Account{} -		if err := c.db.GetByID(n.OriginAccountID, ogAccount); err != nil { +	if n.OriginAccount == nil { +		ogAccount, err := c.db.GetAccountByID(n.OriginAccountID) +		if err != nil {  			return nil, fmt.Errorf("NotificationToMasto: error getting origin account with id %s from the db: %s", n.OriginAccountID, err)  		} -		n.GTSOriginAccount = ogAccount +		n.OriginAccount = ogAccount  	} -	mastoAccount, err := c.AccountToMastoPublic(n.GTSOriginAccount) + +	mastoAccount, err := c.AccountToMastoPublic(n.OriginAccount)  	if err != nil {  		return nil, fmt.Errorf("NotificationToMasto: error converting account to masto: %s", err)  	}  	var mastoStatus *model.Status  	if n.StatusID != "" { -		if n.GTSStatus == nil { -			status := >smodel.Status{} -			if err := c.db.GetByID(n.StatusID, status); err != nil { +		if n.Status == nil { +			status, err := c.db.GetStatusByID(n.StatusID) +			if err != nil {  				return nil, fmt.Errorf("NotificationToMasto: error getting status with id %s from the db: %s", n.StatusID, err)  			} -			n.GTSStatus = status +			n.Status = status  		} -		if n.GTSStatus.GTSAuthorAccount == nil { -			if n.GTSStatus.AccountID == n.GTSTargetAccount.ID { -				n.GTSStatus.GTSAuthorAccount = n.GTSTargetAccount -			} else if n.GTSStatus.AccountID == n.GTSOriginAccount.ID { -				n.GTSStatus.GTSAuthorAccount = n.GTSOriginAccount +		if n.Status.Account == nil { +			if n.Status.AccountID == n.TargetAccount.ID { +				n.Status.Account = n.TargetAccount +			} else if n.Status.AccountID == n.OriginAccount.ID { +				n.Status.Account = n.OriginAccount  			}  		}  		var err error -		mastoStatus, err = c.StatusToMasto(n.GTSStatus, nil) +		mastoStatus, err = c.StatusToMasto(n.Status, nil)  		if err != nil {  			return nil, fmt.Errorf("NotificationToMasto: error converting status to masto: %s", err)  		} diff --git a/internal/typeutils/util.go b/internal/typeutils/util.go index 1e13f0713..5751fbc84 100644 --- a/internal/typeutils/util.go +++ b/internal/typeutils/util.go @@ -10,25 +10,25 @@ func (c *converter) interactionsWithStatusForAccount(s *gtsmodel.Status, request  	si := &statusInteractions{}  	if requestingAccount != nil { -		faved, err := c.db.StatusFavedBy(s, requestingAccount.ID) +		faved, err := c.db.IsStatusFavedBy(s, requestingAccount.ID)  		if err != nil {  			return nil, fmt.Errorf("error checking if requesting account has faved status: %s", err)  		}  		si.Faved = faved -		reblogged, err := c.db.StatusRebloggedBy(s, requestingAccount.ID) +		reblogged, err := c.db.IsStatusRebloggedBy(s, requestingAccount.ID)  		if err != nil {  			return nil, fmt.Errorf("error checking if requesting account has reblogged status: %s", err)  		}  		si.Reblogged = reblogged -		muted, err := c.db.StatusMutedBy(s, requestingAccount.ID) +		muted, err := c.db.IsStatusMutedBy(s, requestingAccount.ID)  		if err != nil {  			return nil, fmt.Errorf("error checking if requesting account has muted status: %s", err)  		}  		si.Muted = muted -		bookmarked, err := c.db.StatusBookmarkedBy(s, requestingAccount.ID) +		bookmarked, err := c.db.IsStatusBookmarkedBy(s, requestingAccount.ID)  		if err != nil {  			return nil, fmt.Errorf("error checking if requesting account has bookmarked status: %s", err)  		} diff --git a/internal/util/statustools.go b/internal/util/statustools.go index ce5860c6d..4a89e60f6 100644 --- a/internal/util/statustools.go +++ b/internal/util/statustools.go @@ -34,7 +34,7 @@ func DeriveMentionsFromStatus(status string) []string {  	for _, m := range mentionFinderRegex.FindAllStringSubmatch(status, -1) {  		mentionedAccounts = append(mentionedAccounts, m[1])  	} -	return unique(mentionedAccounts) +	return UniqueStrings(mentionedAccounts)  }  // DeriveHashtagsFromStatus takes a plaintext (ie., not html-formatted) status, @@ -46,7 +46,7 @@ func DeriveHashtagsFromStatus(status string) []string {  	for _, m := range HashtagFinderRegex.FindAllStringSubmatch(status, -1) {  		tags = append(tags, strings.TrimPrefix(m[1], "#"))  	} -	return unique(tags) +	return UniqueStrings(tags)  }  // DeriveEmojisFromStatus takes a plaintext (ie., not html-formatted) status, @@ -57,7 +57,7 @@ func DeriveEmojisFromStatus(status string) []string {  	for _, m := range emojiFinderRegex.FindAllStringSubmatch(status, -1) {  		emojis = append(emojis, m[1])  	} -	return unique(emojis) +	return UniqueStrings(emojis)  }  // ExtractMentionParts extracts the username test_user and the domain example.org @@ -79,16 +79,3 @@ func ExtractMentionParts(mention string) (username, domain string, err error) {  func IsMention(mention string) bool {  	return mentionNameRegex.MatchString(strings.ToLower(mention))  } - -// unique returns a deduplicated version of a given string slice. -func unique(s []string) []string { -	keys := make(map[string]bool) -	list := []string{} -	for _, entry := range s { -		if _, value := keys[entry]; !value { -			keys[entry] = true -			list = append(list, entry) -		} -	} -	return list -} diff --git a/internal/util/unique.go b/internal/util/unique.go new file mode 100644 index 000000000..d679515d0 --- /dev/null +++ b/internal/util/unique.go @@ -0,0 +1,32 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +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 <http://www.gnu.org/licenses/>. +*/ +  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 <http://www.gnu.org/licenses/>. +*/ + +package visibility + +import ( +	"errors" +	"fmt" + +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +// relevantAccounts denotes accounts that are replied to, boosted by, or mentioned in a status. +type relevantAccounts struct { +	// Who wrote the status +	Account *gtsmodel.Account +	// Who is the status replying to +	InReplyToAccount *gtsmodel.Account +	// Which accounts are mentioned (tagged) in the status +	MentionedAccounts []*gtsmodel.Account +	// Who authed the boosted status +	BoostedAccount *gtsmodel.Account +	// If the boosted status replies to another account, who does it reply to? +	BoostedInReplyToAccount *gtsmodel.Account +	// Who is mentioned (tagged) in the boosted status +	BoostedMentionedAccounts []*gtsmodel.Account +} + +func (f *filter) relevantAccounts(status *gtsmodel.Status, getBoosted bool) (*relevantAccounts, error) { +	relAccts := &relevantAccounts{ +		MentionedAccounts:        []*gtsmodel.Account{}, +		BoostedMentionedAccounts: []*gtsmodel.Account{}, +	} + +	/* +		Here's what we need to try and extract from the status: + +			// 1. Who wrote the status +		    Account *gtsmodel.Account + +		    // 2. Who is the status replying to +		    InReplyToAccount *gtsmodel.Account + +		    // 3. Which accounts are mentioned (tagged) in the status +		    MentionedAccounts []*gtsmodel.Account + +			if getBoosted: +				// 4. Who wrote the boosted status +				BoostedAccount *gtsmodel.Account + +				// 5. If the boosted status replies to another account, who does it reply to? +				BoostedInReplyToAccount *gtsmodel.Account + +				// 6. Who is mentioned (tagged) in the boosted status +				BoostedMentionedAccounts []*gtsmodel.Account +	*/ + +	// 1. Account. +	// Account might be set on the status already +	if status.Account != nil { +		// it was set +		relAccts.Account = status.Account +	} else { +		// it wasn't set, so get it from the db +		account, err := f.db.GetAccountByID(status.AccountID) +		if err != nil { +			return nil, fmt.Errorf("relevantAccounts: error getting account with id %s: %s", status.AccountID, err) +		} +		// set it on the status in case we need it further along +		status.Account = account +		// set it on relevant accounts +		relAccts.Account = account +	} + +	// 2. InReplyToAccount +	// only get this if InReplyToAccountID is set +	if status.InReplyToAccountID != "" { +		// InReplyToAccount might be set on the status already +		if status.InReplyToAccount != nil { +			// it was set +			relAccts.InReplyToAccount = status.InReplyToAccount +		} else { +			// it wasn't set, so get it from the db +			inReplyToAccount, err := f.db.GetAccountByID(status.InReplyToAccountID) +			if err != nil { +				return nil, fmt.Errorf("relevantAccounts: error getting inReplyToAccount with id %s: %s", status.InReplyToAccountID, err) +			} +			// set it on the status in case we need it further along +			status.InReplyToAccount = inReplyToAccount +			// set it on relevant accounts +			relAccts.InReplyToAccount = inReplyToAccount +		} +	} + +	// 3. MentionedAccounts +	// First check if status.Mentions is populated with all mentions that correspond to status.MentionIDs +	for _, mID := range status.MentionIDs { +		if mID == "" { +			continue +		} +		if !idIn(mID, status.Mentions) { +			// mention with ID isn't in status.Mentions +			mention, err := f.db.GetMention(mID) +			if err != nil { +				return nil, fmt.Errorf("relevantAccounts: error getting mention with id %s: %s", mID, err) +			} +			if mention == nil { +				return nil, fmt.Errorf("relevantAccounts: mention with id %s was nil", mID) +			} +			status.Mentions = append(status.Mentions, mention) +		} +	} +	// now filter mentions to make sure we only have mentions with a corresponding ID +	nm := []*gtsmodel.Mention{} +	for _, m := range status.Mentions { +		if m == nil { +			continue +		} +		if mentionIn(m, status.MentionIDs) { +			nm = append(nm, m) +		} +	} +	status.Mentions = nm + +	if len(status.Mentions) != len(status.MentionIDs) { +		return nil, errors.New("relevantAccounts: mentions length did not correspond with mentionIDs length") +	} + +	// if getBoosted is set, we should check the same properties on the boosted account as well +	if getBoosted { +		// 4, 5, 6. Boosted status items +		// get the boosted status if it's not set on the status already +		if status.BoostOfID != "" && status.BoostOf == nil { +			boostedStatus, err := f.db.GetStatusByID(status.BoostOfID) +			if err != nil { +				return nil, fmt.Errorf("relevantAccounts: error getting boosted status with id %s: %s", status.BoostOfID, err) +			} +			status.BoostOf = boostedStatus +		} + +		if status.BoostOf != nil { +			// return relevant accounts for the boosted status +			boostedRelAccts, err := f.relevantAccounts(status.BoostOf, false) // false because we don't want to recurse +			if err != nil { +				return nil, fmt.Errorf("relevantAccounts: error getting relevant accounts of boosted status %s: %s", status.BoostOf.ID, err) +			} +			relAccts.BoostedAccount = boostedRelAccts.Account +			relAccts.BoostedInReplyToAccount = boostedRelAccts.InReplyToAccount +			relAccts.BoostedMentionedAccounts = boostedRelAccts.MentionedAccounts +		} +	} + +	return relAccts, nil +} + +// domainBlockedRelevant checks through all relevant accounts attached to a status +// to make sure none of them are domain blocked by this instance. +func (f *filter) domainBlockedRelevant(r *relevantAccounts) (bool, error) { +	domains := []string{} + +	if r.Account != nil { +		domains = append(domains, r.Account.Domain) +	} + +	if r.InReplyToAccount != nil { +		domains = append(domains, r.InReplyToAccount.Domain) +	} + +	for _, a := range r.MentionedAccounts { +		if a != nil { +			domains = append(domains, a.Domain) +		} +	} + +	if r.BoostedAccount != nil { +		domains = append(domains, r.BoostedAccount.Domain) +	} + +	if r.BoostedInReplyToAccount != nil { +		domains = append(domains, r.BoostedInReplyToAccount.Domain) +	} + +	for _, a := range r.BoostedMentionedAccounts { +		if a != nil { +			domains = append(domains, a.Domain) +		} +	} + +	return f.db.AreDomainsBlocked(domains) +} + +func idIn(id string, mentions []*gtsmodel.Mention) bool { +	for _, m := range mentions { +		if m == nil { +			continue +		} +		if m.ID == id { +			return true +		} +	} +	return false +} + +func mentionIn(mention *gtsmodel.Mention, ids []string) bool { +	if mention == nil { +		return false +	} +	for _, i := range ids { +		if mention.ID == i { +			return true +		} +	} +	return false +} diff --git a/internal/visibility/statushometimelineable.go b/internal/visibility/statushometimelineable.go index bc5f7bcb8..a3ca62fb3 100644 --- a/internal/visibility/statushometimelineable.go +++ b/internal/visibility/statushometimelineable.go @@ -1,3 +1,21 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ +  package visibility  import ( @@ -28,6 +46,13 @@ func (f *filter) StatusHometimelineable(targetStatus *gtsmodel.Status, timelineO  		return false, nil  	} +	for _, m := range targetStatus.Mentions { +		if m.TargetAccountID == timelineOwnerAccount.ID { +			// if we're mentioned we should be able to see the post +			return true, nil +		} +	} +  	// Don't timeline a status whose parent hasn't been dereferenced yet or can't be dereferenced.  	// If we have the reply to URI but don't have an ID for the replied-to account or the replied-to status in our database, we haven't dereferenced it yet.  	if targetStatus.InReplyToURI != "" && (targetStatus.InReplyToID == "" || targetStatus.InReplyToAccountID == "") { @@ -37,21 +62,21 @@ func (f *filter) StatusHometimelineable(targetStatus *gtsmodel.Status, timelineO  	// if a status replies to an ID we know in the database, we need to make sure we also follow the replied-to status owner account  	if targetStatus.InReplyToID != "" {  		// pin the reply to status on to this status if it hasn't been done already -		if targetStatus.GTSReplyToStatus == nil { -			rs := >smodel.Status{} -			if err := f.db.GetByID(targetStatus.InReplyToID, rs); err != nil { +		if targetStatus.InReplyTo == nil { +			rs, err := f.db.GetStatusByID(targetStatus.InReplyToID) +			if err != nil {  				return false, fmt.Errorf("StatusHometimelineable: error getting replied to status with id %s: %s", targetStatus.InReplyToID, err)  			} -			targetStatus.GTSReplyToStatus = rs +			targetStatus.InReplyTo = rs  		}  		// pin the reply to account on to this status if it hasn't been done already -		if targetStatus.GTSReplyToAccount == nil { -			ra := >smodel.Account{} -			if err := f.db.GetByID(targetStatus.InReplyToAccountID, ra); err != nil { +		if targetStatus.InReplyToAccount == nil { +			ra, err := f.db.GetAccountByID(targetStatus.InReplyToAccountID) +			if err != nil {  				return false, fmt.Errorf("StatusHometimelineable: error getting replied to account with id %s: %s", targetStatus.InReplyToAccountID, err)  			} -			targetStatus.GTSReplyToAccount = ra +			targetStatus.InReplyToAccount = ra  		}  		// if it's a reply to the timelineOwnerAccount, we don't need to check if the timelineOwnerAccount follows itself, just return true, they can see it @@ -60,7 +85,7 @@ func (f *filter) StatusHometimelineable(targetStatus *gtsmodel.Status, timelineO  		}  		// the replied-to account != timelineOwnerAccount, so make sure the timelineOwnerAccount follows the replied-to account -		follows, err := f.db.Follows(timelineOwnerAccount, targetStatus.GTSReplyToAccount) +		follows, err := f.db.IsFollowing(timelineOwnerAccount, targetStatus.InReplyToAccount)  		if err != nil {  			return false, fmt.Errorf("StatusHometimelineable: error checking follow from account %s to account %s: %s", timelineOwnerAccount.ID, targetStatus.InReplyToAccountID, err)  		} diff --git a/internal/visibility/statuspublictimelineable.go b/internal/visibility/statuspublictimelineable.go index d7f68faee..f07e06aae 100644 --- a/internal/visibility/statuspublictimelineable.go +++ b/internal/visibility/statuspublictimelineable.go @@ -1,3 +1,21 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ +  package visibility  import ( diff --git a/internal/visibility/statusvisible.go b/internal/visibility/statusvisible.go index dc6b74702..15e545881 100644 --- a/internal/visibility/statusvisible.go +++ b/internal/visibility/statusvisible.go @@ -1,3 +1,21 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ +  package visibility  import ( @@ -16,10 +34,11 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount  		"statusID": targetStatus.ID,  	}) -	relevantAccounts, err := f.pullRelevantAccountsFromStatus(targetStatus) +	getBoosted := true +	relevantAccounts, err := f.relevantAccounts(targetStatus, getBoosted)  	if err != nil {  		l.Debugf("error pulling relevant accounts for status %s: %s", targetStatus.ID, err) -		return false, fmt.Errorf("error pulling relevant accounts for status %s: %s", targetStatus.ID, err) +		return false, fmt.Errorf("StatusVisible: error pulling relevant accounts for status %s: %s", targetStatus.ID, err)  	}  	domainBlocked, err := f.domainBlockedRelevant(relevantAccounts) @@ -32,7 +51,12 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount  		return false, nil  	} -	targetAccount := relevantAccounts.StatusAuthor +	targetAccount := relevantAccounts.Account +	if targetAccount == nil { +		l.Trace("target account is not set") +		return false, nil +	} +  	// if target account is suspended then don't show the status  	if !targetAccount.SuspendedAt.IsZero() {  		l.Trace("target account suspended at is not zero") @@ -45,7 +69,7 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount  		targetUser := >smodel.User{}  		if err := f.db.GetWhere([]db.Where{{Key: "account_id", Value: targetAccount.ID}}, targetUser); err != nil {  			l.Debug("target user could not be selected") -			if _, ok := err.(db.ErrNoEntries); ok { +			if err == db.ErrNoEntries {  				return false, nil  			}  			return false, fmt.Errorf("StatusVisible: db error selecting user for local target account %s: %s", targetAccount.ID, err) @@ -76,7 +100,7 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount  		if err := f.db.GetWhere([]db.Where{{Key: "account_id", Value: requestingAccount.ID}}, requestingUser); err != nil {  			// if the requesting account is local but doesn't have a corresponding user in the db this is a problem  			l.Debug("requesting user could not be selected") -			if _, ok := err.(db.ErrNoEntries); ok { +			if err == db.ErrNoEntries {  				return false, nil  			}  			return false, fmt.Errorf("StatusVisible: db error selecting user for local requesting account %s: %s", requestingAccount.ID, err) @@ -102,7 +126,7 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount  	// At this point we have a populated targetAccount, targetStatus, and requestingAccount, so we can check for blocks and whathaveyou  	// First check if a block exists directly between the target account (which authored the status) and the requesting account. -	if blocked, err := f.db.Blocked(targetAccount.ID, requestingAccount.ID); err != nil { +	if blocked, err := f.db.IsBlocked(targetAccount.ID, requestingAccount.ID, true); err != nil {  		l.Debugf("something went wrong figuring out if the accounts have a block: %s", err)  		return false, err  	} else if blocked { @@ -112,8 +136,8 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount  	}  	// status replies to account id -	if relevantAccounts.ReplyToAccount != nil && relevantAccounts.ReplyToAccount.ID != requestingAccount.ID { -		if blocked, err := f.db.Blocked(relevantAccounts.ReplyToAccount.ID, requestingAccount.ID); err != nil { +	if relevantAccounts.InReplyToAccount != nil && relevantAccounts.InReplyToAccount.ID != requestingAccount.ID { +		if blocked, err := f.db.IsBlocked(relevantAccounts.InReplyToAccount.ID, requestingAccount.ID, true); err != nil {  			return false, err  		} else if blocked {  			l.Trace("a block exists between requesting account and reply to account") @@ -122,7 +146,7 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount  		// check reply to ID  		if targetStatus.InReplyToID != "" && (targetStatus.Visibility == gtsmodel.VisibilityFollowersOnly || targetStatus.Visibility == gtsmodel.VisibilityDirect) { -			followsRepliedAccount, err := f.db.Follows(requestingAccount, relevantAccounts.ReplyToAccount) +			followsRepliedAccount, err := f.db.IsFollowing(requestingAccount, relevantAccounts.InReplyToAccount)  			if err != nil {  				return false, err  			} @@ -134,8 +158,8 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount  	}  	// status boosts accounts id -	if relevantAccounts.BoostedStatusAuthor != nil { -		if blocked, err := f.db.Blocked(relevantAccounts.BoostedStatusAuthor.ID, requestingAccount.ID); err != nil { +	if relevantAccounts.BoostedAccount != nil { +		if blocked, err := f.db.IsBlocked(relevantAccounts.BoostedAccount.ID, requestingAccount.ID, true); err != nil {  			return false, err  		} else if blocked {  			l.Trace("a block exists between requesting account and boosted account") @@ -144,8 +168,8 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount  	}  	// status boosts a reply to account id -	if relevantAccounts.BoostedReplyToAccount != nil { -		if blocked, err := f.db.Blocked(relevantAccounts.BoostedReplyToAccount.ID, requestingAccount.ID); err != nil { +	if relevantAccounts.BoostedInReplyToAccount != nil { +		if blocked, err := f.db.IsBlocked(relevantAccounts.BoostedInReplyToAccount.ID, requestingAccount.ID, true); err != nil {  			return false, err  		} else if blocked {  			l.Trace("a block exists between requesting account and boosted reply to account") @@ -155,7 +179,10 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount  	// status mentions accounts  	for _, a := range relevantAccounts.MentionedAccounts { -		if blocked, err := f.db.Blocked(a.ID, requestingAccount.ID); err != nil { +		if a == nil { +			continue +		} +		if blocked, err := f.db.IsBlocked(a.ID, requestingAccount.ID, true); err != nil {  			return false, err  		} else if blocked {  			l.Trace("a block exists between requesting account and a mentioned account") @@ -165,7 +192,10 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount  	// boost mentions accounts  	for _, a := range relevantAccounts.BoostedMentionedAccounts { -		if blocked, err := f.db.Blocked(a.ID, requestingAccount.ID); err != nil { +		if a == nil { +			continue +		} +		if blocked, err := f.db.IsBlocked(a.ID, requestingAccount.ID, true); err != nil {  			return false, err  		} else if blocked {  			l.Trace("a block exists between requesting account and a boosted mentioned account") @@ -175,6 +205,9 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount  	// if the requesting account is mentioned in the status it should always be visible  	for _, acct := range relevantAccounts.MentionedAccounts { +		if acct == nil { +			continue +		}  		if acct.ID == requestingAccount.ID {  			return true, nil // yep it's mentioned!  		} @@ -188,7 +221,7 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount  		return true, nil  	case gtsmodel.VisibilityFollowersOnly:  		// check one-way follow -		follows, err := f.db.Follows(requestingAccount, targetAccount) +		follows, err := f.db.IsFollowing(requestingAccount, targetAccount)  		if err != nil {  			return false, err  		} @@ -199,7 +232,7 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount  		return true, nil  	case gtsmodel.VisibilityMutualsOnly:  		// check mutual follow -		mutuals, err := f.db.Mutuals(requestingAccount, targetAccount) +		mutuals, err := f.db.IsMutualFollowing(requestingAccount, targetAccount)  		if err != nil {  			return false, err  		} diff --git a/internal/visibility/util.go b/internal/visibility/util.go deleted file mode 100644 index a12dd555f..000000000 --- a/internal/visibility/util.go +++ /dev/null @@ -1,191 +0,0 @@ -package visibility - -import ( -	"fmt" - -	"github.com/superseriousbusiness/gotosocial/internal/db" -	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func (f *filter) pullRelevantAccountsFromStatus(targetStatus *gtsmodel.Status) (*relevantAccounts, error) { -	accounts := &relevantAccounts{ -		MentionedAccounts:        []*gtsmodel.Account{}, -		BoostedMentionedAccounts: []*gtsmodel.Account{}, -	} - -	// get the author account -	if targetStatus.GTSAuthorAccount == nil { -		statusAuthor := >smodel.Account{} -		if err := f.db.GetByID(targetStatus.AccountID, statusAuthor); err != nil { -			return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting statusAuthor with id %s: %s", targetStatus.AccountID, err) -		} -		targetStatus.GTSAuthorAccount = statusAuthor -	} -	accounts.StatusAuthor = targetStatus.GTSAuthorAccount - -	// get the replied to account from the status and add it to the pile -	if targetStatus.InReplyToAccountID != "" { -		repliedToAccount := >smodel.Account{} -		if err := f.db.GetByID(targetStatus.InReplyToAccountID, repliedToAccount); err != nil { -			return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting repliedToAcount with id %s: %s", targetStatus.InReplyToAccountID, err) -		} -		accounts.ReplyToAccount = repliedToAccount -	} - -	// now get all accounts with IDs that are mentioned in the status -	for _, mentionID := range targetStatus.Mentions { - -		mention := >smodel.Mention{} -		if err := f.db.GetByID(mentionID, mention); err != nil { -			return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting mention with id %s: %s", mentionID, err) -		} - -		mentionedAccount := >smodel.Account{} -		if err := f.db.GetByID(mention.TargetAccountID, mentionedAccount); err != nil { -			return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting mentioned account: %s", err) -		} -		accounts.MentionedAccounts = append(accounts.MentionedAccounts, mentionedAccount) -	} - -	// get the boosted account from the status and add it to the pile -	if targetStatus.BoostOfID != "" { -		// retrieve the boosted status first -		boostedStatus := >smodel.Status{} -		if err := f.db.GetByID(targetStatus.BoostOfID, boostedStatus); err != nil { -			return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boostedStatus with id %s: %s", targetStatus.BoostOfID, err) -		} -		boostedAccount := >smodel.Account{} -		if err := f.db.GetByID(boostedStatus.AccountID, boostedAccount); err != nil { -			return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boostedAccount with id %s: %s", boostedStatus.AccountID, err) -		} -		accounts.BoostedStatusAuthor = boostedAccount - -		// the boosted status might be a reply to another account so we should get that too -		if boostedStatus.InReplyToAccountID != "" { -			boostedStatusRepliedToAccount := >smodel.Account{} -			if err := f.db.GetByID(boostedStatus.InReplyToAccountID, boostedStatusRepliedToAccount); err != nil { -				return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boostedStatusRepliedToAccount with id %s: %s", boostedStatus.InReplyToAccountID, err) -			} -			accounts.BoostedReplyToAccount = boostedStatusRepliedToAccount -		} - -		// now get all accounts with IDs that are mentioned in the status -		for _, mentionID := range boostedStatus.Mentions { -			mention := >smodel.Mention{} -			if err := f.db.GetByID(mentionID, mention); err != nil { -				return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boosted mention with id %s: %s", mentionID, err) -			} - -			mentionedAccount := >smodel.Account{} -			if err := f.db.GetByID(mention.TargetAccountID, mentionedAccount); err != nil { -				return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boosted mentioned account: %s", err) -			} -			accounts.BoostedMentionedAccounts = append(accounts.BoostedMentionedAccounts, mentionedAccount) -		} -	} - -	return accounts, nil -} - -// relevantAccounts denotes accounts that are replied to, boosted by, or mentioned in a status. -type relevantAccounts struct { -	// Who wrote the status -	StatusAuthor *gtsmodel.Account -	// Who is the status replying to -	ReplyToAccount *gtsmodel.Account -	// Which accounts are mentioned (tagged) in the status -	MentionedAccounts []*gtsmodel.Account -	// Who authed the boosted status -	BoostedStatusAuthor *gtsmodel.Account -	// If the boosted status replies to another account, who does it reply to? -	BoostedReplyToAccount *gtsmodel.Account -	// Who is mentioned (tagged) in the boosted status -	BoostedMentionedAccounts []*gtsmodel.Account -} - -// blockedDomain checks whether the given domain is blocked by us or not -func (f *filter) blockedDomain(host string) (bool, error) { -	b := >smodel.DomainBlock{} -	err := f.db.GetWhere([]db.Where{{Key: "domain", Value: host, CaseInsensitive: true}}, b) -	if err == nil { -		// block exists -		return true, nil -	} - -	if _, ok := err.(db.ErrNoEntries); ok { -		// there are no entries so there's no block -		return false, nil -	} - -	// there's an actual error -	return false, err -} - -// domainBlockedRelevant checks through all relevant accounts attached to a status -// to make sure none of them are domain blocked by this instance. -// -// Will return true+nil if there's a block, false+nil if there's no block, or -// an error if something goes wrong. -func (f *filter) domainBlockedRelevant(r *relevantAccounts) (bool, error) { -	if r.StatusAuthor != nil { -		b, err := f.blockedDomain(r.StatusAuthor.Domain) -		if err != nil { -			return false, err -		} -		if b { -			return true, nil -		} -	} - -	if r.ReplyToAccount != nil { -		b, err := f.blockedDomain(r.ReplyToAccount.Domain) -		if err != nil { -			return false, err -		} -		if b { -			return true, nil -		} -	} - -	for _, a := range r.MentionedAccounts { -		b, err := f.blockedDomain(a.Domain) -		if err != nil { -			return false, err -		} -		if b { -			return true, nil -		} -	} - -	if r.BoostedStatusAuthor != nil { -		b, err := f.blockedDomain(r.BoostedStatusAuthor.Domain) -		if err != nil { -			return false, err -		} -		if b { -			return true, nil -		} -	} - -	if r.BoostedReplyToAccount != nil { -		b, err := f.blockedDomain(r.BoostedReplyToAccount.Domain) -		if err != nil { -			return false, err -		} -		if b { -			return true, nil -		} -	} - -	for _, a := range r.BoostedMentionedAccounts { -		b, err := f.blockedDomain(a.Domain) -		if err != nil { -			return false, err -		} -		if b { -			return true, nil -		} -	} - -	return false, nil -}  | 
