diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml index faf9f181e..40a2caa5e 100644 --- a/docs/api/swagger.yaml +++ b/docs/api/swagger.yaml @@ -1679,7 +1679,7 @@ info: name: AGPL3 url: https://www.gnu.org/licenses/agpl-3.0.en.html title: GoToSocial - version: 0.1.0-SNAPSHOT-dereference_remote_replies + version: 0.1.0-SNAPSHOT paths: /api/v1/accounts: post: @@ -3404,6 +3404,8 @@ paths: description: "" schema: $ref: '#/definitions/swaggerStatusRepliesCollection' + "400": + description: bad request "401": description: unauthorized "403": diff --git a/internal/api/security/signaturecheck.go b/internal/api/security/signaturecheck.go index a3a04180d..88b0b4dff 100644 --- a/internal/api/security/signaturecheck.go +++ b/internal/api/security/signaturecheck.go @@ -6,8 +6,6 @@ import ( "github.com/gin-gonic/gin" "github.com/go-fed/httpsig" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -33,13 +31,13 @@ func (m *Module) SignatureCheck(c *gin.Context) { // we managed to parse the url! // if the domain is blocked we want to bail as early as possible - blockedDomain, err := m.blockedDomain(requestingPublicKeyID.Host) + blocked, err := m.db.IsURIBlocked(requestingPublicKeyID) if err != nil { l.Errorf("could not tell if domain %s was blocked or not: %s", requestingPublicKeyID.Host, err) c.AbortWithStatus(http.StatusInternalServerError) return } - if blockedDomain { + if blocked { l.Infof("domain %s is blocked", requestingPublicKeyID.Host) c.AbortWithStatus(http.StatusForbidden) return @@ -50,20 +48,3 @@ func (m *Module) SignatureCheck(c *gin.Context) { } } } - -func (m *Module) blockedDomain(host string) (bool, error) { - b := >smodel.DomainBlock{} - err := m.db.GetWhere([]db.Where{{Key: "domain", Value: host, CaseInsensitive: true}}, b) - if err == nil { - // block exists - return true, nil - } - - if err == db.ErrNoEntries { - // there are no entries so there's no block - return false, nil - } - - // there's an actual error - return false, err -} diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 3f797beb6..5a570cad4 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -35,11 +35,11 @@ type cache struct { // New returns a new in-memory cache. func New() Cache { - cache := &cache{ + cache := &cache{ stored: &sync.Map{}, } - go cache.sweep() - return cache + go cache.sweep() + return cache } type cacheEntry struct { diff --git a/internal/cache/error.go b/internal/cache/error.go index df7cd8710..3f32aa7ce 100644 --- a/internal/cache/error.go +++ b/internal/cache/error.go @@ -20,8 +20,8 @@ package cache import "errors" -// CacheError models an error returned by the in-memory cache. -type CacheError error +// Error models an error returned by the in-memory cache. +type Error error // ErrNotFound means that a value for the requested key was not found in the cache. var ErrNotFound = errors.New("value not found in cache") diff --git a/internal/db/account.go b/internal/db/account.go index 29194b551..15c6839f8 100644 --- a/internal/db/account.go +++ b/internal/db/account.go @@ -24,68 +24,69 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) +// Account contains functions related to account getting/setting/creation. type Account interface { // GetAccountByID returns one account with the given ID, or an error if something goes wrong. - GetAccountByID(id string) (*gtsmodel.Account, DBError) + GetAccountByID(id string) (*gtsmodel.Account, Error) // GetAccountByURI returns one account with the given URI, or an error if something goes wrong. - GetAccountByURI(uri string) (*gtsmodel.Account, DBError) + GetAccountByURI(uri string) (*gtsmodel.Account, Error) // GetAccountByURL returns one account with the given URL, or an error if something goes wrong. - GetAccountByURL(uri string) (*gtsmodel.Account, DBError) + GetAccountByURL(uri string) (*gtsmodel.Account, Error) // GetLocalAccountByUsername is a shortcut for the common action of fetching an account ON THIS INSTANCE // according to its username, which should be unique. // The given account pointer will be set to the result of the query, whatever it is. // In case of no entries, a 'no entries' error will be returned - GetLocalAccountByUsername(username string) (*gtsmodel.Account, DBError) + GetLocalAccountByUsername(username string) (*gtsmodel.Account, Error) // GetAccountFollowRequests is a shortcut for the common action of fetching a list of follow requests targeting the given account ID. // The given slice 'followRequests' will be set to the result of the query, whatever it is. // In case of no entries, a 'no entries' error will be returned - GetAccountFollowRequests(accountID string, followRequests *[]gtsmodel.FollowRequest) DBError + GetAccountFollowRequests(accountID string, followRequests *[]gtsmodel.FollowRequest) Error // GetAccountFollowing is a shortcut for the common action of fetching a list of accounts that accountID is following. // The given slice 'following' will be set to the result of the query, whatever it is. // In case of no entries, a 'no entries' error will be returned - GetAccountFollowing(accountID string, following *[]gtsmodel.Follow) DBError + GetAccountFollowing(accountID string, following *[]gtsmodel.Follow) Error - CountAccountFollowing(accountID string, localOnly bool) (int, DBError) + CountAccountFollowing(accountID string, localOnly bool) (int, Error) // GetAccountFollowers is a shortcut for the common action of fetching a list of accounts that accountID is followed by. // The given slice 'followers' will be set to the result of the query, whatever it is. // In case of no entries, a 'no entries' error will be returned // // If localOnly is set to true, then only followers from *this instance* will be returned. - GetAccountFollowers(accountID string, followers *[]gtsmodel.Follow, localOnly bool) DBError + GetAccountFollowers(accountID string, followers *[]gtsmodel.Follow, localOnly bool) Error - CountAccountFollowers(accountID string, localOnly bool) (int, DBError) + CountAccountFollowers(accountID string, localOnly bool) (int, Error) // GetAccountFaves is a shortcut for the common action of fetching a list of faves made by the given accountID. // The given slice 'faves' will be set to the result of the query, whatever it is. // In case of no entries, a 'no entries' error will be returned - GetAccountFaves(accountID string, faves *[]gtsmodel.StatusFave) DBError + GetAccountFaves(accountID string, faves *[]gtsmodel.StatusFave) Error // GetAccountStatusesCount is a shortcut for the common action of counting statuses produced by accountID. - CountAccountStatuses(accountID string) (int, DBError) + CountAccountStatuses(accountID string) (int, Error) // GetAccountStatuses is a shortcut for getting the most recent statuses. accountID is optional, if not provided // then all statuses will be returned. If limit is set to 0, the size of the returned slice will not be limited. This can // be very memory intensive so you probably shouldn't do this! // In case of no entries, a 'no entries' error will be returned - GetAccountStatuses(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, DBError) + GetAccountStatuses(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, Error) - GetAccountBlocks(accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, DBError) + GetAccountBlocks(accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, Error) // GetAccountLastPosted simply gets the timestamp of the most recent post by the account. // // The returned time will be zero if account has never posted anything. - GetAccountLastPosted(accountID string) (time.Time, DBError) + GetAccountLastPosted(accountID string) (time.Time, Error) // SetAccountHeaderOrAvatar sets the header or avatar for the given accountID to the given media attachment. - SetAccountHeaderOrAvatar(mediaAttachment *gtsmodel.MediaAttachment, accountID string) DBError + SetAccountHeaderOrAvatar(mediaAttachment *gtsmodel.MediaAttachment, accountID string) Error // GetInstanceAccount returns the instance account for the given domain. // If domain is empty, this instance account will be returned. - GetInstanceAccount(domain string) (*gtsmodel.Account, DBError) + GetInstanceAccount(domain string) (*gtsmodel.Account, Error) } diff --git a/internal/db/admin.go b/internal/db/admin.go index c9cc96117..aa2b22f47 100644 --- a/internal/db/admin.go +++ b/internal/db/admin.go @@ -24,29 +24,30 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) +// Admin contains functions related to instance administration (new signups etc). type Admin interface { // IsUsernameAvailable checks whether a given username is available on our domain. // Returns an error if the username is already taken, or something went wrong in the db. - IsUsernameAvailable(username string) DBError + IsUsernameAvailable(username string) Error // IsEmailAvailable checks whether a given email address for a new account is available to be used on our domain. // Return an error if: // A) the email is already associated with an account // B) we block signups from this email domain // C) something went wrong in the db - IsEmailAvailable(email string) DBError + IsEmailAvailable(email string) Error // NewSignup creates a new user in the database with the given parameters. // By the time this function is called, it should be assumed that all the parameters have passed validation! - NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string, emailVerified bool, admin bool) (*gtsmodel.User, DBError) + NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string, emailVerified bool, admin bool) (*gtsmodel.User, Error) // CreateInstanceAccount creates an account in the database with the same username as the instance host value. // Ie., if the instance is hosted at 'example.org' the instance user will have a username of 'example.org'. // This is needed for things like serving files that belong to the instance and not an individual user/account. - CreateInstanceAccount() DBError + CreateInstanceAccount() Error // CreateInstanceInstance creates an instance in the database with the same domain as the instance host value. // Ie., if the instance is hosted at 'example.org' the instance will have a domain of 'example.org'. // This is needed for things like serving instance information through /api/v1/instance - CreateInstanceInstance() DBError + CreateInstanceInstance() Error } diff --git a/internal/db/basic.go b/internal/db/basic.go index 558bf8d91..729920bba 100644 --- a/internal/db/basic.go +++ b/internal/db/basic.go @@ -20,67 +20,68 @@ package db import "context" +// Basic wraps basic database functionality. type Basic interface { // CreateTable creates a table for the given interface. // For implementations that don't use tables, this can just return nil. - CreateTable(i interface{}) DBError + CreateTable(i interface{}) Error // DropTable drops the table for the given interface. // For implementations that don't use tables, this can just return nil. - DropTable(i interface{}) DBError + DropTable(i interface{}) Error // RegisterTable registers a table for use in many2many relations. // For implementations that don't use tables, or many2many relations, this can just return nil. - RegisterTable(i interface{}) DBError + RegisterTable(i interface{}) Error // Stop should stop and close the database connection cleanly, returning an error if this is not possible. // If the database implementation doesn't need to be stopped, this can just return nil. - Stop(ctx context.Context) DBError + Stop(ctx context.Context) Error // IsHealthy should return nil if the database connection is healthy, or an error if not. - IsHealthy(ctx context.Context) DBError + IsHealthy(ctx context.Context) Error // GetByID gets one entry by its id. In a database like postgres, this might be the 'id' field of the entry, // for other implementations (for example, in-memory) it might just be the key of a map. // The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. // In case of no entries, a 'no entries' error will be returned - GetByID(id string, i interface{}) DBError + GetByID(id string, i interface{}) Error // GetWhere gets one entry where key = value. This is similar to GetByID but allows the caller to specify the // name of the key to select from. // The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. // In case of no entries, a 'no entries' error will be returned - GetWhere(where []Where, i interface{}) DBError + GetWhere(where []Where, i interface{}) Error // GetAll will try to get all entries of type i. // The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. // In case of no entries, a 'no entries' error will be returned - GetAll(i interface{}) DBError + GetAll(i interface{}) Error // Put simply stores i. It is up to the implementation to figure out how to store it, and using what key. // The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. - Put(i interface{}) DBError + Put(i interface{}) Error // Upsert stores or updates i based on the given conflict column, as in https://www.postgresqltutorial.com/postgresql-upsert/ // It is up to the implementation to figure out how to store it, and using what key. // The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. - Upsert(i interface{}, conflictColumn string) DBError + Upsert(i interface{}, conflictColumn string) Error // UpdateByID updates i with id id. // The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. - UpdateByID(id string, i interface{}) DBError + UpdateByID(id string, i interface{}) Error // UpdateOneByID updates interface i with database the given database id. It will update one field of key key and value value. - UpdateOneByID(id string, key string, value interface{}, i interface{}) DBError + UpdateOneByID(id string, key string, value interface{}, i interface{}) Error // UpdateWhere updates column key of interface i with the given value, where the given parameters apply. - UpdateWhere(where []Where, key string, value interface{}, i interface{}) DBError + UpdateWhere(where []Where, key string, value interface{}, i interface{}) Error // DeleteByID removes i with id id. // If i didn't exist anyway, then no error should be returned. - DeleteByID(id string, i interface{}) DBError + DeleteByID(id string, i interface{}) Error // DeleteWhere deletes i where key = value // If i didn't exist anyway, then no error should be returned. - DeleteWhere(where []Where, i interface{}) DBError + DeleteWhere(where []Where, i interface{}) Error } diff --git a/internal/db/db.go b/internal/db/db.go index d74eb27ed..d6ac883e4 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -27,13 +27,12 @@ const ( DBTypePostgres string = "POSTGRES" ) -// DB provides methods for interacting with an underlying database or other storage mechanism (for now, just postgres). -// Note that in all of the functions below, the passed interface should be a pointer or a slice, which will then be populated -// by whatever is returned from the database. +// DB provides methods for interacting with an underlying database or other storage mechanism. type DB interface { Account Admin Basic + Domain Instance Media Mention diff --git a/internal/db/domain.go b/internal/db/domain.go new file mode 100644 index 000000000..a6583c80c --- /dev/null +++ b/internal/db/domain.go @@ -0,0 +1,36 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package db + +import "net/url" + +// Domain contains DB functions related to domains and domain blocks. +type Domain interface { + // IsDomainBlocked checks if an instance-level domain block exists for the given domain string (eg., `example.org`). + IsDomainBlocked(domain string) (bool, Error) + + // AreDomainsBlocked checks if an instance-level domain block exists for any of the given domains strings, and returns true if even one is found. + AreDomainsBlocked(domains []string) (bool, Error) + + // IsURIBlocked checks if an instance-level domain block exists for the `host` in the given URI (eg., `https://example.org/users/whatever`). + IsURIBlocked(uri *url.URL) (bool, Error) + + // AreURIsBlocked checks if an instance-level domain block exists for any `host` in the given URI slice, and returns true if even one is found. + AreURIsBlocked(uris []*url.URL) (bool, Error) +} diff --git a/internal/db/error.go b/internal/db/error.go index 9ccc37b3e..c13bd78dd 100644 --- a/internal/db/error.go +++ b/internal/db/error.go @@ -20,11 +20,16 @@ package db import "fmt" -type DBError error +// Error denotes a database error. +type Error error var ( - ErrNoEntries DBError = fmt.Errorf("no entries") - ErrMultipleEntries DBError = fmt.Errorf("multiple entries") - ErrAlreadyExists DBError = fmt.Errorf("already exists") - ErrUnknown DBError = fmt.Errorf("unknown error") + // ErrNoEntries is returned when a caller expected an entry for a query, but none was found. + ErrNoEntries Error = fmt.Errorf("no entries") + // ErrMultipleEntries is returned when a caller expected ONE entry for a query, but multiples were found. + ErrMultipleEntries Error = fmt.Errorf("multiple entries") + // ErrAlreadyExists is returned when a caller tries to insert a database entry that already exists in the db. + ErrAlreadyExists Error = fmt.Errorf("already exists") + // ErrUnknown denotes an unknown database error. + ErrUnknown Error = fmt.Errorf("unknown error") ) diff --git a/internal/db/instance.go b/internal/db/instance.go index 3268b2085..afac8266c 100644 --- a/internal/db/instance.go +++ b/internal/db/instance.go @@ -20,16 +20,17 @@ package db import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +// Instance contains functions for instance-level actions (counting instance users etc.). type Instance interface { // GetUserCountForInstance returns the number of known accounts registered with the given domain. - GetUserCountForInstance(domain string) (int, DBError) + GetUserCountForInstance(domain string) (int, Error) // GetStatusCountForInstance returns the number of known statuses posted from the given domain. - GetStatusCountForInstance(domain string) (int, DBError) + GetStatusCountForInstance(domain string) (int, Error) // GetDomainCountForInstance returns the number of known instances known that the given domain federates with. - GetDomainCountForInstance(domain string) (int, DBError) + GetDomainCountForInstance(domain string) (int, Error) // GetAccountsForInstance returns a slice of accounts from the given instance, arranged by ID. - GetAccountsForInstance(domain string, maxID string, limit int) ([]*gtsmodel.Account, DBError) + GetAccountsForInstance(domain string, maxID string, limit int) ([]*gtsmodel.Account, Error) } diff --git a/internal/db/media.go b/internal/db/media.go index a677ad019..db4db3411 100644 --- a/internal/db/media.go +++ b/internal/db/media.go @@ -20,7 +20,8 @@ package db import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +// Media contains functions related to creating/getting/removing media attachments. type Media interface { // GetAttachmentByID gets a single attachment by its ID - GetAttachmentByID(id string) (*gtsmodel.MediaAttachment, DBError) + GetAttachmentByID(id string) (*gtsmodel.MediaAttachment, Error) } diff --git a/internal/db/mention.go b/internal/db/mention.go index 0e6216508..cb1c56dc1 100644 --- a/internal/db/mention.go +++ b/internal/db/mention.go @@ -20,10 +20,11 @@ package db import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +// Mention contains functions for getting/creating mentions in the database. type Mention interface { // GetMention gets a single mention by ID - GetMention(id string) (*gtsmodel.Mention, DBError) + GetMention(id string) (*gtsmodel.Mention, Error) // GetMentions gets multiple mentions. - GetMentions(ids []string) ([]*gtsmodel.Mention, DBError) + GetMentions(ids []string) ([]*gtsmodel.Mention, Error) } diff --git a/internal/db/notification.go b/internal/db/notification.go index 26bcc0a79..b8e2829e7 100644 --- a/internal/db/notification.go +++ b/internal/db/notification.go @@ -20,7 +20,8 @@ package db import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +// Notification contains functions for creating and getting notifications. type Notification interface { // GetNotificationsForAccount returns a list of notifications that pertain to the given accountID. - GetNotificationsForAccount(accountID string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, DBError) + GetNotificationsForAccount(accountID string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, Error) } diff --git a/internal/db/pg/account.go b/internal/db/pg/account.go index 4d17488d8..485c66e4f 100644 --- a/internal/db/pg/account.go +++ b/internal/db/pg/account.go @@ -45,7 +45,7 @@ func (a *accountDB) newAccountQ(account *gtsmodel.Account) *orm.Query { Relation("HeaderMediaAttachment") } -func (a *accountDB) GetAccountByID(id string) (*gtsmodel.Account, db.DBError) { +func (a *accountDB) GetAccountByID(id string) (*gtsmodel.Account, db.Error) { account := >smodel.Account{} q := a.newAccountQ(account). @@ -56,7 +56,7 @@ func (a *accountDB) GetAccountByID(id string) (*gtsmodel.Account, db.DBError) { return account, err } -func (a *accountDB) GetAccountByURI(uri string) (*gtsmodel.Account, db.DBError) { +func (a *accountDB) GetAccountByURI(uri string) (*gtsmodel.Account, db.Error) { account := >smodel.Account{} q := a.newAccountQ(account). @@ -67,7 +67,7 @@ func (a *accountDB) GetAccountByURI(uri string) (*gtsmodel.Account, db.DBError) return account, err } -func (a *accountDB) GetAccountByURL(uri string) (*gtsmodel.Account, db.DBError) { +func (a *accountDB) GetAccountByURL(uri string) (*gtsmodel.Account, db.Error) { account := >smodel.Account{} q := a.newAccountQ(account). @@ -78,7 +78,7 @@ func (a *accountDB) GetAccountByURL(uri string) (*gtsmodel.Account, db.DBError) return account, err } -func (a *accountDB) GetInstanceAccount(domain string) (*gtsmodel.Account, db.DBError) { +func (a *accountDB) GetInstanceAccount(domain string) (*gtsmodel.Account, db.Error) { account := >smodel.Account{} q := a.newAccountQ(account) @@ -98,7 +98,7 @@ func (a *accountDB) GetInstanceAccount(domain string) (*gtsmodel.Account, db.DBE return account, err } -func (a *accountDB) GetAccountLastPosted(accountID string) (time.Time, db.DBError) { +func (a *accountDB) GetAccountLastPosted(accountID string) (time.Time, db.Error) { status := >smodel.Status{} q := a.conn.Model(status). @@ -112,7 +112,7 @@ func (a *accountDB) GetAccountLastPosted(accountID string) (time.Time, db.DBErro return status.CreatedAt, err } -func (a *accountDB) SetAccountHeaderOrAvatar(mediaAttachment *gtsmodel.MediaAttachment, accountID string) db.DBError { +func (a *accountDB) SetAccountHeaderOrAvatar(mediaAttachment *gtsmodel.MediaAttachment, accountID string) db.Error { if mediaAttachment.Avatar && mediaAttachment.Header { return errors.New("one media attachment cannot be both header and avatar") } @@ -137,7 +137,7 @@ func (a *accountDB) SetAccountHeaderOrAvatar(mediaAttachment *gtsmodel.MediaAtta return nil } -func (a *accountDB) GetLocalAccountByUsername(username string) (*gtsmodel.Account, db.DBError) { +func (a *accountDB) GetLocalAccountByUsername(username string) (*gtsmodel.Account, db.Error) { account := >smodel.Account{} q := a.newAccountQ(account). @@ -149,7 +149,7 @@ func (a *accountDB) GetLocalAccountByUsername(username string) (*gtsmodel.Accoun return account, err } -func (a *accountDB) GetAccountFollowRequests(accountID string, followRequests *[]gtsmodel.FollowRequest) db.DBError { +func (a *accountDB) GetAccountFollowRequests(accountID string, followRequests *[]gtsmodel.FollowRequest) db.Error { if err := a.conn.Model(followRequests).Where("target_account_id = ?", accountID).Select(); err != nil { if err == pg.ErrNoRows { return nil @@ -159,7 +159,7 @@ func (a *accountDB) GetAccountFollowRequests(accountID string, followRequests *[ return nil } -func (a *accountDB) GetAccountFollowing(accountID string, following *[]gtsmodel.Follow) db.DBError { +func (a *accountDB) GetAccountFollowing(accountID string, following *[]gtsmodel.Follow) db.Error { if err := a.conn.Model(following).Where("account_id = ?", accountID).Select(); err != nil { if err == pg.ErrNoRows { return nil @@ -169,11 +169,11 @@ func (a *accountDB) GetAccountFollowing(accountID string, following *[]gtsmodel. return nil } -func (a *accountDB) CountAccountFollowing(accountID string, localOnly bool) (int, db.DBError) { +func (a *accountDB) CountAccountFollowing(accountID string, localOnly bool) (int, db.Error) { return a.conn.Model(&[]*gtsmodel.Follow{}).Where("account_id = ?", accountID).Count() } -func (a *accountDB) GetAccountFollowers(accountID string, followers *[]gtsmodel.Follow, localOnly bool) db.DBError { +func (a *accountDB) GetAccountFollowers(accountID string, followers *[]gtsmodel.Follow, localOnly bool) db.Error { q := a.conn.Model(followers) @@ -203,11 +203,11 @@ func (a *accountDB) GetAccountFollowers(accountID string, followers *[]gtsmodel. return nil } -func (a *accountDB) CountAccountFollowers(accountID string, localOnly bool) (int, db.DBError) { +func (a *accountDB) CountAccountFollowers(accountID string, localOnly bool) (int, db.Error) { return a.conn.Model(&[]*gtsmodel.Follow{}).Where("target_account_id = ?", accountID).Count() } -func (a *accountDB) GetAccountFaves(accountID string, faves *[]gtsmodel.StatusFave) db.DBError { +func (a *accountDB) GetAccountFaves(accountID string, faves *[]gtsmodel.StatusFave) db.Error { if err := a.conn.Model(faves).Where("account_id = ?", accountID).Select(); err != nil { if err == pg.ErrNoRows { return nil @@ -217,11 +217,11 @@ func (a *accountDB) GetAccountFaves(accountID string, faves *[]gtsmodel.StatusFa return nil } -func (a *accountDB) CountAccountStatuses(accountID string) (int, db.DBError) { +func (a *accountDB) CountAccountStatuses(accountID string) (int, db.Error) { return a.conn.Model(>smodel.Status{}).Where("account_id = ?", accountID).Count() } -func (a *accountDB) GetAccountStatuses(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, db.DBError) { +func (a *accountDB) GetAccountStatuses(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, db.Error) { a.log.Debugf("getting statuses for account %s", accountID) statuses := []*gtsmodel.Status{} @@ -267,7 +267,7 @@ func (a *accountDB) GetAccountStatuses(accountID string, limit int, excludeRepli return statuses, nil } -func (a *accountDB) GetAccountBlocks(accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, db.DBError) { +func (a *accountDB) GetAccountBlocks(accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, db.Error) { blocks := []*gtsmodel.Block{} fq := a.conn.Model(&blocks). diff --git a/internal/db/pg/admin.go b/internal/db/pg/admin.go index fe3d48b54..854f56ef0 100644 --- a/internal/db/pg/admin.go +++ b/internal/db/pg/admin.go @@ -45,7 +45,7 @@ type adminDB struct { cancel context.CancelFunc } -func (a *adminDB) IsUsernameAvailable(username string) db.DBError { +func (a *adminDB) IsUsernameAvailable(username string) db.Error { // if no error we fail because it means we found something // if error but it's not pg.ErrNoRows then we fail // if err is pg.ErrNoRows we're good, we found nothing so continue @@ -57,7 +57,7 @@ func (a *adminDB) IsUsernameAvailable(username string) db.DBError { return nil } -func (a *adminDB) IsEmailAvailable(email string) db.DBError { +func (a *adminDB) IsEmailAvailable(email string) db.Error { // parse the domain from the email m, err := mail.ParseAddress(email) if err != nil { @@ -85,7 +85,7 @@ func (a *adminDB) IsEmailAvailable(email string) db.DBError { return nil } -func (a *adminDB) NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string, emailVerified bool, admin bool) (*gtsmodel.User, db.DBError) { +func (a *adminDB) NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string, emailVerified bool, admin bool) (*gtsmodel.User, db.Error) { key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { a.log.Errorf("error creating new rsa key: %s", err) @@ -168,7 +168,7 @@ func (a *adminDB) NewSignup(username string, reason string, requireApproval bool return u, nil } -func (a *adminDB) CreateInstanceAccount() db.DBError { +func (a *adminDB) CreateInstanceAccount() db.Error { username := a.config.Host key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { @@ -210,7 +210,7 @@ func (a *adminDB) CreateInstanceAccount() db.DBError { return nil } -func (a *adminDB) CreateInstanceInstance() db.DBError { +func (a *adminDB) CreateInstanceInstance() db.Error { iID, err := id.NewRandomULID() if err != nil { return err diff --git a/internal/db/pg/basic.go b/internal/db/pg/basic.go index 337c6add8..6e76b4450 100644 --- a/internal/db/pg/basic.go +++ b/internal/db/pg/basic.go @@ -38,7 +38,7 @@ type basicDB struct { cancel context.CancelFunc } -func (b *basicDB) Put(i interface{}) db.DBError { +func (b *basicDB) Put(i interface{}) db.Error { _, err := b.conn.Model(i).Insert(i) if err != nil && strings.Contains(err.Error(), "duplicate key value violates unique constraint") { return db.ErrAlreadyExists @@ -46,7 +46,7 @@ func (b *basicDB) Put(i interface{}) db.DBError { return err } -func (b *basicDB) GetByID(id string, i interface{}) db.DBError { +func (b *basicDB) GetByID(id string, i interface{}) db.Error { if err := b.conn.Model(i).Where("id = ?", id).Select(); err != nil { if err == pg.ErrNoRows { return db.ErrNoEntries @@ -57,7 +57,7 @@ func (b *basicDB) GetByID(id string, i interface{}) db.DBError { return nil } -func (b *basicDB) GetWhere(where []db.Where, i interface{}) db.DBError { +func (b *basicDB) GetWhere(where []db.Where, i interface{}) db.Error { if len(where) == 0 { return errors.New("no queries provided") } @@ -85,7 +85,7 @@ func (b *basicDB) GetWhere(where []db.Where, i interface{}) db.DBError { return nil } -func (b *basicDB) GetAll(i interface{}) db.DBError { +func (b *basicDB) GetAll(i interface{}) db.Error { if err := b.conn.Model(i).Select(); err != nil { if err == pg.ErrNoRows { return db.ErrNoEntries @@ -95,7 +95,7 @@ func (b *basicDB) GetAll(i interface{}) db.DBError { return nil } -func (b *basicDB) DeleteByID(id string, i interface{}) db.DBError { +func (b *basicDB) DeleteByID(id string, i interface{}) db.Error { if _, err := b.conn.Model(i).Where("id = ?", id).Delete(); err != nil { // if there are no rows *anyway* then that's fine // just return err if there's an actual error @@ -106,7 +106,7 @@ func (b *basicDB) DeleteByID(id string, i interface{}) db.DBError { return nil } -func (b *basicDB) DeleteWhere(where []db.Where, i interface{}) db.DBError { +func (b *basicDB) DeleteWhere(where []db.Where, i interface{}) db.Error { if len(where) == 0 { return errors.New("no queries provided") } @@ -126,7 +126,7 @@ func (b *basicDB) DeleteWhere(where []db.Where, i interface{}) db.DBError { return nil } -func (b *basicDB) Upsert(i interface{}, conflictColumn string) db.DBError { +func (b *basicDB) Upsert(i interface{}, conflictColumn string) db.Error { if _, err := b.conn.Model(i).OnConflict(fmt.Sprintf("(%s) DO UPDATE", conflictColumn)).Insert(); err != nil { if err == pg.ErrNoRows { return db.ErrNoEntries @@ -136,7 +136,7 @@ func (b *basicDB) Upsert(i interface{}, conflictColumn string) db.DBError { return nil } -func (b *basicDB) UpdateByID(id string, i interface{}) db.DBError { +func (b *basicDB) UpdateByID(id string, i interface{}) db.Error { if _, err := b.conn.Model(i).Where("id = ?", id).OnConflict("(id) DO UPDATE").Insert(); err != nil { if err == pg.ErrNoRows { return db.ErrNoEntries @@ -146,12 +146,12 @@ func (b *basicDB) UpdateByID(id string, i interface{}) db.DBError { return nil } -func (b *basicDB) UpdateOneByID(id string, key string, value interface{}, i interface{}) db.DBError { +func (b *basicDB) UpdateOneByID(id string, key string, value interface{}, i interface{}) db.Error { _, err := b.conn.Model(i).Set("? = ?", pg.Safe(key), value).Where("id = ?", id).Update() return err } -func (b *basicDB) UpdateWhere(where []db.Where, key string, value interface{}, i interface{}) db.DBError { +func (b *basicDB) UpdateWhere(where []db.Where, key string, value interface{}, i interface{}) db.Error { q := b.conn.Model(i) for _, w := range where { @@ -173,28 +173,28 @@ func (b *basicDB) UpdateWhere(where []db.Where, key string, value interface{}, i return err } -func (b *basicDB) CreateTable(i interface{}) db.DBError { +func (b *basicDB) CreateTable(i interface{}) db.Error { return b.conn.Model(i).CreateTable(&orm.CreateTableOptions{ IfNotExists: true, }) } -func (b *basicDB) DropTable(i interface{}) db.DBError { +func (b *basicDB) DropTable(i interface{}) db.Error { return b.conn.Model(i).DropTable(&orm.DropTableOptions{ IfExists: true, }) } -func (b *basicDB) RegisterTable(i interface{}) db.DBError { +func (b *basicDB) RegisterTable(i interface{}) db.Error { orm.RegisterTable(i) return nil } -func (b *basicDB) IsHealthy(ctx context.Context) db.DBError { +func (b *basicDB) IsHealthy(ctx context.Context) db.Error { return b.conn.Ping(ctx) } -func (b *basicDB) Stop(ctx context.Context) db.DBError { +func (b *basicDB) Stop(ctx context.Context) db.Error { b.log.Info("closing db connection") if err := b.conn.Close(); err != nil { // only cancel if there's a problem closing the db diff --git a/internal/db/pg/domain.go b/internal/db/pg/domain.go new file mode 100644 index 000000000..4e9b2ab48 --- /dev/null +++ b/internal/db/pg/domain.go @@ -0,0 +1,83 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package pg + +import ( + "context" + "net/url" + + "github.com/go-pg/pg/v10" + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/util" +) + +type domainDB struct { + config *config.Config + conn *pg.DB + log *logrus.Logger + cancel context.CancelFunc +} + +func (d *domainDB) IsDomainBlocked(domain string) (bool, db.Error) { + if domain == "" { + return false, nil + } + + blocked, err := d.conn. + Model(>smodel.DomainBlock{}). + Where("LOWER(domain) = LOWER(?)", domain). + Exists() + + err = processErrorResponse(err) + + return blocked, err +} + +func (d *domainDB) AreDomainsBlocked(domains []string) (bool, db.Error) { + // filter out any doubles + uniqueDomains := util.UniqueStrings(domains) + + for _, domain := range uniqueDomains { + if blocked, err := d.IsDomainBlocked(domain); err != nil { + return false, err + } else if blocked { + return blocked, nil + } + } + + // no blocks found + return false, nil +} + +func (d *domainDB) IsURIBlocked(uri *url.URL) (bool, db.Error) { + domain := uri.Hostname() + return d.IsDomainBlocked(domain) +} + +func (d *domainDB) AreURIsBlocked(uris []*url.URL) (bool, db.Error) { + domains := []string{} + for _, uri := range uris { + domains = append(domains, uri.Hostname()) + } + + return d.AreDomainsBlocked(domains) +} diff --git a/internal/db/pg/instance.go b/internal/db/pg/instance.go index e2e92b23d..71ef06e0a 100644 --- a/internal/db/pg/instance.go +++ b/internal/db/pg/instance.go @@ -35,7 +35,7 @@ type instanceDB struct { cancel context.CancelFunc } -func (i *instanceDB) GetUserCountForInstance(domain string) (int, db.DBError) { +func (i *instanceDB) GetUserCountForInstance(domain string) (int, db.Error) { q := i.conn.Model(&[]*gtsmodel.Account{}) if domain == i.config.Host { @@ -51,7 +51,7 @@ func (i *instanceDB) GetUserCountForInstance(domain string) (int, db.DBError) { return q.Count() } -func (i *instanceDB) GetStatusCountForInstance(domain string) (int, db.DBError) { +func (i *instanceDB) GetStatusCountForInstance(domain string) (int, db.Error) { q := i.conn.Model(&[]*gtsmodel.Status{}) if domain == i.config.Host { @@ -66,7 +66,7 @@ func (i *instanceDB) GetStatusCountForInstance(domain string) (int, db.DBError) return q.Count() } -func (i *instanceDB) GetDomainCountForInstance(domain string) (int, db.DBError) { +func (i *instanceDB) GetDomainCountForInstance(domain string) (int, db.Error) { q := i.conn.Model(&[]*gtsmodel.Instance{}) if domain == i.config.Host { @@ -81,7 +81,7 @@ func (i *instanceDB) GetDomainCountForInstance(domain string) (int, db.DBError) return q.Count() } -func (i *instanceDB) GetAccountsForInstance(domain string, maxID string, limit int) ([]*gtsmodel.Account, db.DBError) { +func (i *instanceDB) GetAccountsForInstance(domain string, maxID string, limit int) ([]*gtsmodel.Account, db.Error) { i.log.Debug("GetAccountsForInstance") accounts := []*gtsmodel.Account{} diff --git a/internal/db/pg/media.go b/internal/db/pg/media.go index dff301fa5..618030af3 100644 --- a/internal/db/pg/media.go +++ b/internal/db/pg/media.go @@ -41,7 +41,7 @@ func (m *mediaDB) newMediaQ(i interface{}) *orm.Query { Relation("Account") } -func (m *mediaDB) GetAttachmentByID(id string) (*gtsmodel.MediaAttachment, db.DBError) { +func (m *mediaDB) GetAttachmentByID(id string) (*gtsmodel.MediaAttachment, db.Error) { attachment := >smodel.MediaAttachment{} q := m.newMediaQ(attachment). diff --git a/internal/db/pg/mention.go b/internal/db/pg/mention.go index 7ab395756..b31f07b67 100644 --- a/internal/db/pg/mention.go +++ b/internal/db/pg/mention.go @@ -24,6 +24,7 @@ import ( "github.com/go-pg/pg/v10" "github.com/go-pg/pg/v10/orm" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/cache" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" @@ -34,6 +35,36 @@ type mentionDB struct { conn *pg.DB log *logrus.Logger cancel context.CancelFunc + cache cache.Cache +} + +func (m *mentionDB) cacheMention(id string, mention *gtsmodel.Mention) { + if m.cache == nil { + m.cache = cache.New() + } + + if err := m.cache.Store(id, mention); err != nil { + m.log.Panicf("mentionDB: error storing in cache: %s", err) + } +} + +func (m *mentionDB) mentionCached(id string) (*gtsmodel.Mention, bool) { + if m.cache == nil { + m.cache = cache.New() + return nil, false + } + + mI, err := m.cache.Fetch(id) + if err != nil || mI == nil { + return nil, false + } + + mention, ok := mI.(*gtsmodel.Mention) + if !ok { + m.log.Panicf("mentionDB: cached interface with key %s was not a mention", id) + } + + return mention, true } func (m *mentionDB) newMentionQ(i interface{}) *orm.Query { @@ -43,7 +74,11 @@ func (m *mentionDB) newMentionQ(i interface{}) *orm.Query { Relation("TargetAccount") } -func (m *mentionDB) GetMention(id string) (*gtsmodel.Mention, db.DBError) { +func (m *mentionDB) GetMention(id string) (*gtsmodel.Mention, db.Error) { + if mention, cached := m.mentionCached(id); cached { + return mention, nil + } + mention := >smodel.Mention{} q := m.newMentionQ(mention). @@ -51,20 +86,23 @@ func (m *mentionDB) GetMention(id string) (*gtsmodel.Mention, db.DBError) { err := processErrorResponse(q.Select()) + if err == nil && mention != nil { + m.cacheMention(id, mention) + } + return mention, err } -func (m *mentionDB) GetMentions(ids []string) ([]*gtsmodel.Mention, db.DBError) { +func (m *mentionDB) GetMentions(ids []string) ([]*gtsmodel.Mention, db.Error) { mentions := []*gtsmodel.Mention{} - if len(ids) == 0 { - return mentions, nil + for _, i := range ids { + mention, err := m.GetMention(i) + if err != nil { + return nil, processErrorResponse(err) + } + mentions = append(mentions, mention) } - q := m.newMentionQ(&mentions). - Where("mention.id in (?)", pg.In(ids)) - - err := processErrorResponse(q.Select()) - - return mentions, err + return mentions, nil } diff --git a/internal/db/pg/notification.go b/internal/db/pg/notification.go index 84359a981..ac3a2149b 100644 --- a/internal/db/pg/notification.go +++ b/internal/db/pg/notification.go @@ -24,7 +24,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) -func (ps *postgresService) GetNotificationsForAccount(accountID string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, db.DBError) { +func (ps *postgresService) GetNotificationsForAccount(accountID string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, db.Error) { notifications := []*gtsmodel.Notification{} q := ps.conn.Model(¬ifications).Where("target_account_id = ?", accountID) diff --git a/internal/db/pg/pg.go b/internal/db/pg/pg.go index f63c62cf4..9d9c8e572 100644 --- a/internal/db/pg/pg.go +++ b/internal/db/pg/pg.go @@ -49,6 +49,7 @@ type postgresService struct { db.Account db.Admin db.Basic + db.Domain db.Instance db.Media db.Mention @@ -123,6 +124,12 @@ func NewPostgresService(ctx context.Context, c *config.Config, log *logrus.Logge log: log, cancel: cancel, }, + Domain: &domainDB{ + config: c, + conn: conn, + log: log, + cancel: cancel, + }, Instance: &instanceDB{ config: c, conn: conn, diff --git a/internal/db/pg/relationship.go b/internal/db/pg/relationship.go index aa4f35104..e9349ce7f 100644 --- a/internal/db/pg/relationship.go +++ b/internal/db/pg/relationship.go @@ -43,7 +43,7 @@ func (r *relationshipDB) newBlockQ(block *gtsmodel.Block) *orm.Query { Relation("TargetAccount") } -func (r *relationshipDB) processResponse(block *gtsmodel.Block, err error) (*gtsmodel.Block, db.DBError) { +func (r *relationshipDB) processResponse(block *gtsmodel.Block, err error) (*gtsmodel.Block, db.Error) { switch err { case pg.ErrNoRows: return nil, db.ErrNoEntries @@ -54,7 +54,7 @@ func (r *relationshipDB) processResponse(block *gtsmodel.Block, err error) (*gts } } -func (r *relationshipDB) Blocked(account1 string, account2 string, eitherDirection bool) (bool, db.DBError) { +func (r *relationshipDB) Blocked(account1 string, account2 string, eitherDirection bool) (bool, db.Error) { q := r.conn.Model(>smodel.Block{}).Where("account_id = ?", account1).Where("target_account_id = ?", account2) if eitherDirection { @@ -64,7 +64,7 @@ func (r *relationshipDB) Blocked(account1 string, account2 string, eitherDirecti return q.Exists() } -func (r *relationshipDB) GetBlock(account1 string, account2 string) (*gtsmodel.Block, db.DBError) { +func (r *relationshipDB) GetBlock(account1 string, account2 string) (*gtsmodel.Block, db.Error) { block := >smodel.Block{} q := r.newBlockQ(block). @@ -74,7 +74,7 @@ func (r *relationshipDB) GetBlock(account1 string, account2 string) (*gtsmodel.B return r.processResponse(block, q.Select()) } -func (r *relationshipDB) GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, db.DBError) { +func (r *relationshipDB) GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, db.Error) { rel := >smodel.Relationship{ ID: targetAccount, } @@ -128,7 +128,7 @@ func (r *relationshipDB) GetRelationship(requestingAccount string, targetAccount return rel, nil } -func (r *relationshipDB) Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, db.DBError) { +func (r *relationshipDB) Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, db.Error) { if sourceAccount == nil || targetAccount == nil { return false, nil } @@ -136,7 +136,7 @@ func (r *relationshipDB) Follows(sourceAccount *gtsmodel.Account, targetAccount return r.conn.Model(>smodel.Follow{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists() } -func (r *relationshipDB) FollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, db.DBError) { +func (r *relationshipDB) FollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, db.Error) { if sourceAccount == nil || targetAccount == nil { return false, nil } @@ -144,7 +144,7 @@ func (r *relationshipDB) FollowRequested(sourceAccount *gtsmodel.Account, target return r.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists() } -func (r *relationshipDB) Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, db.DBError) { +func (r *relationshipDB) Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, db.Error) { if account1 == nil || account2 == nil { return false, nil } @@ -170,7 +170,7 @@ func (r *relationshipDB) Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel. return f1 && f2, nil } -func (r *relationshipDB) AcceptFollowRequest(originAccountID string, targetAccountID string) (*gtsmodel.Follow, db.DBError) { +func (r *relationshipDB) AcceptFollowRequest(originAccountID string, targetAccountID string) (*gtsmodel.Follow, db.Error) { // make sure the original follow request exists fr := >smodel.FollowRequest{} if err := r.conn.Model(fr).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Select(); err != nil { diff --git a/internal/db/pg/status.go b/internal/db/pg/status.go index 227c48353..99790428e 100644 --- a/internal/db/pg/status.go +++ b/internal/db/pg/status.go @@ -27,6 +27,7 @@ import ( "github.com/go-pg/pg/v10" "github.com/go-pg/pg/v10/orm" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/cache" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" @@ -37,6 +38,36 @@ type statusDB struct { conn *pg.DB log *logrus.Logger cancel context.CancelFunc + cache cache.Cache +} + +func (s *statusDB) cacheStatus(id string, status *gtsmodel.Status) { + if s.cache == nil { + s.cache = cache.New() + } + + if err := s.cache.Store(id, status); err != nil { + s.log.Panicf("statusDB: error storing in cache: %s", err) + } +} + +func (s *statusDB) statusCached(id string) (*gtsmodel.Status, bool) { + if s.cache == nil { + s.cache = cache.New() + return nil, false + } + + sI, err := s.cache.Fetch(id) + if err != nil || sI == nil { + return nil, false + } + + status, ok := sI.(*gtsmodel.Status) + if !ok { + s.log.Panicf("statusDB: cached interface with key %s was not a status", id) + } + + return status, true } func (s *statusDB) newStatusQ(status interface{}) *orm.Query { @@ -60,7 +91,11 @@ func (s *statusDB) newFaveQ(faves interface{}) *orm.Query { Relation("Status") } -func (s *statusDB) GetStatusByID(id string) (*gtsmodel.Status, db.DBError) { +func (s *statusDB) GetStatusByID(id string) (*gtsmodel.Status, db.Error) { + if status, cached := s.statusCached(id); cached { + return status, nil + } + status := >smodel.Status{} q := s.newStatusQ(status). @@ -68,10 +103,18 @@ func (s *statusDB) GetStatusByID(id string) (*gtsmodel.Status, db.DBError) { err := processErrorResponse(q.Select()) + if err == nil && status != nil { + s.cacheStatus(id, status) + } + return status, err } -func (s *statusDB) GetStatusByURI(uri string) (*gtsmodel.Status, db.DBError) { +func (s *statusDB) GetStatusByURI(uri string) (*gtsmodel.Status, db.Error) { + if status, cached := s.statusCached(uri); cached { + return status, nil + } + status := >smodel.Status{} q := s.newStatusQ(status). @@ -79,10 +122,18 @@ func (s *statusDB) GetStatusByURI(uri string) (*gtsmodel.Status, db.DBError) { err := processErrorResponse(q.Select()) + if err == nil && status != nil { + s.cacheStatus(uri, status) + } + return status, err } -func (s *statusDB) GetStatusByURL(uri string) (*gtsmodel.Status, db.DBError) { +func (s *statusDB) GetStatusByURL(uri string) (*gtsmodel.Status, db.Error) { + if status, cached := s.statusCached(uri); cached { + return status, nil + } + status := >smodel.Status{} q := s.newStatusQ(status). @@ -90,10 +141,14 @@ func (s *statusDB) GetStatusByURL(uri string) (*gtsmodel.Status, db.DBError) { err := processErrorResponse(q.Select()) + if err == nil && status != nil { + s.cacheStatus(uri, status) + } + return status, err } -func (s *statusDB) PutStatus(status *gtsmodel.Status) db.DBError { +func (s *statusDB) PutStatus(status *gtsmodel.Status) db.Error { transaction := func(tx *pg.Tx) error { // create links between this status and any emojis it uses for _, i := range status.EmojiIDs { @@ -133,7 +188,7 @@ func (s *statusDB) PutStatus(status *gtsmodel.Status) db.DBError { return processErrorResponse(s.conn.RunInTransaction(context.Background(), transaction)) } -func (s *statusDB) GetStatusParents(status *gtsmodel.Status, onlyDirect bool) ([]*gtsmodel.Status, db.DBError) { +func (s *statusDB) GetStatusParents(status *gtsmodel.Status, onlyDirect bool) ([]*gtsmodel.Status, db.Error) { parents := []*gtsmodel.Status{} s.statusParent(status, &parents, onlyDirect) @@ -157,7 +212,7 @@ func (s *statusDB) statusParent(status *gtsmodel.Status, foundStatuses *[]*gtsmo s.statusParent(parentStatus, foundStatuses, false) } -func (s *statusDB) GetStatusChildren(status *gtsmodel.Status, onlyDirect bool, minID string) ([]*gtsmodel.Status, db.DBError) { +func (s *statusDB) GetStatusChildren(status *gtsmodel.Status, onlyDirect bool, minID string) ([]*gtsmodel.Status, db.Error) { foundStatuses := &list.List{} foundStatuses.PushFront(status) s.statusChildren(status, foundStatuses, onlyDirect, minID) @@ -212,35 +267,35 @@ func (s *statusDB) statusChildren(status *gtsmodel.Status, foundStatuses *list.L } } -func (s *statusDB) CountStatusReplies(status *gtsmodel.Status) (int, db.DBError) { +func (s *statusDB) CountStatusReplies(status *gtsmodel.Status) (int, db.Error) { return s.conn.Model(>smodel.Status{}).Where("in_reply_to_id = ?", status.ID).Count() } -func (s *statusDB) CountStatusReblogs(status *gtsmodel.Status) (int, db.DBError) { +func (s *statusDB) CountStatusReblogs(status *gtsmodel.Status) (int, db.Error) { return s.conn.Model(>smodel.Status{}).Where("boost_of_id = ?", status.ID).Count() } -func (s *statusDB) CountStatusFaves(status *gtsmodel.Status) (int, db.DBError) { +func (s *statusDB) CountStatusFaves(status *gtsmodel.Status) (int, db.Error) { return s.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Count() } -func (s *statusDB) IsStatusFavedBy(status *gtsmodel.Status, accountID string) (bool, db.DBError) { +func (s *statusDB) IsStatusFavedBy(status *gtsmodel.Status, accountID string) (bool, db.Error) { return s.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() } -func (s *statusDB) IsStatusRebloggedBy(status *gtsmodel.Status, accountID string) (bool, db.DBError) { +func (s *statusDB) IsStatusRebloggedBy(status *gtsmodel.Status, accountID string) (bool, db.Error) { return s.conn.Model(>smodel.Status{}).Where("boost_of_id = ?", status.ID).Where("account_id = ?", accountID).Exists() } -func (s *statusDB) IsStatusMutedBy(status *gtsmodel.Status, accountID string) (bool, db.DBError) { +func (s *statusDB) IsStatusMutedBy(status *gtsmodel.Status, accountID string) (bool, db.Error) { return s.conn.Model(>smodel.StatusMute{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() } -func (s *statusDB) IsStatusBookmarkedBy(status *gtsmodel.Status, accountID string) (bool, db.DBError) { +func (s *statusDB) IsStatusBookmarkedBy(status *gtsmodel.Status, accountID string) (bool, db.Error) { return s.conn.Model(>smodel.StatusBookmark{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() } -func (s *statusDB) GetStatusFaves(status *gtsmodel.Status) ([]*gtsmodel.StatusFave, db.DBError) { +func (s *statusDB) GetStatusFaves(status *gtsmodel.Status) ([]*gtsmodel.StatusFave, db.Error) { faves := []*gtsmodel.StatusFave{} q := s.newFaveQ(&faves). @@ -251,7 +306,7 @@ func (s *statusDB) GetStatusFaves(status *gtsmodel.Status) ([]*gtsmodel.StatusFa return faves, err } -func (s *statusDB) GetStatusReblogs(status *gtsmodel.Status) ([]*gtsmodel.Status, db.DBError) { +func (s *statusDB) GetStatusReblogs(status *gtsmodel.Status) ([]*gtsmodel.Status, db.Error) { reblogs := []*gtsmodel.Status{} q := s.newStatusQ(&reblogs). diff --git a/internal/db/pg/status_test.go b/internal/db/pg/status_test.go index 9d699f0b9..8a185757c 100644 --- a/internal/db/pg/status_test.go +++ b/internal/db/pg/status_test.go @@ -19,7 +19,9 @@ package pg_test import ( + "fmt" "testing" + "time" "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/testrig" @@ -94,6 +96,39 @@ func (suite *StatusTestSuite) TestGetStatusWithExtras() { suite.NotEmpty(status.Emojis) } +func (suite *StatusTestSuite) TestGetStatusWithMention() { + status, err := suite.db.GetStatusByID(suite.testStatuses["local_account_2_status_5"].ID) + if err != nil { + suite.FailNow(err.Error()) + } + suite.NotNil(status) + suite.NotNil(status.Account) + suite.NotNil(status.CreatedWithApplication) + suite.NotEmpty(status.Mentions) + suite.NotEmpty(status.MentionIDs) + suite.NotNil(status.InReplyTo) + suite.NotNil(status.InReplyToAccount) +} + +func (suite *StatusTestSuite) TestGetStatusTwice() { + before1 := time.Now() + _, err := suite.db.GetStatusByURI(suite.testStatuses["local_account_1_status_1"].URI) + suite.NoError(err) + after1 := time.Now() + duration1 := after1.Sub(before1) + fmt.Println(duration1.Nanoseconds()) + + before2 := time.Now() + _, err = suite.db.GetStatusByURI(suite.testStatuses["local_account_1_status_1"].URI) + suite.NoError(err) + after2 := time.Now() + duration2 := after2.Sub(before2) + fmt.Println(duration2.Nanoseconds()) + + // second retrieval should be several orders faster since it will be cached now + suite.Less(duration2, duration1) +} + func TestStatusTestSuite(t *testing.T) { suite.Run(t, new(StatusTestSuite)) } diff --git a/internal/db/pg/timeline.go b/internal/db/pg/timeline.go index 87e306da8..41599a785 100644 --- a/internal/db/pg/timeline.go +++ b/internal/db/pg/timeline.go @@ -36,7 +36,7 @@ type timelineDB struct { cancel context.CancelFunc } -func (t *timelineDB) GetHomeTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, db.DBError) { +func (t *timelineDB) GetHomeTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, db.Error) { statuses := []*gtsmodel.Status{} q := t.conn.Model(&statuses) @@ -96,7 +96,7 @@ func (t *timelineDB) GetHomeTimelineForAccount(accountID string, maxID string, s return statuses, nil } -func (t *timelineDB) GetPublicTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, db.DBError) { +func (t *timelineDB) GetPublicTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, db.Error) { statuses := []*gtsmodel.Status{} q := t.conn.Model(&statuses). @@ -143,7 +143,7 @@ func (t *timelineDB) GetPublicTimelineForAccount(accountID string, maxID string, // TODO optimize this query and the logic here, because it's slow as balls -- it takes like a literal second to return with a limit of 20! // It might be worth serving it through a timeline instead of raw DB queries, like we do for Home feeds. -func (t *timelineDB) GetFavedTimelineForAccount(accountID string, maxID string, minID string, limit int) ([]*gtsmodel.Status, string, string, db.DBError) { +func (t *timelineDB) GetFavedTimelineForAccount(accountID string, maxID string, minID string, limit int) ([]*gtsmodel.Status, string, string, db.Error) { faves := []*gtsmodel.StatusFave{} diff --git a/internal/db/pg/util.go b/internal/db/pg/util.go index e6d901961..17c09b720 100644 --- a/internal/db/pg/util.go +++ b/internal/db/pg/util.go @@ -8,7 +8,7 @@ import ( ) // processErrorResponse parses the given error and returns an appropriate DBError. -func processErrorResponse(err error) db.DBError { +func processErrorResponse(err error) db.Error { switch err { case nil: return nil diff --git a/internal/db/relationship.go b/internal/db/relationship.go index be171f9b1..8966b9145 100644 --- a/internal/db/relationship.go +++ b/internal/db/relationship.go @@ -20,32 +20,33 @@ package db import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +// Relationship contains functions for getting or modifying the relationship between two accounts. type Relationship interface { // Blocked checks whether account 1 has a block in place against block2. // If eitherDirection is true, then the function returns true if account1 blocks account2, OR if account2 blocks account1. - Blocked(account1 string, account2 string, eitherDirection bool) (bool, DBError) + Blocked(account1 string, account2 string, eitherDirection bool) (bool, Error) // GetBlock returns the block from account1 targeting account2, if it exists, or an error if it doesn't. // // Because this is slower than Blocked, only use it if you need the actual Block struct for some reason, // not if you're just checking for the existence of a block. - GetBlock(account1 string, account2 string) (*gtsmodel.Block, DBError) + GetBlock(account1 string, account2 string) (*gtsmodel.Block, Error) // GetRelationship retrieves the relationship of the targetAccount to the requestingAccount. - GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, DBError) + GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, Error) // Follows returns true if sourceAccount follows target account, or an error if something goes wrong while finding out. - Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, DBError) + Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, Error) // FollowRequested returns true if sourceAccount has requested to follow target account, or an error if something goes wrong while finding out. - FollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, DBError) + FollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, Error) // Mutuals returns true if account1 and account2 both follow each other, or an error if something goes wrong while finding out. - Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, DBError) + Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, Error) // AcceptFollowRequest moves a follow request in the database from the follow_requests table to the follows table. // In other words, it should create the follow, and delete the existing follow request. // // It will return the newly created follow for further processing. - AcceptFollowRequest(originAccountID string, targetAccountID string) (*gtsmodel.Follow, DBError) + AcceptFollowRequest(originAccountID string, targetAccountID string) (*gtsmodel.Follow, Error) } diff --git a/internal/db/status.go b/internal/db/status.go index 2a6e527e2..9d206c198 100644 --- a/internal/db/status.go +++ b/internal/db/status.go @@ -20,55 +20,56 @@ package db import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +// Status contains functions for getting statuses, creating statuses, and checking various other fields on statuses. type Status interface { // GetStatusByID returns one status from the database, with all rel fields populated (if possible). - GetStatusByID(id string) (*gtsmodel.Status, DBError) + GetStatusByID(id string) (*gtsmodel.Status, Error) // GetStatusByURI returns one status from the database, with all rel fields populated (if possible). - GetStatusByURI(uri string) (*gtsmodel.Status, DBError) + GetStatusByURI(uri string) (*gtsmodel.Status, Error) // GetStatusByURL returns one status from the database, with all rel fields populated (if possible). - GetStatusByURL(uri string) (*gtsmodel.Status, DBError) + GetStatusByURL(uri string) (*gtsmodel.Status, Error) // PutStatus stores one status in the database. - PutStatus(status *gtsmodel.Status) DBError + PutStatus(status *gtsmodel.Status) Error // CountStatusReplies returns the amount of replies recorded for a status, or an error if something goes wrong - CountStatusReplies(status *gtsmodel.Status) (int, DBError) + CountStatusReplies(status *gtsmodel.Status) (int, Error) // CountStatusReblogs returns the amount of reblogs/boosts recorded for a status, or an error if something goes wrong - CountStatusReblogs(status *gtsmodel.Status) (int, DBError) + CountStatusReblogs(status *gtsmodel.Status) (int, Error) // CountStatusFaves returns the amount of faves/likes recorded for a status, or an error if something goes wrong - CountStatusFaves(status *gtsmodel.Status) (int, DBError) + CountStatusFaves(status *gtsmodel.Status) (int, Error) - // GetStatusParents get the parent statuses of a given status. + // GetStatusParents gets the parent statuses of a given status. // // If onlyDirect is true, only the immediate parent will be returned. - GetStatusParents(status *gtsmodel.Status, onlyDirect bool) ([]*gtsmodel.Status, DBError) + GetStatusParents(status *gtsmodel.Status, onlyDirect bool) ([]*gtsmodel.Status, Error) // GetStatusChildren gets the child statuses of a given status. // // If onlyDirect is true, only the immediate children will be returned. - GetStatusChildren(status *gtsmodel.Status, onlyDirect bool, minID string) ([]*gtsmodel.Status, DBError) + GetStatusChildren(status *gtsmodel.Status, onlyDirect bool, minID string) ([]*gtsmodel.Status, Error) // IsStatusFavedBy checks if a given status has been faved by a given account ID - IsStatusFavedBy(status *gtsmodel.Status, accountID string) (bool, DBError) + IsStatusFavedBy(status *gtsmodel.Status, accountID string) (bool, Error) // IsStatusRebloggedBy checks if a given status has been reblogged/boosted by a given account ID - IsStatusRebloggedBy(status *gtsmodel.Status, accountID string) (bool, DBError) + IsStatusRebloggedBy(status *gtsmodel.Status, accountID string) (bool, Error) // IsStatusMutedBy checks if a given status has been muted by a given account ID - IsStatusMutedBy(status *gtsmodel.Status, accountID string) (bool, DBError) + IsStatusMutedBy(status *gtsmodel.Status, accountID string) (bool, Error) // IsStatusBookmarkedBy checks if a given status has been bookmarked by a given account ID - IsStatusBookmarkedBy(status *gtsmodel.Status, accountID string) (bool, DBError) + IsStatusBookmarkedBy(status *gtsmodel.Status, accountID string) (bool, Error) // GetStatusFaves returns a slice of faves/likes of the given status. // This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user. - GetStatusFaves(status *gtsmodel.Status) ([]*gtsmodel.StatusFave, DBError) + GetStatusFaves(status *gtsmodel.Status) ([]*gtsmodel.StatusFave, Error) // GetStatusReblogs returns a slice of statuses that are a boost/reblog of the given status. // This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user. - GetStatusReblogs(status *gtsmodel.Status) ([]*gtsmodel.Status, DBError) + GetStatusReblogs(status *gtsmodel.Status) ([]*gtsmodel.Status, Error) } diff --git a/internal/db/timeline.go b/internal/db/timeline.go index 638d2d10d..ed84b268a 100644 --- a/internal/db/timeline.go +++ b/internal/db/timeline.go @@ -20,17 +20,18 @@ package db import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +// Timeline contains functionality for retrieving home/public/faved etc timelines for an account. type Timeline interface { // GetHomeTimelineForAccount returns a slice of statuses from accounts that are followed by the given account id. // // Statuses should be returned in descending order of when they were created (newest first). - GetHomeTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, DBError) + GetHomeTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, Error) // GetPublicTimelineForAccount fetches the account's PUBLIC timeline -- ie., posts and replies that are public. // It will use the given filters and try to return as many statuses as possible up to the limit. // // Statuses should be returned in descending order of when they were created (newest first). - GetPublicTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, DBError) + GetPublicTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, Error) // GetFavedTimelineForAccount fetches the account's FAVED timeline -- ie., posts and replies that the requesting account has faved. // It will use the given filters and try to return as many statuses as possible up to the limit. @@ -39,5 +40,5 @@ type Timeline interface { // In other words, they'll be returned in descending order of when they were faved by the requesting user, not when they were created. // // Also note the extra return values, which correspond to the nextMaxID and prevMinID for building Link headers. - GetFavedTimelineForAccount(accountID string, maxID string, minID string, limit int) ([]*gtsmodel.Status, string, string, DBError) + GetFavedTimelineForAccount(accountID string, maxID string, minID string, limit int) ([]*gtsmodel.Status, string, string, Error) } diff --git a/internal/gtsmodel/account.go b/internal/gtsmodel/account.go index 369fd6b77..435caea6d 100644 --- a/internal/gtsmodel/account.go +++ b/internal/gtsmodel/account.go @@ -45,13 +45,13 @@ type Account struct { */ // ID of the avatar as a media attachment - AvatarMediaAttachmentID string `pg:"type:CHAR(26)"` - AvatarMediaAttachment *MediaAttachment `pg:"rel:has-one"` + AvatarMediaAttachmentID string `pg:"type:CHAR(26)"` + AvatarMediaAttachment *MediaAttachment `pg:"rel:has-one"` // For a non-local account, where can the header be fetched? AvatarRemoteURL string // ID of the header as a media attachment - HeaderMediaAttachmentID string `pg:"type:CHAR(26)"` - HeaderMediaAttachment *MediaAttachment `pg:"rel:has-one"` + HeaderMediaAttachmentID string `pg:"type:CHAR(26)"` + HeaderMediaAttachment *MediaAttachment `pg:"rel:has-one"` // For a non-local account, where can the header be fetched? HeaderRemoteURL string // DisplayName for this account. Can be empty, then just the Username will be used for display purposes. diff --git a/internal/gtsmodel/domainblock.go b/internal/gtsmodel/domainblock.go index 809250276..1bed86d8f 100644 --- a/internal/gtsmodel/domainblock.go +++ b/internal/gtsmodel/domainblock.go @@ -31,8 +31,8 @@ type DomainBlock struct { // When was this block updated UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // Account ID of the creator of this block - CreatedByAccountID string `pg:"type:CHAR(26),notnull"` - CreatedByAccount *Account `pg:"rel:belongs-to"` + CreatedByAccountID string `pg:"type:CHAR(26),notnull"` + CreatedByAccount *Account `pg:"rel:belongs-to"` // Private comment on this block, viewable to admins PrivateComment string // Public comment on this block, viewable (optionally) by everyone diff --git a/internal/gtsmodel/emaildomainblock.go b/internal/gtsmodel/emaildomainblock.go index 03618dd3a..374454374 100644 --- a/internal/gtsmodel/emaildomainblock.go +++ b/internal/gtsmodel/emaildomainblock.go @@ -31,6 +31,6 @@ type EmailDomainBlock struct { // When was this block updated UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // Account ID of the creator of this block - CreatedByAccountID string `pg:"type:CHAR(26),notnull"` - CreatedByAccount *Account `pg:"rel:belongs-to"` + CreatedByAccountID string `pg:"type:CHAR(26),notnull"` + CreatedByAccount *Account `pg:"rel:belongs-to"` } diff --git a/internal/gtsmodel/follow.go b/internal/gtsmodel/follow.go index 22f87768e..8f169f8c4 100644 --- a/internal/gtsmodel/follow.go +++ b/internal/gtsmodel/follow.go @@ -29,11 +29,11 @@ type Follow struct { // When was this follow last updated? UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // Who does this follow belong to? - AccountID string `pg:"type:CHAR(26),unique:srctarget,notnull"` - Account *Account `pg:"rel:belongs-to"` + AccountID string `pg:"type:CHAR(26),unique:srctarget,notnull"` + Account *Account `pg:"rel:belongs-to"` // Who does AccountID follow? - TargetAccountID string `pg:"type:CHAR(26),unique:srctarget,notnull"` - TargetAccount *Account `pg:"rel:has-one"` + TargetAccountID string `pg:"type:CHAR(26),unique:srctarget,notnull"` + TargetAccount *Account `pg:"rel:has-one"` // Does this follow also want to see reblogs and not just posts? ShowReblogs bool `pg:"default:true"` // What is the activitypub URI of this follow? diff --git a/internal/gtsmodel/instance.go b/internal/gtsmodel/instance.go index e0eb1435a..7b453a0b3 100644 --- a/internal/gtsmodel/instance.go +++ b/internal/gtsmodel/instance.go @@ -19,8 +19,8 @@ type Instance struct { // When was this instance suspended, if at all? SuspendedAt time.Time // ID of any existing domain block for this instance in the database - DomainBlockID string `pg:"type:CHAR(26)"` - DomainBlock *DomainBlock `pg:"rel:has-one"` + DomainBlockID string `pg:"type:CHAR(26)"` + DomainBlock *DomainBlock `pg:"rel:has-one"` // Short description of this instance ShortDescription string // Longer description of this instance @@ -32,8 +32,8 @@ type Instance struct { // Username of the contact account for this instance ContactAccountUsername string // Contact account ID in the database for this instance - ContactAccountID string `pg:"type:CHAR(26)"` - ContactAccount *Account `pg:"rel:has-one"` + ContactAccountID string `pg:"type:CHAR(26)"` + ContactAccount *Account `pg:"rel:has-one"` // Reputation score of this instance Reputation int64 `pg:",notnull,default:0"` // Version of the software used on this instance diff --git a/internal/gtsmodel/mediaattachment.go b/internal/gtsmodel/mediaattachment.go index 4a8510bf4..0f12caaad 100644 --- a/internal/gtsmodel/mediaattachment.go +++ b/internal/gtsmodel/mediaattachment.go @@ -42,8 +42,8 @@ type MediaAttachment struct { // Metadata about the file FileMeta FileMeta // To which account does this attachment belong - AccountID string `pg:"type:CHAR(26),notnull"` - Account *Account `pg:"rel:belongs-to"` + AccountID string `pg:"type:CHAR(26),notnull"` + Account *Account `pg:"rel:belongs-to"` // Description of the attachment (for screenreaders) Description string // To which scheduled status does this attachment belong diff --git a/internal/gtsmodel/mention.go b/internal/gtsmodel/mention.go index 5252e80d7..931e681db 100644 --- a/internal/gtsmodel/mention.go +++ b/internal/gtsmodel/mention.go @@ -25,20 +25,20 @@ type Mention struct { // ID of this mention in the database ID string `pg:"type:CHAR(26),pk,notnull,unique"` // ID of the status this mention originates from - StatusID string `pg:"type:CHAR(26),notnull"` - Status *Status `pg:"rel:belongs-to"` + StatusID string `pg:"type:CHAR(26),notnull"` + Status *Status `pg:"rel:belongs-to"` // When was this mention created? CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // When was this mention last updated? UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // What's the internal account ID of the originator of the mention? - OriginAccountID string `pg:"type:CHAR(26),notnull"` - OriginAccount *Account `pg:"rel:has-one"` + OriginAccountID string `pg:"type:CHAR(26),notnull"` + OriginAccount *Account `pg:"rel:has-one"` // What's the AP URI of the originator of the mention? OriginAccountURI string `pg:",notnull"` // What's the internal account ID of the mention target? - TargetAccountID string `pg:"type:CHAR(26),notnull"` - TargetAccount *Account `pg:"rel:has-one"` + TargetAccountID string `pg:"type:CHAR(26),notnull"` + TargetAccount *Account `pg:"rel:has-one"` // Prevent this mention from generating a notification? Silent bool diff --git a/internal/gtsmodel/statusbookmark.go b/internal/gtsmodel/statusbookmark.go index de66ebc87..468939bae 100644 --- a/internal/gtsmodel/statusbookmark.go +++ b/internal/gtsmodel/statusbookmark.go @@ -27,11 +27,11 @@ type StatusBookmark struct { // when was this bookmark created CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // id of the account that created ('did') the bookmarking - AccountID string `pg:"type:CHAR(26),notnull"` - Account *Account `pg:"rel:belongs-to"` + AccountID string `pg:"type:CHAR(26),notnull"` + Account *Account `pg:"rel:belongs-to"` // id the account owning the bookmarked status - TargetAccountID string `pg:"type:CHAR(26),notnull"` - TargetAccount *Account `pg:"rel:has-one"` + TargetAccountID string `pg:"type:CHAR(26),notnull"` + TargetAccount *Account `pg:"rel:has-one"` // database id of the status that has been bookmarked StatusID string `pg:"type:CHAR(26),notnull"` } diff --git a/internal/gtsmodel/statusfave.go b/internal/gtsmodel/statusfave.go index 012360bff..17952673a 100644 --- a/internal/gtsmodel/statusfave.go +++ b/internal/gtsmodel/statusfave.go @@ -27,14 +27,14 @@ type StatusFave struct { // when was this fave created CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // id of the account that created ('did') the fave - AccountID string `pg:"type:CHAR(26),notnull"` - Account *Account `pg:"rel:has-one"` + AccountID string `pg:"type:CHAR(26),notnull"` + Account *Account `pg:"rel:has-one"` // id the account owning the faved status - TargetAccountID string `pg:"type:CHAR(26),notnull"` - TargetAccount *Account `pg:"rel:has-one"` + TargetAccountID string `pg:"type:CHAR(26),notnull"` + TargetAccount *Account `pg:"rel:has-one"` // database id of the status that has been 'faved' - StatusID string `pg:"type:CHAR(26),notnull"` - Status *Status `pg:"rel:has-one"` + StatusID string `pg:"type:CHAR(26),notnull"` + Status *Status `pg:"rel:has-one"` // ActivityPub URI of this fave URI string `pg:",notnull"` } diff --git a/internal/gtsmodel/statusmute.go b/internal/gtsmodel/statusmute.go index 4d7fa5ce2..472a5ec09 100644 --- a/internal/gtsmodel/statusmute.go +++ b/internal/gtsmodel/statusmute.go @@ -27,12 +27,12 @@ type StatusMute struct { // when was this mute created CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // id of the account that created ('did') the mute - AccountID string `pg:"type:CHAR(26),notnull"` - Account *Account `pg:"rel:belongs-to"` + AccountID string `pg:"type:CHAR(26),notnull"` + Account *Account `pg:"rel:belongs-to"` // id the account owning the muted status (can be the same as accountID) - TargetAccountID string `pg:"type:CHAR(26),notnull"` - TargetAccount *Account `pg:"rel:has-one"` + TargetAccountID string `pg:"type:CHAR(26),notnull"` + TargetAccount *Account `pg:"rel:has-one"` // database id of the status that has been muted - StatusID string `pg:"type:CHAR(26),notnull"` - Status *Status `pg:"rel:has-one"` + StatusID string `pg:"type:CHAR(26),notnull"` + Status *Status `pg:"rel:has-one"` } diff --git a/internal/processing/account/getstatuses.go b/internal/processing/account/getstatuses.go index 69a605d67..e1e3d0006 100644 --- a/internal/processing/account/getstatuses.go +++ b/internal/processing/account/getstatuses.go @@ -35,7 +35,7 @@ func (p *processor) StatusesGet(requestingAccount *gtsmodel.Account, targetAccou } apiStatuses := []apimodel.Status{} - + statuses, err := p.db.GetAccountStatuses(targetAccountID, limit, excludeReplies, maxID, pinnedOnly, mediaOnly) if err != nil { if err == db.ErrNoEntries { diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go index 3a90cfa53..e477a6135 100644 --- a/internal/typeutils/converter.go +++ b/internal/typeutils/converter.go @@ -176,16 +176,18 @@ type TypeConverter interface { } type converter struct { - config *config.Config - db db.DB + config *config.Config + db db.DB frontendCache cache.Cache + asCache cache.Cache } // NewConverter returns a new Converter func NewConverter(config *config.Config, db db.DB) TypeConverter { return &converter{ - config: config, - db: db, + config: config, + db: db, frontendCache: cache.New(), + asCache: cache.New(), } } diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go index 61ddef1c8..11ace9dfa 100644 --- a/internal/typeutils/internaltoas.go +++ b/internal/typeutils/internaltoas.go @@ -34,6 +34,14 @@ import ( // Converts a gts model account into an Activity Streams person type, following // the spec laid out for mastodon here: https://docs.joinmastodon.org/spec/activitypub/ func (c *converter) AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error) { + // first check if we have this person in our asCache already + if personI, err := c.asCache.Fetch(a.ID); err == nil { + if person, ok := personI.(vocab.ActivityStreamsPerson); ok { + // we have it, so just return it as-is + return person, nil + } + } + person := streams.NewActivityStreamsPerson() // id should be the activitypub URI of this user @@ -256,6 +264,11 @@ func (c *converter) AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerso person.SetActivityStreamsImage(headerProperty) } + // put the person in our cache in case we need it again soon + if err := c.asCache.Store(a.ID, person); err != nil { + return nil, err + } + return person, nil } @@ -326,13 +339,21 @@ func (c *converter) AccountToASMinimal(a *gtsmodel.Account) (vocab.ActivityStrea } func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, error) { + // first check if we have this note in our asCache already + if noteI, err := c.asCache.Fetch(s.ID); err == nil { + if note, ok := noteI.(vocab.ActivityStreamsNote); ok { + // we have it, so just return it as-is + return note, nil + } + } + // ensure prerequisites here before we get stuck in // check if author account is already attached to status and attach it if not // if we can't retrieve this, bail here already because we can't attribute the status to anyone if s.Account == nil { - a := >smodel.Account{} - if err := c.db.GetByID(s.AccountID, a); err != nil { + a, err := c.db.GetAccountByID(s.AccountID) + if err != nil { return nil, fmt.Errorf("StatusToAS: error retrieving author account from db: %s", err) } s.Account = a @@ -515,6 +536,11 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e repliesProp.SetActivityStreamsCollection(repliesCollection) status.SetActivityStreamsReplies(repliesProp) + // put the note in our cache in case we need it again soon + if err := c.asCache.Store(s.ID, status); err != nil { + return nil, err + } + return status, nil } diff --git a/internal/util/statustools.go b/internal/util/statustools.go index ce5860c6d..4a89e60f6 100644 --- a/internal/util/statustools.go +++ b/internal/util/statustools.go @@ -34,7 +34,7 @@ func DeriveMentionsFromStatus(status string) []string { for _, m := range mentionFinderRegex.FindAllStringSubmatch(status, -1) { mentionedAccounts = append(mentionedAccounts, m[1]) } - return unique(mentionedAccounts) + return UniqueStrings(mentionedAccounts) } // DeriveHashtagsFromStatus takes a plaintext (ie., not html-formatted) status, @@ -46,7 +46,7 @@ func DeriveHashtagsFromStatus(status string) []string { for _, m := range HashtagFinderRegex.FindAllStringSubmatch(status, -1) { tags = append(tags, strings.TrimPrefix(m[1], "#")) } - return unique(tags) + return UniqueStrings(tags) } // DeriveEmojisFromStatus takes a plaintext (ie., not html-formatted) status, @@ -57,7 +57,7 @@ func DeriveEmojisFromStatus(status string) []string { for _, m := range emojiFinderRegex.FindAllStringSubmatch(status, -1) { emojis = append(emojis, m[1]) } - return unique(emojis) + return UniqueStrings(emojis) } // ExtractMentionParts extracts the username test_user and the domain example.org @@ -79,16 +79,3 @@ func ExtractMentionParts(mention string) (username, domain string, err error) { func IsMention(mention string) bool { return mentionNameRegex.MatchString(strings.ToLower(mention)) } - -// unique returns a deduplicated version of a given string slice. -func unique(s []string) []string { - keys := make(map[string]bool) - list := []string{} - for _, entry := range s { - if _, value := keys[entry]; !value { - keys[entry] = true - list = append(list, entry) - } - } - return list -} diff --git a/internal/util/unique.go b/internal/util/unique.go new file mode 100644 index 000000000..d679515d0 --- /dev/null +++ b/internal/util/unique.go @@ -0,0 +1,32 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package util + +// UniqueStrings returns a deduplicated version of a given string slice. +func UniqueStrings(s []string) []string { + keys := make(map[string]bool) + list := []string{} + for _, entry := range s { + if _, value := keys[entry]; !value { + keys[entry] = true + list = append(list, entry) + } + } + return list +} diff --git a/internal/visibility/filter.go b/internal/visibility/filter.go index 181eb8ee7..2c43fa4ee 100644 --- a/internal/visibility/filter.go +++ b/internal/visibility/filter.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package visibility import ( diff --git a/internal/visibility/relevantaccounts.go b/internal/visibility/relevantaccounts.go new file mode 100644 index 000000000..5957d3111 --- /dev/null +++ b/internal/visibility/relevantaccounts.go @@ -0,0 +1,229 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package visibility + +import ( + "errors" + "fmt" + + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +// relevantAccounts denotes accounts that are replied to, boosted by, or mentioned in a status. +type relevantAccounts struct { + // Who wrote the status + Account *gtsmodel.Account + // Who is the status replying to + InReplyToAccount *gtsmodel.Account + // Which accounts are mentioned (tagged) in the status + MentionedAccounts []*gtsmodel.Account + // Who authed the boosted status + BoostedAccount *gtsmodel.Account + // If the boosted status replies to another account, who does it reply to? + BoostedInReplyToAccount *gtsmodel.Account + // Who is mentioned (tagged) in the boosted status + BoostedMentionedAccounts []*gtsmodel.Account +} + +func (f *filter) relevantAccounts(status *gtsmodel.Status, getBoosted bool) (*relevantAccounts, error) { + relAccts := &relevantAccounts{ + MentionedAccounts: []*gtsmodel.Account{}, + BoostedMentionedAccounts: []*gtsmodel.Account{}, + } + + /* + Here's what we need to try and extract from the status: + + // 1. Who wrote the status + Account *gtsmodel.Account + + // 2. Who is the status replying to + InReplyToAccount *gtsmodel.Account + + // 3. Which accounts are mentioned (tagged) in the status + MentionedAccounts []*gtsmodel.Account + + if getBoosted: + // 4. Who wrote the boosted status + BoostedAccount *gtsmodel.Account + + // 5. If the boosted status replies to another account, who does it reply to? + BoostedInReplyToAccount *gtsmodel.Account + + // 6. Who is mentioned (tagged) in the boosted status + BoostedMentionedAccounts []*gtsmodel.Account + */ + + // 1. Account. + // Account might be set on the status already + if status.Account != nil { + // it was set + relAccts.Account = status.Account + } else { + // it wasn't set, so get it from the db + account, err := f.db.GetAccountByID(status.AccountID) + if err != nil { + return nil, fmt.Errorf("relevantAccounts: error getting account with id %s: %s", status.AccountID, err) + } + // set it on the status in case we need it further along + status.Account = account + // set it on relevant accounts + relAccts.Account = account + } + + // 2. InReplyToAccount + // only get this if InReplyToAccountID is set + if status.InReplyToAccountID != "" { + // InReplyToAccount might be set on the status already + if status.InReplyToAccount != nil { + // it was set + relAccts.InReplyToAccount = status.InReplyToAccount + } else { + // it wasn't set, so get it from the db + inReplyToAccount, err := f.db.GetAccountByID(status.InReplyToAccountID) + if err != nil { + return nil, fmt.Errorf("relevantAccounts: error getting inReplyToAccount with id %s: %s", status.InReplyToAccountID, err) + } + // set it on the status in case we need it further along + status.InReplyToAccount = inReplyToAccount + // set it on relevant accounts + relAccts.InReplyToAccount = inReplyToAccount + } + } + + // 3. MentionedAccounts + // First check if status.Mentions is populated with all mentions that correspond to status.MentionIDs + for _, mID := range status.MentionIDs { + if mID == "" { + continue + } + if !idIn(mID, status.Mentions) { + // mention with ID isn't in status.Mentions + mention, err := f.db.GetMention(mID) + if err != nil { + return nil, fmt.Errorf("relevantAccounts: error getting mention with id %s: %s", mID, err) + } + if mention == nil { + return nil, fmt.Errorf("relevantAccounts: mention with id %s was nil", mID) + } + status.Mentions = append(status.Mentions, mention) + } + } + // now filter mentions to make sure we only have mentions with a corresponding ID + nm := []*gtsmodel.Mention{} + for _, m := range status.Mentions { + if m == nil { + continue + } + if mentionIn(m, status.MentionIDs) { + nm = append(nm, m) + } + } + status.Mentions = nm + + if len(status.Mentions) != len(status.MentionIDs) { + return nil, errors.New("relevantAccounts: mentions length did not correspond with mentionIDs length") + } + + // if getBoosted is set, we should check the same properties on the boosted account as well + if getBoosted { + // 4, 5, 6. Boosted status items + // get the boosted status if it's not set on the status already + if status.BoostOfID != "" && status.BoostOf == nil { + boostedStatus, err := f.db.GetStatusByID(status.BoostOfID) + if err != nil { + return nil, fmt.Errorf("relevantAccounts: error getting boosted status with id %s: %s", status.BoostOfID, err) + } + status.BoostOf = boostedStatus + } + + if status.BoostOf != nil { + // return relevant accounts for the boosted status + boostedRelAccts, err := f.relevantAccounts(status.BoostOf, false) // false because we don't want to recurse + if err != nil { + return nil, fmt.Errorf("relevantAccounts: error getting relevant accounts of boosted status %s: %s", status.BoostOf.ID, err) + } + relAccts.BoostedAccount = boostedRelAccts.Account + relAccts.BoostedInReplyToAccount = boostedRelAccts.InReplyToAccount + relAccts.BoostedMentionedAccounts = boostedRelAccts.MentionedAccounts + } + } + + return relAccts, nil +} + +// domainBlockedRelevant checks through all relevant accounts attached to a status +// to make sure none of them are domain blocked by this instance. +func (f *filter) domainBlockedRelevant(r *relevantAccounts) (bool, error) { + domains := []string{} + + if r.Account != nil { + domains = append(domains, r.Account.Domain) + } + + if r.InReplyToAccount != nil { + domains = append(domains, r.InReplyToAccount.Domain) + } + + for _, a := range r.MentionedAccounts { + if a != nil { + domains = append(domains, a.Domain) + } + } + + if r.BoostedAccount != nil { + domains = append(domains, r.BoostedAccount.Domain) + } + + if r.BoostedInReplyToAccount != nil { + domains = append(domains, r.BoostedInReplyToAccount.Domain) + } + + for _, a := range r.BoostedMentionedAccounts { + if a != nil { + domains = append(domains, a.Domain) + } + } + + return f.db.AreDomainsBlocked(domains) +} + +func idIn(id string, mentions []*gtsmodel.Mention) bool { + for _, m := range mentions { + if m == nil { + continue + } + if m.ID == id { + return true + } + } + return false +} + +func mentionIn(mention *gtsmodel.Mention, ids []string) bool { + if mention == nil { + return false + } + for _, i := range ids { + if mention.ID == i { + return true + } + } + return false +} diff --git a/internal/visibility/statushometimelineable.go b/internal/visibility/statushometimelineable.go index ecb1d6857..d1c553a77 100644 --- a/internal/visibility/statushometimelineable.go +++ b/internal/visibility/statushometimelineable.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package visibility import ( @@ -28,6 +46,13 @@ func (f *filter) StatusHometimelineable(targetStatus *gtsmodel.Status, timelineO return false, nil } + for _, m := range targetStatus.Mentions { + if m.TargetAccountID == timelineOwnerAccount.ID { + // if we're mentioned we should be able to see the post + return true, nil + } + } + // Don't timeline a status whose parent hasn't been dereferenced yet or can't be dereferenced. // If we have the reply to URI but don't have an ID for the replied-to account or the replied-to status in our database, we haven't dereferenced it yet. if targetStatus.InReplyToURI != "" && (targetStatus.InReplyToID == "" || targetStatus.InReplyToAccountID == "") { @@ -38,8 +63,8 @@ func (f *filter) StatusHometimelineable(targetStatus *gtsmodel.Status, timelineO if targetStatus.InReplyToID != "" { // pin the reply to status on to this status if it hasn't been done already if targetStatus.InReplyTo == nil { - rs := >smodel.Status{} - if err := f.db.GetByID(targetStatus.InReplyToID, rs); err != nil { + rs, err := f.db.GetStatusByID(targetStatus.InReplyToID) + if err != nil { return false, fmt.Errorf("StatusHometimelineable: error getting replied to status with id %s: %s", targetStatus.InReplyToID, err) } targetStatus.InReplyTo = rs @@ -47,8 +72,8 @@ func (f *filter) StatusHometimelineable(targetStatus *gtsmodel.Status, timelineO // pin the reply to account on to this status if it hasn't been done already if targetStatus.InReplyToAccount == nil { - ra := >smodel.Account{} - if err := f.db.GetByID(targetStatus.InReplyToAccountID, ra); err != nil { + ra, err := f.db.GetAccountByID(targetStatus.InReplyToAccountID) + if err != nil { return false, fmt.Errorf("StatusHometimelineable: error getting replied to account with id %s: %s", targetStatus.InReplyToAccountID, err) } targetStatus.InReplyToAccount = ra diff --git a/internal/visibility/statuspublictimelineable.go b/internal/visibility/statuspublictimelineable.go index d7f68faee..f07e06aae 100644 --- a/internal/visibility/statuspublictimelineable.go +++ b/internal/visibility/statuspublictimelineable.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package visibility import ( diff --git a/internal/visibility/statusvisible.go b/internal/visibility/statusvisible.go index 887f5c313..588114ed5 100644 --- a/internal/visibility/statusvisible.go +++ b/internal/visibility/statusvisible.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package visibility import ( @@ -16,10 +34,11 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount "statusID": targetStatus.ID, }) - relevantAccounts, err := f.pullRelevantAccountsFromStatus(targetStatus) + getBoosted := true + relevantAccounts, err := f.relevantAccounts(targetStatus, getBoosted) if err != nil { l.Debugf("error pulling relevant accounts for status %s: %s", targetStatus.ID, err) - return false, fmt.Errorf("error pulling relevant accounts for status %s: %s", targetStatus.ID, err) + return false, fmt.Errorf("StatusVisible: error pulling relevant accounts for status %s: %s", targetStatus.ID, err) } domainBlocked, err := f.domainBlockedRelevant(relevantAccounts) @@ -32,7 +51,7 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount return false, nil } - targetAccount := relevantAccounts.StatusAuthor + targetAccount := relevantAccounts.Account if targetAccount == nil { l.Trace("target account is not set") return false, nil @@ -117,8 +136,8 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount } // status replies to account id - if relevantAccounts.ReplyToAccount != nil && relevantAccounts.ReplyToAccount.ID != requestingAccount.ID { - if blocked, err := f.db.Blocked(relevantAccounts.ReplyToAccount.ID, requestingAccount.ID, true); err != nil { + if relevantAccounts.InReplyToAccount != nil && relevantAccounts.InReplyToAccount.ID != requestingAccount.ID { + if blocked, err := f.db.Blocked(relevantAccounts.InReplyToAccount.ID, requestingAccount.ID, true); err != nil { return false, err } else if blocked { l.Trace("a block exists between requesting account and reply to account") @@ -127,7 +146,7 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount // check reply to ID if targetStatus.InReplyToID != "" && (targetStatus.Visibility == gtsmodel.VisibilityFollowersOnly || targetStatus.Visibility == gtsmodel.VisibilityDirect) { - followsRepliedAccount, err := f.db.Follows(requestingAccount, relevantAccounts.ReplyToAccount) + followsRepliedAccount, err := f.db.Follows(requestingAccount, relevantAccounts.InReplyToAccount) if err != nil { return false, err } @@ -139,8 +158,8 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount } // status boosts accounts id - if relevantAccounts.BoostedStatusAuthor != nil { - if blocked, err := f.db.Blocked(relevantAccounts.BoostedStatusAuthor.ID, requestingAccount.ID, true); err != nil { + if relevantAccounts.BoostedAccount != nil { + if blocked, err := f.db.Blocked(relevantAccounts.BoostedAccount.ID, requestingAccount.ID, true); err != nil { return false, err } else if blocked { l.Trace("a block exists between requesting account and boosted account") @@ -149,8 +168,8 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount } // status boosts a reply to account id - if relevantAccounts.BoostedReplyToAccount != nil { - if blocked, err := f.db.Blocked(relevantAccounts.BoostedReplyToAccount.ID, requestingAccount.ID, true); err != nil { + if relevantAccounts.BoostedInReplyToAccount != nil { + if blocked, err := f.db.Blocked(relevantAccounts.BoostedInReplyToAccount.ID, requestingAccount.ID, true); err != nil { return false, err } else if blocked { l.Trace("a block exists between requesting account and boosted reply to account") diff --git a/internal/visibility/util.go b/internal/visibility/util.go deleted file mode 100644 index 91b250b3d..000000000 --- a/internal/visibility/util.go +++ /dev/null @@ -1,210 +0,0 @@ -package visibility - -import ( - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func (f *filter) pullRelevantAccountsFromStatus(targetStatus *gtsmodel.Status) (*relevantAccounts, error) { - accounts := &relevantAccounts{ - MentionedAccounts: []*gtsmodel.Account{}, - BoostedMentionedAccounts: []*gtsmodel.Account{}, - } - - // get the author account if it's not set on the status already - if targetStatus.Account == nil { - statusAuthor, err := f.db.GetAccountByID(targetStatus.AccountID) - if err == nil { - targetStatus.Account = statusAuthor - } - } - accounts.StatusAuthor = targetStatus.Account - - // now get all accounts with IDs that are mentioned in the status - if targetStatus.MentionIDs != nil && targetStatus.Mentions == nil { - mentions, err := f.db.GetMentions(targetStatus.MentionIDs) - if err == nil { - targetStatus.Mentions = mentions - } - } - - for _, m := range targetStatus.Mentions { - if m.TargetAccount == nil { - t, err := f.db.GetAccountByID(m.TargetAccountID) - if err == nil { - m.TargetAccount = t - } - } - accounts.MentionedAccounts = append(accounts.MentionedAccounts, m.TargetAccount) - } - - // get the replied to account if it's not set on the status already - if targetStatus.InReplyToAccountID != "" && targetStatus.InReplyToAccount == nil { - repliedToAccount, err := f.db.GetAccountByID(targetStatus.InReplyToAccountID) - if err == nil { - targetStatus.InReplyToAccount = repliedToAccount - } - } - accounts.ReplyToAccount = targetStatus.InReplyToAccount - - // get the boosted status if it's not set on the status already - if targetStatus.BoostOfID != "" && targetStatus.BoostOf == nil { - boostedStatus, err := f.db.GetStatusByID(targetStatus.BoostOfID) - if err == nil { - targetStatus.BoostOf = boostedStatus - } - } - - // get the boosted account if it's not set on the status already - if targetStatus.BoostOfAccountID != "" && targetStatus.BoostOfAccount == nil { - if targetStatus.BoostOf != nil && targetStatus.BoostOf.Account != nil { - targetStatus.BoostOfAccount = targetStatus.BoostOf.Account - } else { - boostedAccount, err := f.db.GetAccountByID(targetStatus.BoostOfAccountID) - if err == nil { - targetStatus.BoostOfAccount = boostedAccount - } - } - } - accounts.BoostedStatusAuthor = targetStatus.BoostOfAccount - - if targetStatus.BoostOf != nil { - // the boosted status might be a reply to another account so we should get that too - if targetStatus.BoostOf.InReplyToAccountID != "" && targetStatus.BoostOf.InReplyToAccount == nil { - boostOfInReplyToAccount, err := f.db.GetAccountByID(targetStatus.BoostOf.InReplyToAccountID) - if err == nil { - targetStatus.BoostOf.InReplyToAccount = boostOfInReplyToAccount - } - } - accounts.BoostedReplyToAccount = targetStatus.BoostOf.InReplyToAccount - - // now get all accounts with IDs that are mentioned in the boosted status - if targetStatus.BoostOf.MentionIDs != nil && targetStatus.BoostOf.Mentions == nil { - mentions, err := f.db.GetMentions(targetStatus.BoostOf.MentionIDs) - if err == nil { - targetStatus.BoostOf.Mentions = mentions - } - } - - for _, m := range targetStatus.BoostOf.Mentions { - if m.TargetAccount == nil { - t, err := f.db.GetAccountByID(m.TargetAccountID) - if err == nil { - m.TargetAccount = t - } - } - accounts.BoostedMentionedAccounts = append(accounts.BoostedMentionedAccounts, m.TargetAccount) - } - } - - return accounts, nil -} - -// relevantAccounts denotes accounts that are replied to, boosted by, or mentioned in a status. -type relevantAccounts struct { - // Who wrote the status - StatusAuthor *gtsmodel.Account - // Who is the status replying to - ReplyToAccount *gtsmodel.Account - // Which accounts are mentioned (tagged) in the status - MentionedAccounts []*gtsmodel.Account - // Who authed the boosted status - BoostedStatusAuthor *gtsmodel.Account - // If the boosted status replies to another account, who does it reply to? - BoostedReplyToAccount *gtsmodel.Account - // Who is mentioned (tagged) in the boosted status - BoostedMentionedAccounts []*gtsmodel.Account -} - -// blockedDomain checks whether the given domain is blocked by us or not -func (f *filter) blockedDomain(host string) (bool, error) { - b := >smodel.DomainBlock{} - err := f.db.GetWhere([]db.Where{{Key: "domain", Value: host, CaseInsensitive: true}}, b) - if err == nil { - // block exists - return true, nil - } - - if err == db.ErrNoEntries { - // there are no entries so there's no block - return false, nil - } - - // there's an actual error - return false, err -} - -// domainBlockedRelevant checks through all relevant accounts attached to a status -// to make sure none of them are domain blocked by this instance. -// -// Will return true+nil if there's a block, false+nil if there's no block, or -// an error if something goes wrong. -func (f *filter) domainBlockedRelevant(r *relevantAccounts) (bool, error) { - if r.StatusAuthor != nil { - b, err := f.blockedDomain(r.StatusAuthor.Domain) - if err != nil { - return false, err - } - if b { - return true, nil - } - } - - if r.ReplyToAccount != nil { - b, err := f.blockedDomain(r.ReplyToAccount.Domain) - if err != nil { - return false, err - } - if b { - return true, nil - } - } - - for _, a := range r.MentionedAccounts { - if a == nil { - continue - } - b, err := f.blockedDomain(a.Domain) - if err != nil { - return false, err - } - if b { - return true, nil - } - } - - if r.BoostedStatusAuthor != nil { - b, err := f.blockedDomain(r.BoostedStatusAuthor.Domain) - if err != nil { - return false, err - } - if b { - return true, nil - } - } - - if r.BoostedReplyToAccount != nil { - b, err := f.blockedDomain(r.BoostedReplyToAccount.Domain) - if err != nil { - return false, err - } - if b { - return true, nil - } - } - - for _, a := range r.BoostedMentionedAccounts { - if a == nil { - continue - } - b, err := f.blockedDomain(a.Domain) - if err != nil { - return false, err - } - if b { - return true, nil - } - } - - return false, nil -} diff --git a/testrig/testmodels.go b/testrig/testmodels.go index 5e1906629..220a3d5ac 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -793,10 +793,10 @@ func NewTestStatuses() map[string]*gtsmodel.Status { URI: "http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R", URL: "http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R", Content: "hello world! #welcome ! first post on the instance :rainbow: !", - AttachmentIDs: []string{"01F8MH6NEM8D7527KZAECTCR76"}, - TagIDs: []string{"01F8MHA1A2NF9MJ3WCCQ3K8BSZ"}, - MentionIDs: []string{}, - EmojiIDs: []string{"01F8MH9H8E4VG3KDYJR9EGPXCQ"}, + AttachmentIDs: []string{"01F8MH6NEM8D7527KZAECTCR76"}, + TagIDs: []string{"01F8MHA1A2NF9MJ3WCCQ3K8BSZ"}, + MentionIDs: []string{}, + EmojiIDs: []string{"01F8MH9H8E4VG3KDYJR9EGPXCQ"}, CreatedAt: time.Now().Add(-71 * time.Hour), UpdatedAt: time.Now().Add(-71 * time.Hour), Local: true, @@ -917,7 +917,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { URI: "http://localhost:8080/users/the_mighty_zork/statuses/01F8MH82FYRXD2RC6108DAJ5HB", URL: "http://localhost:8080/@the_mighty_zork/statuses/01F8MH82FYRXD2RC6108DAJ5HB", Content: "here's a little gif of trent", - AttachmentIDs: []string{"01F8MH7TDVANYKWVE8VVKFPJTJ"}, + AttachmentIDs: []string{"01F8MH7TDVANYKWVE8VVKFPJTJ"}, CreatedAt: time.Now().Add(-1 * time.Hour), UpdatedAt: time.Now().Add(-1 * time.Hour), Local: true, @@ -942,7 +942,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { URI: "http://localhost:8080/users/the_mighty_zork/statuses/01FCTA44PW9H1TB328S9AQXKDS", URL: "http://localhost:8080/@the_mighty_zork/statuses/01FCTA44PW9H1TB328S9AQXKDS", Content: "hi!", - AttachmentIDs: []string{}, + AttachmentIDs: []string{}, CreatedAt: time.Now().Add(-1 * time.Minute), UpdatedAt: time.Now().Add(-1 * time.Minute), Local: true, @@ -1062,10 +1062,11 @@ func NewTestStatuses() map[string]*gtsmodel.Status { ID: "01FCQSQ667XHJ9AV9T27SJJSX5", URI: "http://localhost:8080/users/1happyturtle/statuses/01FCQSQ667XHJ9AV9T27SJJSX5", URL: "http://localhost:8080/@1happyturtle/statuses/01FCQSQ667XHJ9AV9T27SJJSX5", - Content: "🐢 hi zork! 🐢", + Content: "🐢 @the_mighty_zork hi zork! 🐢", CreatedAt: time.Now().Add(-1 * time.Minute), UpdatedAt: time.Now().Add(-1 * time.Minute), Local: true, + MentionIDs: []string{"01FDF2HM2NF6FSRZCDEDV451CN"}, AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF", InReplyToID: "01F8MHAMCHF6Y650WCRSCP4WMY", InReplyToAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF", @@ -1119,17 +1120,29 @@ func NewTestTags() map[string]*gtsmodel.Tag { func NewTestMentions() map[string]*gtsmodel.Mention { return map[string]*gtsmodel.Mention{ "zork_mention_foss_satan": { - ID: "01FCTA2Y6FGHXQA4ZE6N5NMNEX", - StatusID: "01FCTA44PW9H1TB328S9AQXKDS", - CreatedAt: time.Now().Add(-1 * time.Minute), - UpdatedAt: time.Now().Add(-1 * time.Minute), - OriginAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF", - OriginAccountURI: "http://localhost:8080/users/the_mighty_zork", - TargetAccountID: "01F8MH5ZK5VRH73AKHQM6Y9VNX", - NameString: "@foss_satan@fossbros-anonymous.io", + ID: "01FCTA2Y6FGHXQA4ZE6N5NMNEX", + StatusID: "01FCTA44PW9H1TB328S9AQXKDS", + CreatedAt: time.Now().Add(-1 * time.Minute), + UpdatedAt: time.Now().Add(-1 * time.Minute), + OriginAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF", + OriginAccountURI: "http://localhost:8080/users/the_mighty_zork", + TargetAccountID: "01F8MH5ZK5VRH73AKHQM6Y9VNX", + NameString: "@foss_satan@fossbros-anonymous.io", TargetAccountURI: "http://fossbros-anonymous.io/users/foss_satan", TargetAccountURL: "http://fossbros-anonymous.io/@foss_satan", }, + "local_user_2_mention_zork": { + ID: "01FDF2HM2NF6FSRZCDEDV451CN", + StatusID: "01FCQSQ667XHJ9AV9T27SJJSX5", + CreatedAt: time.Now().Add(-1 * time.Minute), + UpdatedAt: time.Now().Add(-1 * time.Minute), + OriginAccountID: "01F8MH5NBDF2MV7CTC4Q5128HF", + OriginAccountURI: "http://localhost:8080/users/1happyturtle", + TargetAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF", + NameString: "@the_mighty_zork", + TargetAccountURI: "http://localhost:8080/users/the_mighty_zork", + TargetAccountURL: "http://localhost:8080/@the_mighty_zork", + }, } }